Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java
new file mode 100644
index 0000000..170582b
--- /dev/null
+++ b/android/widget/AbsListView.java
@@ -0,0 +1,7687 @@
+/*
+ * 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.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+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.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.StateSet;
+import android.view.ActionMode;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.RemoteViews.OnClickHandler;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class that can be used to implement virtualized lists of items. A list does
+ * not have a spatial definition here. For instance, subclases of this class can
+ * display the content of the list in a grid, in a carousel, as stack, etc.
+ *
+ * @attr ref android.R.styleable#AbsListView_listSelector
+ * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+ * @attr ref android.R.styleable#AbsListView_stackFromBottom
+ * @attr ref android.R.styleable#AbsListView_scrollingCache
+ * @attr ref android.R.styleable#AbsListView_textFilterEnabled
+ * @attr ref android.R.styleable#AbsListView_transcriptMode
+ * @attr ref android.R.styleable#AbsListView_cacheColorHint
+ * @attr ref android.R.styleable#AbsListView_fastScrollEnabled
+ * @attr ref android.R.styleable#AbsListView_smoothScrollbar
+ * @attr ref android.R.styleable#AbsListView_choiceMode
+ */
+public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
+ ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
+ ViewTreeObserver.OnTouchModeChangeListener,
+ RemoteViewsAdapter.RemoteAdapterConnectionCallback {
+
+ @SuppressWarnings("UnusedDeclaration")
+ private static final String TAG = "AbsListView";
+
+ /**
+ * 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;
+
+ /**
+ * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end.
+ */
+ static final int TOUCH_MODE_OVERSCROLL = 5;
+
+ /**
+ * Indicates the view is being flung outside of normal content bounds
+ * and will spring back.
+ */
+ static final int TOUCH_MODE_OVERFLING = 6;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * The list allows multiple choices in a modal selection mode
+ */
+ public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
+
+ /**
+ * The thread that created this view.
+ */
+ private final Thread mOwnerThread;
+
+ /**
+ * Controls if/how the user may choose/check items in the list
+ */
+ int mChoiceMode = CHOICE_MODE_NONE;
+
+ /**
+ * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
+ */
+ ActionMode mChoiceActionMode;
+
+ /**
+ * Wrapper for the multiple choice mode callback; AbsListView needs to perform
+ * a few extra actions around what application code does.
+ */
+ MultiChoiceModeWrapper mMultiChoiceModeCallback;
+
+ /**
+ * Running count of how many items are currently checked
+ */
+ int mCheckedItemCount;
+
+ /**
+ * Running state of which positions are currently checked
+ */
+ SparseBooleanArray mCheckStates;
+
+ /**
+ * Running state of which IDs are currently checked.
+ * If there is a value for a given key, the checked state for that ID is true
+ * and the value holds the last known position in the adapter for that id.
+ */
+ LongSparseArray<Integer> mCheckedIdStates;
+
+ /**
+ * 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;
+
+ /**
+ * The remote adapter containing the data to be displayed by this view to be set
+ */
+ private RemoteViewsAdapter mRemoteAdapter;
+
+ /**
+ * If mAdapter != null, whenever this is true the adapter has stable IDs.
+ */
+ boolean mAdapterHasStableIds;
+
+ /**
+ * This flag indicates the a full notify is required when the RemoteViewsAdapter connects
+ */
+ private boolean mDeferNotifyDataSetChanged = false;
+
+ /**
+ * 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;
+
+ /**
+ * The current position of the selector in the list.
+ */
+ int mSelectorPosition = INVALID_POSITION;
+
+ /**
+ * 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;
+ boolean mCachingActive;
+
+ /**
+ * 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;
+
+ /**
+ * Handles scrolling between positions within the list.
+ */
+ AbsPositionScroller mPositionScroller;
+
+ /**
+ * The offset in pixels form the top of the AdapterView to the top
+ * of the currently selected view. Used to save and restore state.
+ */
+ int mSelectedTop = 0;
+
+ /**
+ * Indicates whether the list is stacked from the bottom edge or
+ * the top edge.
+ */
+ boolean mStackFromBottom;
+
+ /**
+ * When set to true, the list automatically discards the children's
+ * bitmap cache after scrolling.
+ */
+ boolean mScrollingCacheEnabled;
+
+ /**
+ * Whether or not to enable the fast scroll feature on this list
+ */
+ boolean mFastScrollEnabled;
+
+ /**
+ * Whether or not to always show the fast scroll feature on this list
+ */
+ boolean mFastScrollAlwaysVisible;
+
+ /**
+ * 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 whether to use pixels-based or position-based scrollbar
+ * properties.
+ */
+ private boolean mSmoothScrollbarEnabled = true;
+
+ /**
+ * 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;
+
+ /**
+ * Maximum distance to record overscroll
+ */
+ int mOverscrollMax;
+
+ /**
+ * Content height divided by this is the overscroll limit.
+ */
+ static final int OVERSCROLL_LIMIT_DIVISOR = 3;
+
+ /**
+ * How many positions in either direction we will search to try to
+ * find a checked item with a stable ID that moved position across
+ * a data set change. If the item isn't found it will be unselected.
+ */
+ private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
+
+ /**
+ * Used to request a layout when we changed touch mode
+ */
+ private static final int TOUCH_MODE_UNKNOWN = -1;
+ private static final int TOUCH_MODE_ON = 0;
+ private static final int TOUCH_MODE_OFF = 1;
+
+ private int mLastTouchMode = TOUCH_MODE_UNKNOWN;
+
+ private static final boolean PROFILE_SCROLLING = false;
+ private boolean mScrollProfilingStarted = false;
+
+ private static final boolean PROFILE_FLINGING = false;
+ private boolean mFlingProfilingStarted = false;
+
+ /**
+ * The StrictMode "critical time span" objects to catch animation
+ * stutters. Non-null when a time-sensitive animation is
+ * in-flight. Must call finish() on them when done animating.
+ * These are no-ops on user builds.
+ */
+ private StrictMode.Span mScrollStrictSpan = null;
+ private StrictMode.Span mFlingStrictSpan = null;
+
+ /**
+ * The last CheckForLongPress runnable we posted, if any
+ */
+ private CheckForLongPress mPendingCheckForLongPress;
+
+ /**
+ * The last CheckForTap runnable we posted, if any
+ */
+ private CheckForTap mPendingCheckForTap;
+
+ /**
+ * The last CheckForKeyLongPress runnable we posted, if any
+ */
+ private CheckForKeyLongPress mPendingCheckForKeyLongPress;
+
+ /**
+ * Acts upon click
+ */
+ private AbsListView.PerformClick mPerformClick;
+
+ /**
+ * Delayed action for touch mode.
+ */
+ private Runnable mTouchModeReset;
+
+ /**
+ * Whether the most recent touch event stream resulted in a successful
+ * long-press action. This is reset on TOUCH_DOWN.
+ */
+ private boolean mHasPerformedLongPress;
+
+ /**
+ * 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 cached drawable state for the selector. Accounts for child enabled
+ * state, but otherwise identical to the view's own drawable state.
+ */
+ private int[] mSelectorState;
+
+ /**
+ * The last scroll state reported to clients through {@link OnScrollListener}.
+ */
+ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+ /**
+ * Helper object that renders and controls the fast scroll thumb.
+ */
+ private FastScroller mFastScroll;
+
+ /**
+ * Temporary holder for fast scroller style until a FastScroller object
+ * is created.
+ */
+ private int mFastScrollStyle;
+
+ private boolean mGlobalLayoutListenerAddedFilter;
+
+ private int mTouchSlop;
+ private float mDensityScale;
+
+ private float mVerticalScrollFactor;
+
+ private InputConnection mDefInputConnection;
+ private InputConnectionWrapper mPublicInputConnection;
+
+ private Runnable mClearScrollingCache;
+ Runnable mPositionScrollAfterLayout;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+ private float mVelocityScale = 1.0f;
+
+ final boolean[] mIsScrap = new boolean[1];
+
+ private final int[] mScrollOffset = new int[2];
+ private final int[] mScrollConsumed = new int[2];
+
+ private final float[] mTmpPoint = new float[2];
+
+ // Used for offsetting MotionEvents that we feed to the VelocityTracker.
+ // In the future it would be nice to be able to give this to the VelocityTracker
+ // directly, or alternatively put a VT into absolute-positioning mode that only
+ // reads the raw screen-coordinate x/y values.
+ private int mNestedYOffset = 0;
+
+ // True when the popup should be hidden because of a call to
+ // dispatchDisplayHint()
+ private boolean mPopupHidden;
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * Maximum distance to overscroll by during edge effects
+ */
+ int mOverscrollDistance;
+
+ /**
+ * Maximum distance to overfling during edge effects
+ */
+ int mOverflingDistance;
+
+ // These two EdgeGlows are always set and used together.
+ // Checking one for null is as good as checking both.
+
+ /**
+ * Tracks the state of the top edge glow.
+ */
+ private EdgeEffect mEdgeGlowTop;
+
+ /**
+ * Tracks the state of the bottom edge glow.
+ */
+ private EdgeEffect mEdgeGlowBottom;
+
+ /**
+ * An estimate of how many pixels are between the top of the list and
+ * the top of the first position in the adapter, based on the last time
+ * we saw it. Used to hint where to draw edge glows.
+ */
+ private int mFirstPositionDistanceGuess;
+
+ /**
+ * An estimate of how many pixels are between the bottom of the list and
+ * the bottom of the last position in the adapter, based on the last time
+ * we saw it. Used to hint where to draw edge glows.
+ */
+ private int mLastPositionDistanceGuess;
+
+ /**
+ * Used for determining when to cancel out of overscroll.
+ */
+ private int mDirection = 0;
+
+ /**
+ * Tracked on measurement in transcript mode. Makes sure that we can still pin to
+ * the bottom correctly on resizes.
+ */
+ private boolean mForceTranscriptScroll;
+
+ /**
+ * Used for interacting with list items from an accessibility service.
+ */
+ private ListItemAccessibilityDelegate mAccessibilityDelegate;
+
+ private int mLastAccessibilityScrollEventFromIndex;
+ private int mLastAccessibilityScrollEventToIndex;
+
+ /**
+ * Track the item count from the last time we handled a data change.
+ */
+ private int mLastHandledItemCount;
+
+ /**
+ * Used for smooth scrolling at a consistent rate
+ */
+ static final Interpolator sLinearInterpolator = new LinearInterpolator();
+
+ /**
+ * The saved state that we will be restoring from when we next sync.
+ * Kept here so that if we happen to be asked to save our state before
+ * the sync happens, we can return this existing data rather than losing
+ * it.
+ */
+ private SavedState mPendingSync;
+
+ /**
+ * Whether the view is in the process of detaching from its window.
+ */
+ private boolean mIsDetaching;
+
+ /**
+ * 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_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);
+ }
+
+ /**
+ * The top-level view of a list item can implement this interface to allow
+ * itself to modify the bounds of the selection shown for that item.
+ */
+ public interface SelectionBoundsAdjuster {
+ /**
+ * Called to allow the list item to adjust the bounds shown for
+ * its selection.
+ *
+ * @param bounds On call, this contains the bounds the list has
+ * selected for the item (that is the bounds of the entire view). The
+ * values can be modified as desired.
+ */
+ public void adjustListItemSelectionBounds(Rect bounds);
+ }
+
+ public AbsListView(Context context) {
+ super(context);
+ initAbsListView();
+
+ mOwnerThread = Thread.currentThread();
+
+ setVerticalScrollBarEnabled(true);
+ TypedArray a = context.obtainStyledAttributes(R.styleable.View);
+ initializeScrollbarsInternal(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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initAbsListView();
+
+ mOwnerThread = Thread.currentThread();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.AbsListView, defStyleAttr, defStyleRes);
+
+ final Drawable selector = a.getDrawable(R.styleable.AbsListView_listSelector);
+ if (selector != null) {
+ setSelector(selector);
+ }
+
+ mDrawSelectorOnTop = a.getBoolean(R.styleable.AbsListView_drawSelectorOnTop, false);
+
+ setStackFromBottom(a.getBoolean(
+ R.styleable.AbsListView_stackFromBottom, false));
+ setScrollingCacheEnabled(a.getBoolean(
+ R.styleable.AbsListView_scrollingCache, true));
+ setTextFilterEnabled(a.getBoolean(
+ R.styleable.AbsListView_textFilterEnabled, false));
+ setTranscriptMode(a.getInt(
+ R.styleable.AbsListView_transcriptMode, TRANSCRIPT_MODE_DISABLED));
+ setCacheColorHint(a.getColor(
+ R.styleable.AbsListView_cacheColorHint, 0));
+ setSmoothScrollbarEnabled(a.getBoolean(
+ R.styleable.AbsListView_smoothScrollbar, true));
+ setChoiceMode(a.getInt(
+ R.styleable.AbsListView_choiceMode, CHOICE_MODE_NONE));
+
+ setFastScrollEnabled(a.getBoolean(
+ R.styleable.AbsListView_fastScrollEnabled, false));
+ setFastScrollStyle(a.getResourceId(
+ R.styleable.AbsListView_fastScrollStyle, 0));
+ setFastScrollAlwaysVisible(a.getBoolean(
+ R.styleable.AbsListView_fastScrollAlwaysVisible, false));
+
+ a.recycle();
+
+ if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+ setRevealOnFocusHint(false);
+ }
+ }
+
+ private void initAbsListView() {
+ // Setting focusable in touch mode will set the focusable property to true
+ setClickable(true);
+ setFocusableInTouchMode(true);
+ setWillNotDraw(false);
+ setAlwaysDrawnWithCacheEnabled(false);
+ setScrollingCacheEnabled(true);
+
+ final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+
+ mDensityScale = getContext().getResources().getDisplayMetrics().density;
+ }
+
+ @Override
+ public void setOverScrollMode(int mode) {
+ if (mode != OVER_SCROLL_NEVER) {
+ if (mEdgeGlowTop == null) {
+ Context context = getContext();
+ mEdgeGlowTop = new EdgeEffect(context);
+ mEdgeGlowBottom = new EdgeEffect(context);
+ }
+ } else {
+ mEdgeGlowTop = null;
+ mEdgeGlowBottom = null;
+ }
+ super.setOverScrollMode(mode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (adapter != null) {
+ mAdapterHasStableIds = mAdapter.hasStableIds();
+ if (mChoiceMode != CHOICE_MODE_NONE && mAdapterHasStableIds &&
+ mCheckedIdStates == null) {
+ mCheckedIdStates = new LongSparseArray<Integer>();
+ }
+ }
+ clearChoices();
+ }
+
+ /**
+ * Returns the number of items currently selected. This will only be valid
+ * if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
+ *
+ * <p>To determine the specific items that are currently selected, use one of
+ * the <code>getChecked*</code> methods.
+ *
+ * @return The number of items currently selected
+ *
+ * @see #getCheckedItemPosition()
+ * @see #getCheckedItemPositions()
+ * @see #getCheckedItemIds()
+ */
+ public int getCheckedItemCount() {
+ return mCheckedItemCount;
+ }
+
+ /**
+ * Returns the checked state of the specified position. The result is only
+ * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
+ * or {@link #CHOICE_MODE_MULTIPLE}.
+ *
+ * @param position The item whose checked state to return
+ * @return The item's checked state or <code>false</code> if choice mode
+ * is invalid
+ *
+ * @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 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_NONE}.
+ *
+ * @return A SparseBooleanArray which will return true for each call to
+ * get(int position) where position is a checked position in the
+ * list and false otherwise, or <code>null</code> if the choice
+ * mode is set to {@link #CHOICE_MODE_NONE}.
+ */
+ public SparseBooleanArray getCheckedItemPositions() {
+ if (mChoiceMode != CHOICE_MODE_NONE) {
+ return mCheckStates;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the set of checked items ids. The result is only valid if the
+ * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
+ * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
+ *
+ * @return A new array which contains the id of each checked item in the
+ * list.
+ */
+ public long[] getCheckedItemIds() {
+ if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) {
+ return new long[0];
+ }
+
+ final LongSparseArray<Integer> idStates = mCheckedIdStates;
+ final int count = idStates.size();
+ final long[] ids = new long[count];
+
+ for (int i = 0; i < count; i++) {
+ ids[i] = idStates.keyAt(i);
+ }
+
+ return ids;
+ }
+
+ /**
+ * Clear any choices previously set
+ */
+ public void clearChoices() {
+ if (mCheckStates != null) {
+ mCheckStates.clear();
+ }
+ if (mCheckedIdStates != null) {
+ mCheckedIdStates.clear();
+ }
+ mCheckedItemCount = 0;
+ }
+
+ /**
+ * Sets the checked state of the specified position. The is only valid if
+ * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
+ * {@link #CHOICE_MODE_MULTIPLE}.
+ *
+ * @param position The item whose checked state is to be checked
+ * @param value The new checked state for the item
+ */
+ public void setItemChecked(int position, boolean value) {
+ if (mChoiceMode == CHOICE_MODE_NONE) {
+ return;
+ }
+
+ // Start selection mode if needed. We don't need to if we're unchecking something.
+ if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
+ if (mMultiChoiceModeCallback == null ||
+ !mMultiChoiceModeCallback.hasWrappedCallback()) {
+ throw new IllegalStateException("AbsListView: attempted to start selection mode " +
+ "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
+ "supplied. Call setMultiChoiceModeListener to set a callback.");
+ }
+ mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
+ }
+
+ final boolean itemCheckChanged;
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+ boolean oldValue = mCheckStates.get(position);
+ mCheckStates.put(position, value);
+ if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+ if (value) {
+ mCheckedIdStates.put(mAdapter.getItemId(position), position);
+ } else {
+ mCheckedIdStates.delete(mAdapter.getItemId(position));
+ }
+ }
+ itemCheckChanged = oldValue != value;
+ if (itemCheckChanged) {
+ if (value) {
+ mCheckedItemCount++;
+ } else {
+ mCheckedItemCount--;
+ }
+ }
+ if (mChoiceActionMode != null) {
+ final long id = mAdapter.getItemId(position);
+ mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+ position, id, value);
+ }
+ } else {
+ boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
+ // Clear all values if we're checking something, or unchecking the currently
+ // selected item
+ itemCheckChanged = isItemChecked(position) != value;
+ if (value || isItemChecked(position)) {
+ mCheckStates.clear();
+ if (updateIds) {
+ mCheckedIdStates.clear();
+ }
+ }
+ // this may end up selecting the value we just cleared but this way
+ // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
+ if (value) {
+ mCheckStates.put(position, true);
+ if (updateIds) {
+ mCheckedIdStates.put(mAdapter.getItemId(position), position);
+ }
+ mCheckedItemCount = 1;
+ } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
+ mCheckedItemCount = 0;
+ }
+ }
+
+ // Do not generate a data change while we are in the layout phase or data has not changed
+ if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) {
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+ }
+ }
+
+ @Override
+ public boolean performItemClick(View view, int position, long id) {
+ boolean handled = false;
+ boolean dispatchItemClick = true;
+
+ if (mChoiceMode != CHOICE_MODE_NONE) {
+ handled = true;
+ boolean checkedStateChanged = false;
+
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE ||
+ (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
+ boolean checked = !mCheckStates.get(position, false);
+ mCheckStates.put(position, checked);
+ if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+ if (checked) {
+ mCheckedIdStates.put(mAdapter.getItemId(position), position);
+ } else {
+ mCheckedIdStates.delete(mAdapter.getItemId(position));
+ }
+ }
+ if (checked) {
+ mCheckedItemCount++;
+ } else {
+ mCheckedItemCount--;
+ }
+ if (mChoiceActionMode != null) {
+ mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+ position, id, checked);
+ dispatchItemClick = false;
+ }
+ checkedStateChanged = true;
+ } else if (mChoiceMode == CHOICE_MODE_SINGLE) {
+ boolean checked = !mCheckStates.get(position, false);
+ if (checked) {
+ mCheckStates.clear();
+ mCheckStates.put(position, true);
+ if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+ mCheckedIdStates.clear();
+ mCheckedIdStates.put(mAdapter.getItemId(position), position);
+ }
+ mCheckedItemCount = 1;
+ } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
+ mCheckedItemCount = 0;
+ }
+ checkedStateChanged = true;
+ }
+
+ if (checkedStateChanged) {
+ updateOnScreenCheckedViews();
+ }
+ }
+
+ if (dispatchItemClick) {
+ handled |= super.performItemClick(view, position, id);
+ }
+
+ return handled;
+ }
+
+ /**
+ * Perform a quick, in-place update of the checked or activated state
+ * on all visible item views. This should only be called when a valid
+ * choice mode is active.
+ */
+ private void updateOnScreenCheckedViews() {
+ final int firstPos = mFirstPosition;
+ final int count = getChildCount();
+ final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
+ >= android.os.Build.VERSION_CODES.HONEYCOMB;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final int position = firstPos + i;
+
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(mCheckStates.get(position));
+ } else if (useActivated) {
+ child.setActivated(mCheckStates.get(position));
+ }
+ }
+ }
+
+ /**
+ * @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 (mChoiceActionMode != null) {
+ mChoiceActionMode.finish();
+ mChoiceActionMode = null;
+ }
+ if (mChoiceMode != CHOICE_MODE_NONE) {
+ if (mCheckStates == null) {
+ mCheckStates = new SparseBooleanArray(0);
+ }
+ if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
+ mCheckedIdStates = new LongSparseArray<Integer>(0);
+ }
+ // Modal multi-choice mode only has choices when the mode is active. Clear them.
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+ clearChoices();
+ setLongClickable(true);
+ }
+ }
+ }
+
+ /**
+ * Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the
+ * selection {@link ActionMode}. Only used when the choice mode is set to
+ * {@link #CHOICE_MODE_MULTIPLE_MODAL}.
+ *
+ * @param listener Listener that will manage the selection mode
+ *
+ * @see #setChoiceMode(int)
+ */
+ public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
+ if (mMultiChoiceModeCallback == null) {
+ mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
+ }
+ mMultiChoiceModeCallback.setWrapped(listener);
+ }
+
+ /**
+ * @return true if all list content currently fits within the view boundaries
+ */
+ private boolean contentFits() {
+ final int childCount = getChildCount();
+ if (childCount == 0) return true;
+ if (childCount != mItemCount) return false;
+
+ return getChildAt(0).getTop() >= mListPadding.top &&
+ getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom;
+ }
+
+ /**
+ * Specifies whether fast scrolling is enabled or disabled.
+ * <p>
+ * When fast scrolling is enabled, the user can quickly scroll through lists
+ * by dragging the fast scroll thumb.
+ * <p>
+ * If the adapter backing this list implements {@link SectionIndexer}, the
+ * fast scroller will display section header previews as the user scrolls.
+ * Additionally, the user will be able to quickly jump between sections by
+ * tapping along the length of the scroll bar.
+ *
+ * @see SectionIndexer
+ * @see #isFastScrollEnabled()
+ * @param enabled true to enable fast scrolling, false otherwise
+ */
+ public void setFastScrollEnabled(final boolean enabled) {
+ if (mFastScrollEnabled != enabled) {
+ mFastScrollEnabled = enabled;
+
+ if (isOwnerThread()) {
+ setFastScrollerEnabledUiThread(enabled);
+ } else {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ setFastScrollerEnabledUiThread(enabled);
+ }
+ });
+ }
+ }
+ }
+
+ private void setFastScrollerEnabledUiThread(boolean enabled) {
+ if (mFastScroll != null) {
+ mFastScroll.setEnabled(enabled);
+ } else if (enabled) {
+ mFastScroll = new FastScroller(this, mFastScrollStyle);
+ mFastScroll.setEnabled(true);
+ }
+
+ resolvePadding();
+
+ if (mFastScroll != null) {
+ mFastScroll.updateLayout();
+ }
+ }
+
+ /**
+ * Specifies the style of the fast scroller decorations.
+ *
+ * @param styleResId style resource containing fast scroller properties
+ * @see android.R.styleable#FastScroll
+ */
+ public void setFastScrollStyle(int styleResId) {
+ if (mFastScroll == null) {
+ mFastScrollStyle = styleResId;
+ } else {
+ mFastScroll.setStyle(styleResId);
+ }
+ }
+
+ /**
+ * Set whether or not the fast scroller should always be shown in place of
+ * the standard scroll bars. This will enable fast scrolling if it is not
+ * already enabled.
+ * <p>
+ * Fast scrollers shown in this way will not fade out and will be a
+ * permanent fixture within the list. This is best combined with an inset
+ * scroll bar style to ensure the scroll bar does not overlap content.
+ *
+ * @param alwaysShow true if the fast scroller should always be displayed,
+ * false otherwise
+ * @see #setScrollBarStyle(int)
+ * @see #setFastScrollEnabled(boolean)
+ */
+ public void setFastScrollAlwaysVisible(final boolean alwaysShow) {
+ if (mFastScrollAlwaysVisible != alwaysShow) {
+ if (alwaysShow && !mFastScrollEnabled) {
+ setFastScrollEnabled(true);
+ }
+
+ mFastScrollAlwaysVisible = alwaysShow;
+
+ if (isOwnerThread()) {
+ setFastScrollerAlwaysVisibleUiThread(alwaysShow);
+ } else {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ setFastScrollerAlwaysVisibleUiThread(alwaysShow);
+ }
+ });
+ }
+ }
+ }
+
+ private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) {
+ if (mFastScroll != null) {
+ mFastScroll.setAlwaysShow(alwaysShow);
+ }
+ }
+
+ /**
+ * @return whether the current thread is the one that created the view
+ */
+ private boolean isOwnerThread() {
+ return mOwnerThread == Thread.currentThread();
+ }
+
+ /**
+ * Returns true if the fast scroller is set to always show on this view.
+ *
+ * @return true if the fast scroller will always show
+ * @see #setFastScrollAlwaysVisible(boolean)
+ */
+ public boolean isFastScrollAlwaysVisible() {
+ if (mFastScroll == null) {
+ return mFastScrollEnabled && mFastScrollAlwaysVisible;
+ } else {
+ return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled();
+ }
+ }
+
+ @Override
+ public int getVerticalScrollbarWidth() {
+ if (mFastScroll != null && mFastScroll.isEnabled()) {
+ return Math.max(super.getVerticalScrollbarWidth(), mFastScroll.getWidth());
+ }
+ return super.getVerticalScrollbarWidth();
+ }
+
+ /**
+ * Returns true if the fast scroller is enabled.
+ *
+ * @see #setFastScrollEnabled(boolean)
+ * @return true if fast scroll is enabled, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isFastScrollEnabled() {
+ if (mFastScroll == null) {
+ return mFastScrollEnabled;
+ } else {
+ return mFastScroll.isEnabled();
+ }
+ }
+
+ @Override
+ public void setVerticalScrollbarPosition(int position) {
+ super.setVerticalScrollbarPosition(position);
+ if (mFastScroll != null) {
+ mFastScroll.setScrollbarPosition(position);
+ }
+ }
+
+ @Override
+ public void setScrollBarStyle(int style) {
+ super.setScrollBarStyle(style);
+ if (mFastScroll != null) {
+ mFastScroll.setScrollBarStyle(style);
+ }
+ }
+
+ /**
+ * If fast scroll is enabled, then don't draw the vertical scrollbar.
+ * @hide
+ */
+ @Override
+ protected boolean isVerticalScrollBarHidden() {
+ return isFastScrollEnabled();
+ }
+
+ /**
+ * When smooth scrollbar is enabled, the position and size of the scrollbar thumb
+ * is computed based on the number of visible pixels in the visible items. This
+ * however assumes that all list items have the same height. If you use a list in
+ * which items have different heights, the scrollbar will change appearance as the
+ * user scrolls through the list. To avoid this issue, you need to disable this
+ * property.
+ *
+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb
+ * is based solely on the number of items in the adapter and the position of the
+ * visible items inside the adapter. This provides a stable scrollbar as the user
+ * navigates through a list of items with varying heights.
+ *
+ * @param enabled Whether or not to enable smooth scrollbar.
+ *
+ * @see #setSmoothScrollbarEnabled(boolean)
+ * @attr ref android.R.styleable#AbsListView_smoothScrollbar
+ */
+ public void setSmoothScrollbarEnabled(boolean enabled) {
+ mSmoothScrollbarEnabled = enabled;
+ }
+
+ /**
+ * Returns the current state of the fast scroll feature.
+ *
+ * @return True if smooth scrollbar is enabled is enabled, false otherwise.
+ *
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isSmoothScrollbarEnabled() {
+ return mSmoothScrollbarEnabled;
+ }
+
+ /**
+ * Set the listener that will receive notifications every time the list scrolls.
+ *
+ * @param l the scroll listener
+ */
+ public void setOnScrollListener(OnScrollListener l) {
+ mOnScrollListener = l;
+ invokeOnItemScrollListener();
+ }
+
+ /**
+ * Notify our scroll listener (if there is one) of a change in scroll state
+ */
+ void invokeOnItemScrollListener() {
+ if (mFastScroll != null) {
+ mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount);
+ }
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
+ }
+ onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
+ }
+
+ /** @hide */
+ @Override
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+ // Since this class calls onScrollChanged even if the mFirstPosition and the
+ // child count have not changed we will avoid sending duplicate accessibility
+ // events.
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ final int firstVisiblePosition = getFirstVisiblePosition();
+ final int lastVisiblePosition = getLastVisiblePosition();
+ if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
+ && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
+ return;
+ } else {
+ mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
+ mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
+ }
+ }
+ super.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AbsListView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (isEnabled()) {
+ if (canScrollUp()) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+ info.setScrollable(true);
+ }
+ if (canScrollDown()) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+ info.setScrollable(true);
+ }
+ }
+
+ info.removeAction(AccessibilityAction.ACTION_CLICK);
+ info.setClickable(false);
+ }
+
+ int getSelectionModeForAccessibility() {
+ final int choiceMode = getChoiceMode();
+ switch (choiceMode) {
+ case CHOICE_MODE_NONE:
+ return CollectionInfo.SELECTION_MODE_NONE;
+ case CHOICE_MODE_SINGLE:
+ return CollectionInfo.SELECTION_MODE_SINGLE;
+ case CHOICE_MODE_MULTIPLE:
+ case CHOICE_MODE_MULTIPLE_MODAL:
+ return CollectionInfo.SELECTION_MODE_MULTIPLE;
+ default:
+ return CollectionInfo.SELECTION_MODE_NONE;
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case R.id.accessibilityActionScrollDown: {
+ if (isEnabled() && canScrollDown()) {
+ final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+ smoothScrollBy(viewportHeight, PositionScroller.SCROLL_DURATION);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ case R.id.accessibilityActionScrollUp: {
+ if (isEnabled() && canScrollUp()) {
+ final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+ smoothScrollBy(-viewportHeight, PositionScroller.SCROLL_DURATION);
+ return true;
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public View findViewByAccessibilityIdTraversal(int accessibilityId) {
+ if (accessibilityId == getAccessibilityViewId()) {
+ return this;
+ }
+ return super.findViewByAccessibilityIdTraversal(accessibilityId);
+ }
+
+ /**
+ * Indicates whether the children's drawing cache is used during a scroll.
+ * By default, the drawing cache is enabled but this will consume more memory.
+ *
+ * @return true if the scrolling cache is enabled, false otherwise
+ *
+ * @see #setScrollingCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isScrollingCacheEnabled() {
+ return mScrollingCacheEnabled;
+ }
+
+ /**
+ * Enables or disables the children's drawing cache during a scroll.
+ * By default, the drawing cache is enabled but this will use more memory.
+ *
+ * When the scrolling cache is enabled, the caches are kept after the
+ * first scrolling. You can manually clear the cache by calling
+ * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}.
+ *
+ * @param enabled true to enable the scroll cache, false otherwise
+ *
+ * @see #isScrollingCacheEnabled()
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ public void setScrollingCacheEnabled(boolean enabled) {
+ if (mScrollingCacheEnabled && !enabled) {
+ clearScrollingCache();
+ }
+ mScrollingCacheEnabled = enabled;
+ }
+
+ /**
+ * 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
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isTextFilterEnabled() {
+ return mTextFilterEnabled;
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ View view = getSelectedView();
+ if (view != null && view.getParent() == this) {
+ // the focused rectangle of the selected view offset into the
+ // coordinate space of this view.
+ view.getFocusedRect(r);
+ offsetDescendantRectToMyCoords(view, r);
+ } else {
+ // otherwise, just the norm
+ super.getFocusedRect(r);
+ }
+ }
+
+ private void useDefaultSelector() {
+ setSelector(getContext().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
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isStackFromBottom() {
+ return mStackFromBottom;
+ }
+
+ /**
+ * When stack from bottom is set to true, the list fills its content starting from
+ * the bottom of the view.
+ *
+ * @param stackFromBottom true to pin the view's content to the bottom edge,
+ * false to pin the view's content to the top edge
+ */
+ public void setStackFromBottom(boolean stackFromBottom) {
+ if (mStackFromBottom != stackFromBottom) {
+ mStackFromBottom = stackFromBottom;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ void requestLayoutIfNecessary() {
+ if (getChildCount() > 0) {
+ resetList();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ long selectedId;
+ long firstId;
+ int viewTop;
+ int position;
+ int height;
+ String filter;
+ boolean inActionMode;
+ int checkedItemCount;
+ SparseBooleanArray checkState;
+ LongSparseArray<Integer> checkIdState;
+
+ /**
+ * 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();
+ inActionMode = in.readByte() != 0;
+ checkedItemCount = in.readInt();
+ checkState = in.readSparseBooleanArray();
+ final int N = in.readInt();
+ if (N > 0) {
+ checkIdState = new LongSparseArray<Integer>();
+ for (int i=0; i<N; i++) {
+ final long key = in.readLong();
+ final int value = in.readInt();
+ checkIdState.put(key, value);
+ }
+ }
+ }
+
+ @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);
+ out.writeByte((byte) (inActionMode ? 1 : 0));
+ out.writeInt(checkedItemCount);
+ out.writeSparseBooleanArray(checkState);
+ final int N = checkIdState != null ? checkIdState.size() : 0;
+ out.writeInt(N);
+ for (int i=0; i<N; i++) {
+ out.writeLong(checkIdState.keyAt(i));
+ out.writeInt(checkIdState.valueAt(i));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AbsListView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " selectedId=" + selectedId
+ + " firstId=" + firstId
+ + " viewTop=" + viewTop
+ + " position=" + position
+ + " height=" + height
+ + " filter=" + filter
+ + " checkState=" + checkState + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ /*
+ * This doesn't really make sense as the place to dismiss the
+ * popups, but there don't seem to be any other useful hooks
+ * that happen early enough to keep from getting complaints
+ * about having leaked the window.
+ */
+ dismissPopup();
+
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ if (mPendingSync != null) {
+ // Just keep what we last restored.
+ ss.selectedId = mPendingSync.selectedId;
+ ss.firstId = mPendingSync.firstId;
+ ss.viewTop = mPendingSync.viewTop;
+ ss.position = mPendingSync.position;
+ ss.height = mPendingSync.height;
+ ss.filter = mPendingSync.filter;
+ ss.inActionMode = mPendingSync.inActionMode;
+ ss.checkedItemCount = mPendingSync.checkedItemCount;
+ ss.checkState = mPendingSync.checkState;
+ ss.checkIdState = mPendingSync.checkIdState;
+ return ss;
+ }
+
+ boolean haveChildren = getChildCount() > 0 && mItemCount > 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 && mFirstPosition > 0) {
+ // Remember the position of the first child.
+ // We only do this if we are not currently at the top of
+ // the list, for two reasons:
+ // (1) The list may be in the process of becoming empty, in
+ // which case mItemCount may not be 0, but if we try to
+ // ask for any information about position 0 we will crash.
+ // (2) Being "at the top" seems like a special case, anyway,
+ // and the user wouldn't expect to end up somewhere else when
+ // they revisit the list even if its content has changed.
+ View v = getChildAt(0);
+ ss.viewTop = v.getTop();
+ int firstPos = mFirstPosition;
+ if (firstPos >= mItemCount) {
+ firstPos = mItemCount - 1;
+ }
+ ss.position = firstPos;
+ ss.firstId = mAdapter.getItemId(firstPos);
+ } 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();
+ }
+ }
+ }
+
+ ss.inActionMode = mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null;
+
+ if (mCheckStates != null) {
+ ss.checkState = mCheckStates.clone();
+ }
+ if (mCheckedIdStates != null) {
+ final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
+ final int count = mCheckedIdStates.size();
+ for (int i = 0; i < count; i++) {
+ idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
+ }
+ ss.checkIdState = idState;
+ }
+ ss.checkedItemCount = mCheckedItemCount;
+
+ if (mRemoteAdapter != null) {
+ mRemoteAdapter.saveRemoteViewsCache();
+ }
+
+ 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;
+ mPendingSync = ss;
+ 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);
+ mSelectorPosition = INVALID_POSITION;
+ mNeedSync = true;
+ mPendingSync = ss;
+ mSyncRowId = ss.firstId;
+ mSyncPosition = ss.position;
+ mSpecificTop = ss.viewTop;
+ mSyncMode = SYNC_FIRST_POSITION;
+ }
+
+ setFilterText(ss.filter);
+
+ if (ss.checkState != null) {
+ mCheckStates = ss.checkState;
+ }
+
+ if (ss.checkIdState != null) {
+ mCheckedIdStates = ss.checkIdState;
+ }
+
+ mCheckedItemCount = ss.checkedItemCount;
+
+ if (ss.inActionMode && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL &&
+ mMultiChoiceModeCallback != null) {
+ mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
+ }
+
+ requestLayout();
+ }
+
+ private boolean acceptFilter() {
+ return mTextFilterEnabled && getAdapter() instanceof Filterable &&
+ ((Filterable) getAdapter()).getFilter() != null;
+ }
+
+ /**
+ * Sets the initial value for the text filter.
+ * @param filterText The text to use for the filter.
+ *
+ * @see #setTextFilterEnabled
+ */
+ public void setFilterText(String filterText) {
+ // TODO: Should we check for acceptFilter()?
+ if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) {
+ createTextFilter(false);
+ // This is going to call our listener onTextChanged, but we might not
+ // be ready to bring up a window yet
+ mTextFilter.setText(filterText);
+ mTextFilter.setSelection(filterText.length());
+ if (mAdapter instanceof Filterable) {
+ // if mPopup is non-null, then onTextChanged will do the filtering
+ if (mPopup == null) {
+ 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();
+ }
+ }
+ }
+
+ /**
+ * Returns the list's text filter, if available.
+ * @return the list's text filter or null if filtering isn't enabled
+ */
+ public CharSequence getTextFilter() {
+ if (mTextFilterEnabled && mTextFilter != null) {
+ return mTextFilter.getText();
+ }
+ return null;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
+ if (!isAttachedToWindow() && mAdapter != null) {
+ // Data may have changed while we were detached and it's valid
+ // to change focus while detached. Refresh so we don't die.
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ }
+ resurrectSelection();
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests && !mInLayout) {
+ super.requestLayout();
+ }
+ }
+
+ /**
+ * The list is empty. Clear everything out.
+ */
+ void resetList() {
+ removeAllViewsInLayout();
+ mFirstPosition = 0;
+ mDataChanged = false;
+ mPositionScrollAfterLayout = null;
+ mNeedSync = false;
+ mPendingSync = null;
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ mSelectedTop = 0;
+ mSelectorPosition = INVALID_POSITION;
+ mSelectorRect.setEmpty();
+ invalidate();
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ final int count = getChildCount();
+ if (count > 0) {
+ if (mSmoothScrollbarEnabled) {
+ int extent = count * 100;
+
+ View view = getChildAt(0);
+ final int top = view.getTop();
+ 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;
+ } else {
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ final int firstPosition = mFirstPosition;
+ final int childCount = getChildCount();
+ if (firstPosition >= 0 && childCount > 0) {
+ if (mSmoothScrollbarEnabled) {
+ final View view = getChildAt(0);
+ final int top = view.getTop();
+ int height = view.getHeight();
+ if (height > 0) {
+ return Math.max(firstPosition * 100 - (top * 100) / height +
+ (int)((float)mScrollY / getHeight() * mItemCount * 100), 0);
+ }
+ } else {
+ int index;
+ final int count = mItemCount;
+ if (firstPosition == 0) {
+ index = 0;
+ } else if (firstPosition + childCount == count) {
+ index = count;
+ } else {
+ index = firstPosition + childCount / 2;
+ }
+ return (int) (firstPosition + childCount * (index / (float) count));
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ int result;
+ if (mSmoothScrollbarEnabled) {
+ result = Math.max(mItemCount * 100, 0);
+ if (mScrollY != 0) {
+ // Compensate for overscroll
+ result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100));
+ }
+ } else {
+ result = mItemCount;
+ }
+ return result;
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ final int count = getChildCount();
+ final float fadeEdge = super.getTopFadingEdgeStrength();
+ if (count == 0) {
+ return fadeEdge;
+ } else {
+ if (mFirstPosition > 0) {
+ return 1.0f;
+ }
+
+ final int top = getChildAt(0).getTop();
+ final float fadeLength = getVerticalFadingEdgeLength();
+ return top < mPaddingTop ? -(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 = getVerticalFadingEdgeLength();
+ return bottom > height - mPaddingBottom ?
+ (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;
+
+ // Check if our previous measured size was at a point where we should scroll later.
+ if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
+ final int childCount = getChildCount();
+ final int listBottom = getHeight() - getPaddingBottom();
+ final View lastChild = getChildAt(childCount - 1);
+ final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
+ mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount &&
+ lastBottom <= listBottom;
+ }
+ }
+
+ /**
+ * Subclasses should NOT override this method but
+ * {@link #layoutChildren()} instead.
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ mInLayout = true;
+
+ final int childCount = getChildCount();
+ if (changed) {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).forceLayout();
+ }
+ mRecycler.markChildrenDirty();
+ }
+
+ layoutChildren();
+
+ mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
+
+ // TODO: Move somewhere sane. This doesn't belong in onLayout().
+ if (mFastScroll != null) {
+ mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
+ }
+ mInLayout = false;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected boolean setFrame(int left, int top, int right, int bottom) {
+ final boolean changed = super.setFrame(left, top, right, bottom);
+
+ if (changed) {
+ // Reposition the popup when the frame has changed. This includes
+ // translating the widget, not just changing its dimension. The
+ // filter popup needs to follow the widget.
+ final boolean visible = getWindowVisibility() == View.VISIBLE;
+ if (mFiltered && visible && mPopup != null && mPopup.isShowing()) {
+ positionPopup();
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Subclasses must override this method to layout their children.
+ */
+ protected void layoutChildren() {
+ }
+
+ /**
+ * @param focusedView view that holds accessibility focus
+ * @return direct child that contains accessibility focus, or null if no
+ * child contains accessibility focus
+ */
+ View getAccessibilityFocusedChild(View focusedView) {
+ ViewParent viewParent = focusedView.getParent();
+ while ((viewParent instanceof View) && (viewParent != this)) {
+ focusedView = (View) viewParent;
+ viewParent = viewParent.getParent();
+ }
+
+ if (!(viewParent instanceof View)) {
+ return null;
+ }
+
+ return focusedView;
+ }
+
+ void updateScrollIndicators() {
+ if (mScrollUp != null) {
+ mScrollUp.setVisibility(canScrollUp() ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ if (mScrollDown != null) {
+ mScrollDown.setVisibility(canScrollDown() ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ private boolean canScrollUp() {
+ 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;
+ }
+ }
+
+ return canScrollUp;
+ }
+
+ private boolean canScrollDown() {
+ 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;
+ }
+
+ return canScrollDown;
+ }
+
+ @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;
+ }
+
+ /**
+ * Gets a view and have it show the data associated with the specified
+ * position. This is called when we have already discovered that the view
+ * is not available for reuse in the recycle bin. The only choices left are
+ * converting an old view or making a new one.
+ *
+ * @param position the position to display
+ * @param outMetadata an array of at least 1 boolean where the first entry
+ * will be set {@code true} if the view is currently
+ * attached to the window, {@code false} otherwise (e.g.
+ * newly-inflated or remained scrap for multiple layout
+ * passes)
+ *
+ * @return A view displaying the data associated with the specified position
+ */
+ View obtainView(int position, boolean[] outMetadata) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
+
+ outMetadata[0] = false;
+
+ // Check whether we have a transient state view. Attempt to re-bind the
+ // data and discard the view if we fail.
+ final View transientView = mRecycler.getTransientStateView(position);
+ if (transientView != null) {
+ final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
+
+ // If the view type hasn't changed, attempt to re-bind the data.
+ if (params.viewType == mAdapter.getItemViewType(position)) {
+ final View updatedView = mAdapter.getView(position, transientView, this);
+
+ // If we failed to re-bind the data, scrap the obtained view.
+ if (updatedView != transientView) {
+ setItemViewLayoutParams(updatedView, position);
+ mRecycler.addScrapView(updatedView, position);
+ }
+ }
+
+ outMetadata[0] = true;
+
+ // Finish the temporary detach started in addScrapView().
+ transientView.dispatchFinishTemporaryDetach();
+ return transientView;
+ }
+
+ final View scrapView = mRecycler.getScrapView(position);
+ final View child = mAdapter.getView(position, scrapView, this);
+ if (scrapView != null) {
+ if (child != scrapView) {
+ // Failed to re-bind the data, return scrap to the heap.
+ mRecycler.addScrapView(scrapView, position);
+ } else if (child.isTemporarilyDetached()) {
+ outMetadata[0] = true;
+
+ // Finish the temporary detach started in addScrapView().
+ child.dispatchFinishTemporaryDetach();
+ }
+ }
+
+ if (mCacheColorHint != 0) {
+ child.setDrawingCacheBackgroundColor(mCacheColorHint);
+ }
+
+ if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ setItemViewLayoutParams(child, position);
+
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ if (mAccessibilityDelegate == null) {
+ mAccessibilityDelegate = new ListItemAccessibilityDelegate();
+ }
+ if (child.getAccessibilityDelegate() == null) {
+ child.setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+ }
+
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+
+ return child;
+ }
+
+ private void setItemViewLayoutParams(View child, int position) {
+ final ViewGroup.LayoutParams vlp = child.getLayoutParams();
+ LayoutParams lp;
+ if (vlp == null) {
+ lp = (LayoutParams) generateDefaultLayoutParams();
+ } else if (!checkLayoutParams(vlp)) {
+ lp = (LayoutParams) generateLayoutParams(vlp);
+ } else {
+ lp = (LayoutParams) vlp;
+ }
+
+ if (mAdapterHasStableIds) {
+ lp.itemId = mAdapter.getItemId(position);
+ }
+ lp.viewType = mAdapter.getItemViewType(position);
+ lp.isEnabled = mAdapter.isEnabled(position);
+ if (lp != vlp) {
+ child.setLayoutParams(lp);
+ }
+ }
+
+ class ListItemAccessibilityDelegate extends AccessibilityDelegate {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ final int position = getPositionForView(host);
+ onInitializeAccessibilityNodeInfoForItem(host, position, info);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+ if (super.performAccessibilityAction(host, action, arguments)) {
+ return true;
+ }
+
+ final int position = getPositionForView(host);
+ if (position == INVALID_POSITION || mAdapter == null) {
+ // Cannot perform actions on invalid items.
+ return false;
+ }
+
+ if (position >= mAdapter.getCount()) {
+ // The position is no longer valid, likely due to a data set
+ // change. We could fail here for all data set changes, since
+ // there is a chance that the data bound to the view may no
+ // longer exist at the same position within the adapter, but
+ // it's more consistent with the standard touch interaction to
+ // click at whatever may have moved into that position.
+ return false;
+ }
+
+ final boolean isItemEnabled;
+ final ViewGroup.LayoutParams lp = host.getLayoutParams();
+ if (lp instanceof AbsListView.LayoutParams) {
+ isItemEnabled = ((AbsListView.LayoutParams) lp).isEnabled;
+ } else {
+ isItemEnabled = false;
+ }
+
+ if (!isEnabled() || !isItemEnabled) {
+ // Cannot perform actions on disabled items.
+ return false;
+ }
+
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
+ if (getSelectedItemPosition() == position) {
+ setSelection(INVALID_POSITION);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SELECT: {
+ if (getSelectedItemPosition() != position) {
+ setSelection(position);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ if (isItemClickable(host)) {
+ final long id = getItemIdAtPosition(position);
+ return performItemClick(host, position, id);
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
+ if (isLongClickable()) {
+ final long id = getItemIdAtPosition(position);
+ return performLongPress(host, position, id);
+ }
+ } return false;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Initializes an {@link AccessibilityNodeInfo} with information about a
+ * particular item in the list.
+ *
+ * @param view View representing the list item.
+ * @param position Position of the list item within the adapter.
+ * @param info Node info to populate.
+ */
+ public void onInitializeAccessibilityNodeInfoForItem(
+ View view, int position, AccessibilityNodeInfo info) {
+ if (position == INVALID_POSITION) {
+ // The item doesn't exist, so there's not much we can do here.
+ return;
+ }
+
+ final boolean isItemEnabled;
+ final ViewGroup.LayoutParams lp = view.getLayoutParams();
+ if (lp instanceof AbsListView.LayoutParams) {
+ isItemEnabled = ((AbsListView.LayoutParams) lp).isEnabled;
+ } else {
+ isItemEnabled = false;
+ }
+
+ if (!isEnabled() || !isItemEnabled) {
+ info.setEnabled(false);
+ return;
+ }
+
+ if (position == getSelectedItemPosition()) {
+ info.setSelected(true);
+ info.addAction(AccessibilityAction.ACTION_CLEAR_SELECTION);
+ } else {
+ info.addAction(AccessibilityAction.ACTION_SELECT);
+ }
+
+ if (isItemClickable(view)) {
+ info.addAction(AccessibilityAction.ACTION_CLICK);
+ info.setClickable(true);
+ }
+
+ if (isLongClickable()) {
+ info.addAction(AccessibilityAction.ACTION_LONG_CLICK);
+ info.setLongClickable(true);
+ }
+ }
+
+ private boolean isItemClickable(View view) {
+ return !view.hasExplicitFocusable();
+ }
+
+ /**
+ * Positions the selector in a way that mimics touch.
+ */
+ void positionSelectorLikeTouch(int position, View sel, float x, float y) {
+ positionSelector(position, sel, true, x, y);
+ }
+
+ /**
+ * Positions the selector in a way that mimics keyboard focus.
+ */
+ void positionSelectorLikeFocus(int position, View sel) {
+ if (mSelector != null && mSelectorPosition != position && position != INVALID_POSITION) {
+ final Rect bounds = mSelectorRect;
+ final float x = bounds.exactCenterX();
+ final float y = bounds.exactCenterY();
+ positionSelector(position, sel, true, x, y);
+ } else {
+ positionSelector(position, sel);
+ }
+ }
+
+ void positionSelector(int position, View sel) {
+ positionSelector(position, sel, false, -1, -1);
+ }
+
+ private void positionSelector(int position, View sel, boolean manageHotspot, float x, float y) {
+ final boolean positionChanged = position != mSelectorPosition;
+ if (position != INVALID_POSITION) {
+ mSelectorPosition = position;
+ }
+
+ final Rect selectorRect = mSelectorRect;
+ selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
+ if (sel instanceof SelectionBoundsAdjuster) {
+ ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect);
+ }
+
+ // Adjust for selection padding.
+ selectorRect.left -= mSelectionLeftPadding;
+ selectorRect.top -= mSelectionTopPadding;
+ selectorRect.right += mSelectionRightPadding;
+ selectorRect.bottom += mSelectionBottomPadding;
+
+ // Update the child enabled state prior to updating the selector.
+ final boolean isChildViewEnabled = sel.isEnabled();
+ if (mIsChildViewEnabled != isChildViewEnabled) {
+ mIsChildViewEnabled = isChildViewEnabled;
+ }
+
+ // Update the selector drawable's state and position.
+ final Drawable selector = mSelector;
+ if (selector != null) {
+ if (positionChanged) {
+ // Wipe out the current selector state so that we can start
+ // over in the new position with a fresh state.
+ selector.setVisible(false, false);
+ selector.setState(StateSet.NOTHING);
+ }
+ selector.setBounds(selectorRect);
+ if (positionChanged) {
+ if (getVisibility() == VISIBLE) {
+ selector.setVisible(true, false);
+ }
+ updateSelectorState();
+ }
+ if (manageHotspot) {
+ selector.setHotspot(x, y);
+ }
+ }
+ }
+
+ @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 boolean isPaddingOffsetRequired() {
+ return (mGroupFlags & CLIP_TO_PADDING_MASK) != CLIP_TO_PADDING_MASK;
+ }
+
+ @Override
+ protected int getLeftPaddingOffset() {
+ return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingLeft;
+ }
+
+ @Override
+ protected int getTopPaddingOffset() {
+ return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingTop;
+ }
+
+ @Override
+ protected int getRightPaddingOffset() {
+ return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingRight;
+ }
+
+ @Override
+ protected int getBottomPaddingOffset() {
+ return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingBottom;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void internalSetPadding(int left, int top, int right, int bottom) {
+ super.internalSetPadding(left, top, right, bottom);
+ if (isLayoutRequested()) {
+ handleBoundsChange();
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ handleBoundsChange();
+ if (mFastScroll != null) {
+ mFastScroll.onSizeChanged(w, h, oldw, oldh);
+ }
+ }
+
+ /**
+ * Called when bounds of the AbsListView are changed. AbsListView marks data set as changed
+ * and force layouts all children that don't have exact measure specs.
+ * <p>
+ * This invalidation is necessary, otherwise, AbsListView may think the children are valid and
+ * fail to relayout them properly to accommodate for new bounds.
+ */
+ void handleBoundsChange() {
+ if (mInLayout) {
+ return;
+ }
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ mDataChanged = true;
+ rememberSyncState();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+ // force layout child unless it has exact specs
+ if (lp == null || lp.width < 1 || lp.height < 1) {
+ child.forceLayout();
+ }
+ }
+ }
+ }
+
+ /**
+ * @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 (isFocused() && !isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed());
+ }
+
+ private void drawSelector(Canvas canvas) {
+ if (!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(@DrawableRes int resID) {
+ setSelector(getContext().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);
+ updateSelectorState();
+ }
+
+ /**
+ * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
+ * selection in the list.
+ *
+ * @return the drawable used to display the selector
+ */
+ public Drawable getSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
+ * this is a long press.
+ */
+ void keyPressed() {
+ if (!isEnabled() || !isClickable()) {
+ return;
+ }
+
+ Drawable selector = mSelector;
+ Rect selectorRect = mSelectorRect;
+ if (selector != null && (isFocused() || touchModeDrawsInPressedState())
+ && !selectorRect.isEmpty()) {
+
+ final View v = getChildAt(mSelectedPosition - mFirstPosition);
+
+ if (v != null) {
+ if (v.hasExplicitFocusable()) return;
+ v.setPressed(true);
+ }
+ setPressed(true);
+
+ final boolean longClickable = isLongClickable();
+ Drawable d = selector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (longClickable) {
+ ((TransitionDrawable) d).startTransition(
+ ViewConfiguration.getLongPressTimeout());
+ } else {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ }
+ if (longClickable && !mDataChanged) {
+ if (mPendingCheckForKeyLongPress == null) {
+ mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
+ }
+ mPendingCheckForKeyLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
+ }
+ }
+ }
+
+ public void setScrollIndicators(View up, View down) {
+ mScrollUp = up;
+ mScrollDown = down;
+ }
+
+ void updateSelectorState() {
+ final Drawable selector = mSelector;
+ if (selector != null && selector.isStateful()) {
+ if (shouldShowSelector()) {
+ if (selector.setState(getDrawableStateForSelector())) {
+ invalidateDrawable(selector);
+ }
+ } else {
+ selector.setState(StateSet.NOTHING);
+ }
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ updateSelectorState();
+ }
+
+ private int[] getDrawableStateForSelector() {
+ // If the child view is enabled then do the default behavior.
+ if (mIsChildViewEnabled) {
+ // Common case
+ return super.getDrawableState();
+ }
+
+ // 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 is safely mutable.
+ final int[] state = onCreateDrawableState(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(@NonNull Drawable dr) {
+ return mSelector == dr || super.verifyDrawable(dr);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mSelector != null) mSelector.jumpToCurrentState();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ treeObserver.addOnTouchModeChangeListener(this);
+ if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
+ treeObserver.addOnGlobalLayoutListener(this);
+ }
+
+ if (mAdapter != null && mDataSetObserver == null) {
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ // Data may have changed while we were detached. Refresh.
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mIsDetaching = true;
+
+ // Dismiss the popup in case onSaveInstanceState() was not invoked
+ dismissPopup();
+
+ // Detach any view left in the scrap heap
+ mRecycler.clear();
+
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ treeObserver.removeOnTouchModeChangeListener(this);
+ if (mTextFilterEnabled && mPopup != null) {
+ treeObserver.removeOnGlobalLayoutListener(this);
+ mGlobalLayoutListenerAddedFilter = false;
+ }
+
+ if (mAdapter != null && mDataSetObserver != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ mDataSetObserver = null;
+ }
+
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+
+ if (mFlingRunnable != null) {
+ removeCallbacks(mFlingRunnable);
+ }
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ if (mClearScrollingCache != null) {
+ removeCallbacks(mClearScrollingCache);
+ }
+
+ if (mPerformClick != null) {
+ removeCallbacks(mPerformClick);
+ }
+
+ if (mTouchModeReset != null) {
+ removeCallbacks(mTouchModeReset);
+ mTouchModeReset.run();
+ }
+
+ mIsDetaching = false;
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
+
+ if (!hasWindowFocus) {
+ setChildrenDrawingCacheEnabled(false);
+ if (mFlingRunnable != null) {
+ removeCallbacks(mFlingRunnable);
+ // let the fling runnable report its new state which
+ // should be idle
+ mFlingRunnable.mSuppressIdleStateChangeCall = false;
+ mFlingRunnable.endFling();
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ if (mScrollY != 0) {
+ mScrollY = 0;
+ invalidateParentCaches();
+ finishGlows();
+ invalidate();
+ }
+ }
+ // Always hide the type filter
+ dismissPopup();
+
+ if (touchMode == TOUCH_MODE_OFF) {
+ // Remember the last selected element
+ mResurrectToPosition = mSelectedPosition;
+ }
+ } else {
+ if (mFiltered && !mPopupHidden) {
+ // 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;
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ if (mFastScroll != null) {
+ mFastScroll.setScrollbarPosition(getVerticalScrollbarPosition());
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ @Override
+ public void onCancelPendingInputEvents() {
+ super.onCancelPendingInputEvents();
+ if (mPerformClick != null) {
+ removeCallbacks(mPerformClick);
+ }
+ if (mPendingCheckForTap != null) {
+ removeCallbacks(mPendingCheckForTap);
+ }
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+ if (mPendingCheckForKeyLongPress != null) {
+ removeCallbacks(mPendingCheckForKeyLongPress);
+ }
+ }
+
+ /**
+ * 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 getWindowAttachCount() == mOriginalAttachCount;
+ }
+ }
+
+ private class PerformClick extends WindowRunnnable implements Runnable {
+ int mClickMotionPosition;
+
+ @Override
+ public void run() {
+ // The data has changed since we posted this action in the event queue,
+ // bail out before bad things happen
+ if (mDataChanged) return;
+
+ final ListAdapter adapter = mAdapter;
+ final int motionPosition = mClickMotionPosition;
+ if (adapter != null && mItemCount > 0 &&
+ motionPosition != INVALID_POSITION &&
+ motionPosition < adapter.getCount() && sameWindow() &&
+ adapter.isEnabled(motionPosition)) {
+ final View view = getChildAt(motionPosition - mFirstPosition);
+ // If there is no view, something bad happened (the view scrolled off the
+ // screen, etc.) and we should cancel the click
+ if (view != null) {
+ performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
+ }
+ }
+ }
+ }
+
+ private class CheckForLongPress extends WindowRunnnable implements Runnable {
+ private static final int INVALID_COORD = -1;
+ private float mX = INVALID_COORD;
+ private float mY = INVALID_COORD;
+
+ private void setCoords(float x, float y) {
+ mX = x;
+ mY = y;
+ }
+
+ @Override
+ 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) {
+ if (mX != INVALID_COORD && mY != INVALID_COORD) {
+ handled = performLongPress(child, longPressPosition, longPressId, mX, mY);
+ } else {
+ handled = performLongPress(child, longPressPosition, longPressId);
+ }
+ }
+
+ if (handled) {
+ mHasPerformedLongPress = true;
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ child.setPressed(false);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ }
+ }
+ }
+
+ private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
+ @Override
+ 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 performStylusButtonPressAction(MotionEvent ev) {
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
+ final View child = getChildAt(mMotionPosition - mFirstPosition);
+ if (child != null) {
+ final int longPressPosition = mMotionPosition;
+ final long longPressId = mAdapter.getItemId(mMotionPosition);
+ if (performLongPress(child, longPressPosition, longPressId)) {
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ child.setPressed(false);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean performLongPress(final View child,
+ final int longPressPosition, final long longPressId) {
+ return performLongPress(
+ child,
+ longPressPosition,
+ longPressId,
+ CheckForLongPress.INVALID_COORD,
+ CheckForLongPress.INVALID_COORD);
+ }
+
+ boolean performLongPress(final View child,
+ final int longPressPosition, final long longPressId, float x, float y) {
+ // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+ if (mChoiceActionMode == null &&
+ (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) {
+ setItemChecked(longPressPosition, true);
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ return true;
+ }
+
+ boolean handled = false;
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
+ longPressPosition, longPressId);
+ }
+ if (!handled) {
+ mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+ if (x != CheckForLongPress.INVALID_COORD && y != CheckForLongPress.INVALID_COORD) {
+ handled = super.showContextMenuForChild(AbsListView.this, x, y);
+ } else {
+ handled = super.showContextMenuForChild(AbsListView.this);
+ }
+ }
+ if (handled) {
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ return handled;
+ }
+
+ @Override
+ protected ContextMenuInfo getContextMenuInfo() {
+ return mContextMenuInfo;
+ }
+
+ @Override
+ public boolean showContextMenu() {
+ return showContextMenuInternal(0, 0, false);
+ }
+
+ @Override
+ public boolean showContextMenu(float x, float y) {
+ return showContextMenuInternal(x, y, true);
+ }
+
+ private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
+ final int position = pointToPosition((int)x, (int)y);
+ if (position != INVALID_POSITION) {
+ final long id = mAdapter.getItemId(position);
+ View child = getChildAt(position - mFirstPosition);
+ if (child != null) {
+ mContextMenuInfo = createContextMenuInfo(child, position, id);
+ if (useOffsets) {
+ return super.showContextMenuForChild(this, x, y);
+ } else {
+ return super.showContextMenuForChild(this);
+ }
+ }
+ }
+ if (useOffsets) {
+ return super.showContextMenu(x, y);
+ } else {
+ return super.showContextMenu();
+ }
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ if (isShowingContextMenuWithCoords()) {
+ return false;
+ }
+ return showContextMenuForChildInternal(originalView, 0, 0, false);
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView, float x, float y) {
+ return showContextMenuForChildInternal(originalView,x, y, true);
+ }
+
+ private boolean showContextMenuForChildInternal(View originalView, float x, float y,
+ boolean useOffsets) {
+ final int longPressPosition = getPositionForView(originalView);
+ if (longPressPosition < 0) {
+ return false;
+ }
+
+ final long longPressId = mAdapter.getItemId(longPressPosition);
+ boolean handled = false;
+
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(this, originalView,
+ longPressPosition, longPressId);
+ }
+
+ if (!handled) {
+ final View child = getChildAt(longPressPosition - mFirstPosition);
+ mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+
+ if (useOffsets) {
+ handled = super.showContextMenuForChild(originalView, x, y);
+ } else {
+ handled = super.showContextMenuForChild(originalView);
+ }
+ }
+
+ return handled;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (KeyEvent.isConfirmKey(keyCode)) {
+ if (!isEnabled()) {
+ return true;
+ }
+ if (isClickable() && isPressed() &&
+ mSelectedPosition >= 0 && mAdapter != null &&
+ mSelectedPosition < mAdapter.getCount()) {
+
+ final View view = getChildAt(mSelectedPosition - mFirstPosition);
+ if (view != null) {
+ performItemClick(view, mSelectedPosition, mSelectedRowId);
+ view.setPressed(false);
+ }
+ setPressed(false);
+ return true;
+ }
+ }
+ 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.
+ }
+
+ @Override
+ public void dispatchDrawableHotspotChanged(float x, float y) {
+ // Don't dispatch hotspot changes to children. We'll manually handle
+ // calling drawableHotspotChanged on the correct 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;
+ }
+
+ private final class CheckForTap implements Runnable {
+ float x;
+ float y;
+
+ @Override
+ public void run() {
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ mTouchMode = TOUCH_MODE_TAP;
+ final View child = getChildAt(mMotionPosition - mFirstPosition);
+ if (child != null && !child.hasExplicitFocusable()) {
+ mLayoutMode = LAYOUT_NORMAL;
+
+ if (!mDataChanged) {
+ final float[] point = mTmpPoint;
+ point[0] = x;
+ point[1] = y;
+ transformPointToViewLocal(point, child);
+ child.drawableHotspotChanged(point[0], point[1]);
+ child.setPressed(true);
+ setPressed(true);
+ layoutChildren();
+ positionSelector(mMotionPosition, child);
+ refreshDrawableState();
+
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ final boolean longClickable = isLongClickable();
+
+ if (mSelector != null) {
+ final Drawable d = mSelector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (longClickable) {
+ ((TransitionDrawable) d).startTransition(longPressTimeout);
+ } else {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ }
+ mSelector.setHotspot(x, y);
+ }
+
+ if (longClickable) {
+ if (mPendingCheckForLongPress == null) {
+ mPendingCheckForLongPress = new CheckForLongPress();
+ }
+ mPendingCheckForLongPress.setCoords(x, y);
+ mPendingCheckForLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForLongPress, longPressTimeout);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ }
+ }
+ }
+ }
+
+ private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
+ // Check if we have moved far enough that it looks more like a
+ // scroll than a tap
+ final int deltaY = y - mMotionY;
+ final int distance = Math.abs(deltaY);
+ final boolean overscroll = mScrollY != 0;
+ if ((overscroll || distance > mTouchSlop) &&
+ (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+ createScrollingCache();
+ if (overscroll) {
+ mTouchMode = TOUCH_MODE_OVERSCROLL;
+ mMotionCorrection = 0;
+ } else {
+ mTouchMode = TOUCH_MODE_SCROLL;
+ mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
+ }
+ removeCallbacks(mPendingCheckForLongPress);
+ setPressed(false);
+ final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ // Time to start stealing events! Once we've stolen them, don't let anyone
+ // steal from us
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ scrollIfNeeded(x, y, vtev);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
+ int rawDeltaY = y - mMotionY;
+ int scrollOffsetCorrection = 0;
+ int scrollConsumedCorrection = 0;
+ if (mLastY == Integer.MIN_VALUE) {
+ rawDeltaY -= mMotionCorrection;
+ }
+ if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY,
+ mScrollConsumed, mScrollOffset)) {
+ rawDeltaY += mScrollConsumed[1];
+ scrollOffsetCorrection = -mScrollOffset[1];
+ scrollConsumedCorrection = mScrollConsumed[1];
+ if (vtev != null) {
+ vtev.offsetLocation(0, mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ }
+ }
+ final int deltaY = rawDeltaY;
+ int incrementalDeltaY =
+ mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
+ int lastYCorrection = 0;
+
+ if (mTouchMode == TOUCH_MODE_SCROLL) {
+ if (PROFILE_SCROLLING) {
+ if (!mScrollProfilingStarted) {
+ Debug.startMethodTracing("AbsListViewScroll");
+ mScrollProfilingStarted = true;
+ }
+ }
+
+ if (mScrollStrictSpan == null) {
+ // If it's non-null, we're already in a scroll.
+ mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
+ }
+
+ if (y != mLastY) {
+ // We may be here after stopping a fling and continuing to scroll.
+ // If so, we haven't disallowed intercepting touch events yet.
+ // Make sure that we do so in case we're in a parent that can intercept.
+ if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
+ Math.abs(rawDeltaY) > mTouchSlop) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ final int motionIndex;
+ if (mMotionPosition >= 0) {
+ motionIndex = mMotionPosition - mFirstPosition;
+ } else {
+ // If we don't have a motion position that we can reliably track,
+ // pick something in the middle to make a best guess at things below.
+ motionIndex = getChildCount() / 2;
+ }
+
+ int motionViewPrevTop = 0;
+ View motionView = this.getChildAt(motionIndex);
+ if (motionView != null) {
+ motionViewPrevTop = motionView.getTop();
+ }
+
+ // No need to do all this work if we're not going to move anyway
+ boolean atEdge = false;
+ if (incrementalDeltaY != 0) {
+ atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
+ }
+
+ // Check to see if we have bumped into the scroll limit
+ motionView = this.getChildAt(motionIndex);
+ if (motionView != null) {
+ // Check if the top of the motion view is where it is
+ // supposed to be
+ final int motionViewRealTop = motionView.getTop();
+ if (atEdge) {
+ // Apply overscroll
+
+ int overscroll = -incrementalDeltaY -
+ (motionViewRealTop - motionViewPrevTop);
+ if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll,
+ mScrollOffset)) {
+ lastYCorrection -= mScrollOffset[1];
+ if (vtev != null) {
+ vtev.offsetLocation(0, mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ }
+ } else {
+ final boolean atOverscrollEdge = overScrollBy(0, overscroll,
+ 0, mScrollY, 0, 0, 0, mOverscrollDistance, true);
+
+ if (atOverscrollEdge && mVelocityTracker != null) {
+ // Don't allow overfling if we're at the edge
+ mVelocityTracker.clear();
+ }
+
+ final int overscrollMode = getOverScrollMode();
+ if (overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ !contentFits())) {
+ if (!atOverscrollEdge) {
+ mDirection = 0; // Reset when entering overscroll.
+ mTouchMode = TOUCH_MODE_OVERSCROLL;
+ }
+ if (incrementalDeltaY > 0) {
+ mEdgeGlowTop.onPull((float) -overscroll / getHeight(),
+ (float) x / getWidth());
+ if (!mEdgeGlowBottom.isFinished()) {
+ mEdgeGlowBottom.onRelease();
+ }
+ invalidateTopGlow();
+ } else if (incrementalDeltaY < 0) {
+ mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
+ 1.f - (float) x / getWidth());
+ if (!mEdgeGlowTop.isFinished()) {
+ mEdgeGlowTop.onRelease();
+ }
+ invalidateBottomGlow();
+ }
+ }
+ }
+ }
+ mMotionY = y + lastYCorrection + scrollOffsetCorrection;
+ }
+ mLastY = y + lastYCorrection + scrollOffsetCorrection;
+ }
+ } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
+ if (y != mLastY) {
+ final int oldScroll = mScrollY;
+ final int newScroll = oldScroll - incrementalDeltaY;
+ int newDirection = y > mLastY ? 1 : -1;
+
+ if (mDirection == 0) {
+ mDirection = newDirection;
+ }
+
+ int overScrollDistance = -incrementalDeltaY;
+ if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) {
+ overScrollDistance = -oldScroll;
+ incrementalDeltaY += overScrollDistance;
+ } else {
+ incrementalDeltaY = 0;
+ }
+
+ if (overScrollDistance != 0) {
+ overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0,
+ 0, mOverscrollDistance, true);
+ final int overscrollMode = getOverScrollMode();
+ if (overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ !contentFits())) {
+ if (rawDeltaY > 0) {
+ mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(),
+ (float) x / getWidth());
+ if (!mEdgeGlowBottom.isFinished()) {
+ mEdgeGlowBottom.onRelease();
+ }
+ invalidateTopGlow();
+ } else if (rawDeltaY < 0) {
+ mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),
+ 1.f - (float) x / getWidth());
+ if (!mEdgeGlowTop.isFinished()) {
+ mEdgeGlowTop.onRelease();
+ }
+ invalidateBottomGlow();
+ }
+ }
+ }
+
+ if (incrementalDeltaY != 0) {
+ // Coming back to 'real' list scrolling
+ if (mScrollY != 0) {
+ mScrollY = 0;
+ invalidateParentIfNeeded();
+ }
+
+ trackMotionScroll(incrementalDeltaY, incrementalDeltaY);
+
+ mTouchMode = TOUCH_MODE_SCROLL;
+
+ // We did not scroll the full amount. Treat this essentially like the
+ // start of a new touch scroll
+ final int motionPosition = findClosestMotionRow(y);
+
+ mMotionCorrection = 0;
+ View motionView = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0;
+ mMotionY = y + scrollOffsetCorrection;
+ mMotionPosition = motionPosition;
+ }
+ mLastY = y + lastYCorrection + scrollOffsetCorrection;
+ mDirection = newDirection;
+ }
+ }
+ }
+
+ private void invalidateTopGlow() {
+ if (mEdgeGlowTop == null) {
+ return;
+ }
+ final boolean clipToPadding = getClipToPadding();
+ final int top = clipToPadding ? mPaddingTop : 0;
+ final int left = clipToPadding ? mPaddingLeft : 0;
+ final int right = clipToPadding ? getWidth() - mPaddingRight : getWidth();
+ invalidate(left, top, right, top + mEdgeGlowTop.getMaxHeight());
+ }
+
+ private void invalidateBottomGlow() {
+ if (mEdgeGlowBottom == null) {
+ return;
+ }
+ final boolean clipToPadding = getClipToPadding();
+ final int bottom = clipToPadding ? getHeight() - mPaddingBottom : getHeight();
+ final int left = clipToPadding ? mPaddingLeft : 0;
+ final int right = clipToPadding ? getWidth() - mPaddingRight : getWidth();
+ invalidate(left, bottom - mEdgeGlowBottom.getMaxHeight(), right, bottom);
+ }
+
+ @Override
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (isInTouchMode) {
+ // Get rid of the selection when we enter touch mode
+ hideSelector();
+ // Layout, but only if we already have done so previously.
+ // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
+ // state.)
+ if (getHeight() > 0 && getChildCount() > 0) {
+ // We do not lose focus initiating a touch (since AbsListView is focusable in
+ // touch mode). Force an initial layout to get rid of the selection.
+ layoutChildren();
+ }
+ updateSelectorState();
+ } else {
+ int touchMode = mTouchMode;
+ if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) {
+ if (mFlingRunnable != null) {
+ mFlingRunnable.endFling();
+ }
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ if (mScrollY != 0) {
+ mScrollY = 0;
+ invalidateParentCaches();
+ finishGlows();
+ invalidate();
+ }
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected boolean handleScrollBarDragging(MotionEvent event) {
+ // Doesn't support normal scroll bar dragging. Use FastScroller.
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (!isEnabled()) {
+ // A disabled view that is clickable still consumes the touch
+ // events, it just doesn't respond to them.
+ return isClickable() || isLongClickable();
+ }
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ if (mIsDetaching || !isAttachedToWindow()) {
+ // Something isn't right.
+ // Since we rely on being attached to get data set change notifications,
+ // don't risk doing anything where we might try to resync and find things
+ // in a bogus state.
+ return false;
+ }
+
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+
+ if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
+ return true;
+ }
+
+ initVelocityTrackerIfNotExists();
+ final MotionEvent vtev = MotionEvent.obtain(ev);
+
+ final int actionMasked = ev.getActionMasked();
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ mNestedYOffset = 0;
+ }
+ vtev.offsetLocation(0, mNestedYOffset);
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN: {
+ onTouchDown(ev);
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ onTouchMove(ev, vtev);
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ onTouchUp(ev);
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ onTouchCancel();
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ final int x = mMotionX;
+ final int y = mMotionY;
+ final int motionPosition = pointToPosition(x, y);
+ if (motionPosition >= 0) {
+ // Remember where the motion event started
+ final View child = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = child.getTop();
+ mMotionPosition = motionPosition;
+ }
+ mLastY = y;
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ // New pointers take over dragging duties
+ final int index = ev.getActionIndex();
+ final int id = ev.getPointerId(index);
+ final int x = (int) ev.getX(index);
+ final int y = (int) ev.getY(index);
+ mMotionCorrection = 0;
+ mActivePointerId = id;
+ mMotionX = x;
+ mMotionY = y;
+ final int motionPosition = pointToPosition(x, y);
+ if (motionPosition >= 0) {
+ // Remember where the motion event started
+ final View child = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = child.getTop();
+ mMotionPosition = motionPosition;
+ }
+ mLastY = y;
+ break;
+ }
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(vtev);
+ }
+ vtev.recycle();
+ return true;
+ }
+
+ private void onTouchDown(MotionEvent ev) {
+ mHasPerformedLongPress = false;
+ mActivePointerId = ev.getPointerId(0);
+
+ if (mTouchMode == TOUCH_MODE_OVERFLING) {
+ // Stopped the fling. It is a scroll.
+ mFlingRunnable.endFling();
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ mTouchMode = TOUCH_MODE_OVERSCROLL;
+ mMotionX = (int) ev.getX();
+ mMotionY = (int) ev.getY();
+ mLastY = mMotionY;
+ mMotionCorrection = 0;
+ mDirection = 0;
+ } else {
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+ int motionPosition = pointToPosition(x, y);
+
+ if (!mDataChanged) {
+ if (mTouchMode == TOUCH_MODE_FLING) {
+ // Stopped a fling. It is a scroll.
+ createScrollingCache();
+ mTouchMode = TOUCH_MODE_SCROLL;
+ mMotionCorrection = 0;
+ motionPosition = findMotionRow(y);
+ mFlingRunnable.flywheelTouch();
+ } else if ((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();
+ }
+
+ mPendingCheckForTap.x = ev.getX();
+ mPendingCheckForTap.y = ev.getY();
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+ }
+ }
+
+ if (motionPosition >= 0) {
+ // Remember where the motion event started
+ final View v = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = v.getTop();
+ }
+
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ mLastY = Integer.MIN_VALUE;
+ }
+
+ if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION
+ && performButtonActionOnTouchDown(ev)) {
+ removeCallbacks(mPendingCheckForTap);
+ }
+ }
+
+ private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
+ if (mHasPerformedLongPress) {
+ // Consume all move events following a successful long press.
+ return;
+ }
+
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ pointerIndex = 0;
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ }
+
+ if (mDataChanged) {
+ // Re-sync everything if data has been changed
+ // since the scroll operation can query the adapter.
+ layoutChildren();
+ }
+
+ final int y = (int) ev.getY(pointerIndex);
+
+ 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. If so, we'll enter scrolling mode.
+ if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
+ break;
+ }
+ // Otherwise, check containment within list bounds. If we're
+ // outside bounds, cancel any active presses.
+ final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ final float x = ev.getX(pointerIndex);
+ if (!pointInView(x, y, mTouchSlop)) {
+ setPressed(false);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+ mPendingCheckForTap : mPendingCheckForLongPress);
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ updateSelectorState();
+ } else if (motionView != null) {
+ // Still within bounds, update the hotspot.
+ final float[] point = mTmpPoint;
+ point[0] = x;
+ point[1] = y;
+ transformPointToViewLocal(point, motionView);
+ motionView.drawableHotspotChanged(point[0], point[1]);
+ }
+ break;
+ case TOUCH_MODE_SCROLL:
+ case TOUCH_MODE_OVERSCROLL:
+ scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
+ break;
+ }
+ }
+
+ private void onTouchUp(MotionEvent ev) {
+ 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) {
+ if (mTouchMode != TOUCH_MODE_DOWN) {
+ child.setPressed(false);
+ }
+
+ final float x = ev.getX();
+ final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
+ if (inList && !child.hasExplicitFocusable()) {
+ if (mPerformClick == null) {
+ mPerformClick = new PerformClick();
+ }
+
+ final AbsListView.PerformClick performClick = mPerformClick;
+ performClick.mClickMotionPosition = motionPosition;
+ performClick.rememberWindowAttachCount();
+
+ mResurrectToPosition = motionPosition;
+
+ if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
+ removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+ mPendingCheckForTap : mPendingCheckForLongPress);
+ mLayoutMode = LAYOUT_NORMAL;
+ if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
+ mTouchMode = TOUCH_MODE_TAP;
+ setSelectedPositionInt(mMotionPosition);
+ layoutChildren();
+ child.setPressed(true);
+ positionSelector(mMotionPosition, child);
+ setPressed(true);
+ if (mSelector != null) {
+ Drawable d = mSelector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ mSelector.setHotspot(x, ev.getY());
+ }
+ if (mTouchModeReset != null) {
+ removeCallbacks(mTouchModeReset);
+ }
+ mTouchModeReset = new Runnable() {
+ @Override
+ public void run() {
+ mTouchModeReset = null;
+ mTouchMode = TOUCH_MODE_REST;
+ child.setPressed(false);
+ setPressed(false);
+ if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
+ performClick.run();
+ }
+ }
+ };
+ postDelayed(mTouchModeReset,
+ ViewConfiguration.getPressedStateDuration());
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ updateSelectorState();
+ }
+ return;
+ } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
+ performClick.run();
+ }
+ }
+ }
+ mTouchMode = TOUCH_MODE_REST;
+ updateSelectorState();
+ break;
+ case TOUCH_MODE_SCROLL:
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ final int firstChildTop = getChildAt(0).getTop();
+ final int lastChildBottom = getChildAt(childCount - 1).getBottom();
+ final int contentTop = mListPadding.top;
+ final int contentBottom = getHeight() - mListPadding.bottom;
+ if (mFirstPosition == 0 && firstChildTop >= contentTop &&
+ mFirstPosition + childCount < mItemCount &&
+ lastChildBottom <= getHeight() - contentBottom) {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ } else {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+
+ final int initialVelocity = (int)
+ (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
+ // Fling if we have enough velocity and we aren't at a boundary.
+ // Since we can potentially overfling more than we can overscroll, don't
+ // allow the weird behavior where you can scroll to a boundary then
+ // fling further.
+ boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
+ if (flingVelocity &&
+ !((mFirstPosition == 0 &&
+ firstChildTop == contentTop - mOverscrollDistance) ||
+ (mFirstPosition + childCount == mItemCount &&
+ lastChildBottom == contentBottom + mOverscrollDistance))) {
+ if (!dispatchNestedPreFling(0, -initialVelocity)) {
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ mFlingRunnable.start(-initialVelocity);
+ dispatchNestedFling(0, -initialVelocity, true);
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ if (mFlingRunnable != null) {
+ mFlingRunnable.endFling();
+ }
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) {
+ dispatchNestedFling(0, -initialVelocity, false);
+ }
+ }
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ break;
+
+ case TOUCH_MODE_OVERSCROLL:
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ if (Math.abs(initialVelocity) > mMinimumVelocity) {
+ mFlingRunnable.startOverfling(-initialVelocity);
+ } else {
+ mFlingRunnable.startSpringback();
+ }
+
+ break;
+ }
+
+ setPressed(false);
+
+ if (mEdgeGlowTop != null) {
+ mEdgeGlowTop.onRelease();
+ mEdgeGlowBottom.onRelease();
+ }
+
+ // Need to redraw since we probably aren't drawing the selector anymore
+ invalidate();
+ removeCallbacks(mPendingCheckForLongPress);
+ recycleVelocityTracker();
+
+ mActivePointerId = INVALID_POINTER;
+
+ if (PROFILE_SCROLLING) {
+ if (mScrollProfilingStarted) {
+ Debug.stopMethodTracing();
+ mScrollProfilingStarted = false;
+ }
+ }
+
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+ }
+
+ private void onTouchCancel() {
+ switch (mTouchMode) {
+ case TOUCH_MODE_OVERSCROLL:
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ mFlingRunnable.startSpringback();
+ break;
+
+ case TOUCH_MODE_OVERFLING:
+ // Do nothing - let it play out.
+ break;
+
+ default:
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ final View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ clearScrollingCache();
+ removeCallbacks(mPendingCheckForLongPress);
+ recycleVelocityTracker();
+ }
+
+ if (mEdgeGlowTop != null) {
+ mEdgeGlowTop.onRelease();
+ mEdgeGlowBottom.onRelease();
+ }
+ mActivePointerId = INVALID_POINTER;
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+ if (mScrollY != scrollY) {
+ onScrollChanged(mScrollX, scrollY, mScrollX, mScrollY);
+ mScrollY = scrollY;
+ invalidateParentIfNeeded();
+
+ awakenScrollBars();
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL:
+ final float axisValue;
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ } else {
+ axisValue = 0;
+ }
+
+ final int delta = Math.round(axisValue * mVerticalScrollFactor);
+ if (delta != 0) {
+ if (!trackMotionScroll(delta, delta)) {
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ int actionButton = event.getActionButton();
+ if ((actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+ || actionButton == MotionEvent.BUTTON_SECONDARY)
+ && (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP)) {
+ if (performStylusButtonPressAction(event)) {
+ removeCallbacks(mPendingCheckForLongPress);
+ removeCallbacks(mPendingCheckForTap);
+ }
+ }
+ }
+ break;
+ }
+
+ return super.onGenericMotionEvent(event);
+ }
+
+ /**
+ * Initiate a fling with the given velocity.
+ *
+ * <p>Applications can use this method to manually initiate a fling as if the user
+ * initiated it via touch interaction.</p>
+ *
+ * @param velocityY Vertical velocity in pixels per second. Note that this is velocity of
+ * content, not velocity of a touch that initiated the fling.
+ */
+ public void fling(int velocityY) {
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ mFlingRunnable.start(velocityY);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return ((nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0);
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ final int motionIndex = getChildCount() / 2;
+ final View motionView = getChildAt(motionIndex);
+ final int oldTop = motionView != null ? motionView.getTop() : 0;
+ if (motionView == null || trackMotionScroll(-dyUnconsumed, -dyUnconsumed)) {
+ int myUnconsumed = dyUnconsumed;
+ int myConsumed = 0;
+ if (motionView != null) {
+ myConsumed = motionView.getTop() - oldTop;
+ myUnconsumed -= myConsumed;
+ }
+ dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
+ }
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ final int childCount = getChildCount();
+ if (!consumed && childCount > 0 && canScrollList((int) velocityY) &&
+ Math.abs(velocityY) > mMinimumVelocity) {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ if (!dispatchNestedPreFling(0, velocityY)) {
+ mFlingRunnable.start((int) velocityY);
+ }
+ return true;
+ }
+ return dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mEdgeGlowTop != null) {
+ final int scrollY = mScrollY;
+ final boolean clipToPadding = getClipToPadding();
+ final int width;
+ final int height;
+ final int translateX;
+ final int translateY;
+
+ if (clipToPadding) {
+ width = getWidth() - mPaddingLeft - mPaddingRight;
+ height = getHeight() - mPaddingTop - mPaddingBottom;
+ translateX = mPaddingLeft;
+ translateY = mPaddingTop;
+ } else {
+ width = getWidth();
+ height = getHeight();
+ translateX = 0;
+ translateY = 0;
+ }
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ canvas.clipRect(translateX, translateY,
+ translateX + width ,translateY + mEdgeGlowTop.getMaxHeight());
+ final int edgeY = Math.min(0, scrollY + mFirstPositionDistanceGuess) + translateY;
+ canvas.translate(translateX, edgeY);
+ mEdgeGlowTop.setSize(width, height);
+ if (mEdgeGlowTop.draw(canvas)) {
+ invalidateTopGlow();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowBottom.isFinished()) {
+ final int restoreCount = canvas.save();
+ canvas.clipRect(translateX, translateY + height - mEdgeGlowBottom.getMaxHeight(),
+ translateX + width, translateY + height);
+ final int edgeX = -width + translateX;
+ final int edgeY = Math.max(getHeight(), scrollY + mLastPositionDistanceGuess)
+ - (clipToPadding ? mPaddingBottom : 0);
+ canvas.translate(edgeX, edgeY);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ invalidateBottomGlow();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @Override
+ public boolean onInterceptHoverEvent(MotionEvent event) {
+ if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) {
+ return true;
+ }
+
+ return super.onInterceptHoverEvent(event);
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (mFastScroll != null) {
+ PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex);
+ if (pointerIcon != null) {
+ return pointerIcon;
+ }
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int actionMasked = ev.getActionMasked();
+ View v;
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ if (mIsDetaching || !isAttachedToWindow()) {
+ // Something isn't right.
+ // Since we rely on being attached to get data set change notifications,
+ // don't risk doing anything where we might try to resync and find things
+ // in a bogus state.
+ return false;
+ }
+
+ if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) {
+ return true;
+ }
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN: {
+ int touchMode = mTouchMode;
+ if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
+ mMotionCorrection = 0;
+ return true;
+ }
+
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+
+ int motionPosition = findMotionRow(y);
+ if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
+ // User clicked on an actual view (and was not stopping a fling).
+ // Remember where the motion event started
+ v = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = v.getTop();
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ mTouchMode = TOUCH_MODE_DOWN;
+ clearScrollingCache();
+ }
+ mLastY = Integer.MIN_VALUE;
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ mNestedYOffset = 0;
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+ if (touchMode == TOUCH_MODE_FLING) {
+ return true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ pointerIndex = 0;
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ }
+ final int y = (int) ev.getY(pointerIndex);
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
+ return true;
+ }
+ break;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mTouchMode = TOUCH_MODE_REST;
+ mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ stopNestedScroll();
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mMotionX = (int) ev.getX(newPointerIndex);
+ mMotionY = (int) ev.getY(newPointerIndex);
+ mMotionCorrection = 0;
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ }
+ }
+
+ /**
+ * {@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);
+ }
+ }
+
+ /**
+ * Fires an "on scroll state changed" event to the registered
+ * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change
+ * is fired only if the specified state is different from the previously known state.
+ *
+ * @param newState The new scroll state.
+ */
+ void reportScrollStateChange(int newState) {
+ if (newState != mLastScrollState) {
+ if (mOnScrollListener != null) {
+ mLastScrollState = newState;
+ mOnScrollListener.onScrollStateChanged(this, newState);
+ }
+ }
+ }
+
+ /**
+ * Responsible for fling behavior. Use {@link #start(int)} to
+ * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+ * A FlingRunnable will keep re-posting itself until the fling is done.
+ *
+ */
+ private class FlingRunnable implements Runnable {
+ /**
+ * Tracks the decay of a fling scroll
+ */
+ private final OverScroller mScroller;
+
+ /**
+ * Y value reported by mScroller on the previous fling
+ */
+ private int mLastFlingY;
+
+ /**
+ * If true, {@link #endFling()} will not report scroll state change to
+ * {@link OnScrollListener#SCROLL_STATE_IDLE}.
+ */
+ private boolean mSuppressIdleStateChangeCall;
+
+ private final Runnable mCheckFlywheel = new Runnable() {
+ @Override
+ public void run() {
+ final int activeId = mActivePointerId;
+ final VelocityTracker vt = mVelocityTracker;
+ final OverScroller scroller = mScroller;
+ if (vt == null || activeId == INVALID_POINTER) {
+ return;
+ }
+
+ vt.computeCurrentVelocity(1000, mMaximumVelocity);
+ final float yvel = -vt.getYVelocity(activeId);
+
+ if (Math.abs(yvel) >= mMinimumVelocity
+ && scroller.isScrollingInDirection(0, yvel)) {
+ // Keep the fling alive a little longer
+ postDelayed(this, FLYWHEEL_TIMEOUT);
+ } else {
+ endFling();
+ mTouchMode = TOUCH_MODE_SCROLL;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+ }
+ };
+
+ private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds
+
+ FlingRunnable() {
+ mScroller = new OverScroller(getContext());
+ }
+
+ void start(int initialVelocity) {
+ int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingY = initialY;
+ mScroller.setInterpolator(null);
+ mScroller.fling(0, initialY, 0, initialVelocity,
+ 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+ mTouchMode = TOUCH_MODE_FLING;
+ mSuppressIdleStateChangeCall = false;
+ postOnAnimation(this);
+
+ if (PROFILE_FLINGING) {
+ if (!mFlingProfilingStarted) {
+ Debug.startMethodTracing("AbsListViewFling");
+ mFlingProfilingStarted = true;
+ }
+ }
+
+ if (mFlingStrictSpan == null) {
+ mFlingStrictSpan = StrictMode.enterCriticalSpan("AbsListView-fling");
+ }
+ }
+
+ void startSpringback() {
+ mSuppressIdleStateChangeCall = false;
+ if (mScroller.springBack(0, mScrollY, 0, 0, 0, 0)) {
+ mTouchMode = TOUCH_MODE_OVERFLING;
+ invalidate();
+ postOnAnimation(this);
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ }
+
+ void startOverfling(int initialVelocity) {
+ mScroller.setInterpolator(null);
+ mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getHeight());
+ mTouchMode = TOUCH_MODE_OVERFLING;
+ mSuppressIdleStateChangeCall = false;
+ invalidate();
+ postOnAnimation(this);
+ }
+
+ void edgeReached(int delta) {
+ mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance);
+ final int overscrollMode = getOverScrollMode();
+ if (overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
+ mTouchMode = TOUCH_MODE_OVERFLING;
+ final int vel = (int) mScroller.getCurrVelocity();
+ if (delta > 0) {
+ mEdgeGlowTop.onAbsorb(vel);
+ } else {
+ mEdgeGlowBottom.onAbsorb(vel);
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ }
+ invalidate();
+ postOnAnimation(this);
+ }
+
+ void startScroll(int distance, int duration, boolean linear,
+ boolean suppressEndFlingStateChangeCall) {
+ int initialY = distance < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingY = initialY;
+ mScroller.setInterpolator(linear ? sLinearInterpolator : null);
+ mScroller.startScroll(0, initialY, 0, distance, duration);
+ mTouchMode = TOUCH_MODE_FLING;
+ mSuppressIdleStateChangeCall = suppressEndFlingStateChangeCall;
+ postOnAnimation(this);
+ }
+
+ void endFling() {
+ mTouchMode = TOUCH_MODE_REST;
+
+ removeCallbacks(this);
+ removeCallbacks(mCheckFlywheel);
+
+ if (!mSuppressIdleStateChangeCall) {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ clearScrollingCache();
+ mScroller.abortAnimation();
+
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+
+ void flywheelTouch() {
+ postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT);
+ }
+
+ @Override
+ public void run() {
+ switch (mTouchMode) {
+ default:
+ endFling();
+ return;
+
+ case TOUCH_MODE_SCROLL:
+ if (mScroller.isFinished()) {
+ return;
+ }
+ // Fall through
+ case TOUCH_MODE_FLING: {
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ if (mItemCount == 0 || getChildCount() == 0) {
+ endFling();
+ return;
+ }
+
+ final OverScroller 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);
+ }
+
+ // Check to see if we have bumped into the scroll limit
+ View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ int oldTop = 0;
+ if (motionView != null) {
+ oldTop = motionView.getTop();
+ }
+
+ // Don't stop just because delta is zero (it could have been rounded)
+ final boolean atEdge = trackMotionScroll(delta, delta);
+ final boolean atEnd = atEdge && (delta != 0);
+ if (atEnd) {
+ if (motionView != null) {
+ // Tweak the scroll for how far we overshot
+ int overshoot = -(delta - (motionView.getTop() - oldTop));
+ overScrollBy(0, overshoot, 0, mScrollY, 0, 0,
+ 0, mOverflingDistance, false);
+ }
+ if (more) {
+ edgeReached(delta);
+ }
+ break;
+ }
+
+ if (more && !atEnd) {
+ if (atEdge) invalidate();
+ mLastFlingY = y;
+ postOnAnimation(this);
+ } else {
+ endFling();
+
+ if (PROFILE_FLINGING) {
+ if (mFlingProfilingStarted) {
+ Debug.stopMethodTracing();
+ mFlingProfilingStarted = false;
+ }
+
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+ }
+ break;
+ }
+
+ case TOUCH_MODE_OVERFLING: {
+ final OverScroller scroller = mScroller;
+ if (scroller.computeScrollOffset()) {
+ final int scrollY = mScrollY;
+ final int currY = scroller.getCurrY();
+ final int deltaY = currY - scrollY;
+ if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
+ 0, mOverflingDistance, false)) {
+ final boolean crossDown = scrollY <= 0 && currY > 0;
+ final boolean crossUp = scrollY >= 0 && currY < 0;
+ if (crossDown || crossUp) {
+ int velocity = (int) scroller.getCurrVelocity();
+ if (crossUp) velocity = -velocity;
+
+ // Don't flywheel from this; we're just continuing things.
+ scroller.abortAnimation();
+ start(velocity);
+ } else {
+ startSpringback();
+ }
+ } else {
+ invalidate();
+ postOnAnimation(this);
+ }
+ } else {
+ endFling();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * The amount of friction applied to flings. The default value
+ * is {@link ViewConfiguration#getScrollFriction}.
+ */
+ public void setFriction(float friction) {
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ mFlingRunnable.mScroller.setFriction(friction);
+ }
+
+ /**
+ * Sets a scale factor for the fling velocity. The initial scale
+ * factor is 1.0.
+ *
+ * @param scale The scale factor to multiply the velocity by.
+ */
+ public void setVelocityScale(float scale) {
+ mVelocityScale = scale;
+ }
+
+ /**
+ * Override this for better control over position scrolling.
+ */
+ AbsPositionScroller createPositionScroller() {
+ return new PositionScroller();
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will
+ * scroll such that the indicated position is displayed.
+ * @param position Scroll to this adapter position.
+ */
+ public void smoothScrollToPosition(int position) {
+ if (mPositionScroller == null) {
+ mPositionScroller = createPositionScroller();
+ }
+ mPositionScroller.start(position);
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will scroll
+ * such that the indicated position is displayed <code>offset</code> pixels below
+ * the top edge of the view. If this is impossible, (e.g. the offset would scroll
+ * the first or last item beyond the boundaries of the list) it will get as close
+ * as possible. The scroll will take <code>duration</code> milliseconds to complete.
+ *
+ * @param position Position to scroll to
+ * @param offset Desired distance in pixels of <code>position</code> from the top
+ * of the view when scrolling is finished
+ * @param duration Number of milliseconds to use for the scroll
+ */
+ public void smoothScrollToPositionFromTop(int position, int offset, int duration) {
+ if (mPositionScroller == null) {
+ mPositionScroller = createPositionScroller();
+ }
+ mPositionScroller.startWithOffset(position, offset, duration);
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will scroll
+ * such that the indicated position is displayed <code>offset</code> pixels below
+ * the top edge of the view. If this is impossible, (e.g. the offset would scroll
+ * the first or last item beyond the boundaries of the list) it will get as close
+ * as possible.
+ *
+ * @param position Position to scroll to
+ * @param offset Desired distance in pixels of <code>position</code> from the top
+ * of the view when scrolling is finished
+ */
+ public void smoothScrollToPositionFromTop(int position, int offset) {
+ if (mPositionScroller == null) {
+ mPositionScroller = createPositionScroller();
+ }
+ mPositionScroller.startWithOffset(position, offset);
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will
+ * scroll such that the indicated position is displayed, but it will
+ * stop early if scrolling further would scroll boundPosition out of
+ * view.
+ *
+ * @param position Scroll to this adapter position.
+ * @param boundPosition Do not scroll if it would move this adapter
+ * position out of view.
+ */
+ public void smoothScrollToPosition(int position, int boundPosition) {
+ if (mPositionScroller == null) {
+ mPositionScroller = createPositionScroller();
+ }
+ mPositionScroller.start(position, boundPosition);
+ }
+
+ /**
+ * Smoothly scroll by distance pixels over duration milliseconds.
+ * @param distance Distance to scroll in pixels.
+ * @param duration Duration of the scroll animation in milliseconds.
+ */
+ public void smoothScrollBy(int distance, int duration) {
+ smoothScrollBy(distance, duration, false, false);
+ }
+
+ void smoothScrollBy(int distance, int duration, boolean linear,
+ boolean suppressEndFlingStateChangeCall) {
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+
+ // No sense starting to scroll if we're not going anywhere
+ final int firstPos = mFirstPosition;
+ final int childCount = getChildCount();
+ final int lastPos = firstPos + childCount;
+ final int topLimit = getPaddingTop();
+ final int bottomLimit = getHeight() - getPaddingBottom();
+
+ if (distance == 0 || mItemCount == 0 || childCount == 0 ||
+ (firstPos == 0 && getChildAt(0).getTop() == topLimit && distance < 0) ||
+ (lastPos == mItemCount &&
+ getChildAt(childCount - 1).getBottom() == bottomLimit && distance > 0)) {
+ mFlingRunnable.endFling();
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ } else {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ mFlingRunnable.startScroll(distance, duration, linear, suppressEndFlingStateChangeCall);
+ }
+ }
+
+ /**
+ * Allows RemoteViews to scroll relatively to a position.
+ */
+ void smoothScrollByOffset(int position) {
+ int index = -1;
+ if (position < 0) {
+ index = getFirstVisiblePosition();
+ } else if (position > 0) {
+ index = getLastVisiblePosition();
+ }
+
+ if (index > -1) {
+ View child = getChildAt(index - getFirstVisiblePosition());
+ if (child != null) {
+ Rect visibleRect = new Rect();
+ if (child.getGlobalVisibleRect(visibleRect)) {
+ // the child is partially visible
+ int childRectArea = child.getWidth() * child.getHeight();
+ int visibleRectArea = visibleRect.width() * visibleRect.height();
+ float visibleArea = (visibleRectArea / (float) childRectArea);
+ final float visibleThreshold = 0.75f;
+ if ((position < 0) && (visibleArea < visibleThreshold)) {
+ // the top index is not perceivably visible so offset
+ // to account for showing that top index as well
+ ++index;
+ } else if ((position > 0) && (visibleArea < visibleThreshold)) {
+ // the bottom index is not perceivably visible so offset
+ // to account for showing that bottom index as well
+ --index;
+ }
+ }
+ smoothScrollToPosition(Math.max(0, Math.min(getCount(), index + position)));
+ }
+ }
+ }
+
+ private void createScrollingCache() {
+ if (mScrollingCacheEnabled && !mCachingStarted && !isHardwareAccelerated()) {
+ setChildrenDrawnWithCacheEnabled(true);
+ setChildrenDrawingCacheEnabled(true);
+ mCachingStarted = mCachingActive = true;
+ }
+ }
+
+ private void clearScrollingCache() {
+ if (!isHardwareAccelerated()) {
+ if (mClearScrollingCache == null) {
+ mClearScrollingCache = new Runnable() {
+ @Override
+ public void run() {
+ if (mCachingStarted) {
+ mCachingStarted = mCachingActive = false;
+ setChildrenDrawnWithCacheEnabled(false);
+ if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) {
+ setChildrenDrawingCacheEnabled(false);
+ }
+ if (!isAlwaysDrawnWithCacheEnabled()) {
+ invalidate();
+ }
+ }
+ }
+ };
+ }
+ post(mClearScrollingCache);
+ }
+ }
+
+ /**
+ * Scrolls the list items within the view by a specified number of pixels.
+ *
+ * <p>The actual amount of scroll is capped by the list content viewport height
+ * which is the list height minus top and bottom paddings minus one pixel.</p>
+ *
+ * @param y the amount of pixels to scroll by vertically
+ * @see #canScrollList(int)
+ */
+ public void scrollListBy(int y) {
+ trackMotionScroll(-y, -y);
+ }
+
+ /**
+ * Check if the items in the list can be scrolled in a certain direction.
+ *
+ * @param direction Negative to check scrolling up, positive to check
+ * scrolling down.
+ * @return true if the list can be scrolled in the specified direction,
+ * false otherwise.
+ * @see #scrollListBy(int)
+ */
+ public boolean canScrollList(int direction) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return false;
+ }
+
+ final int firstPosition = mFirstPosition;
+ final Rect listPadding = mListPadding;
+ if (direction > 0) {
+ final int lastBottom = getChildAt(childCount - 1).getBottom();
+ final int lastPosition = firstPosition + childCount;
+ return lastPosition < mItemCount || lastBottom > getHeight() - listPadding.bottom;
+ } else {
+ final int firstTop = getChildAt(0).getTop();
+ return firstPosition > 0 || firstTop < listPadding.top;
+ }
+ }
+
+ /**
+ * Track a motion scroll
+ *
+ * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
+ * began. Positive numbers mean the user's finger is moving down the screen.
+ * @param incrementalDeltaY Change in deltaY from the previous event.
+ * @return true if we're already at the beginning/end of the list and have nothing to do.
+ */
+ boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return true;
+ }
+
+ final int firstTop = getChildAt(0).getTop();
+ final int lastBottom = getChildAt(childCount - 1).getBottom();
+
+ final Rect listPadding = mListPadding;
+
+ // "effective padding" In this case is the amount of padding that affects
+ // how much space should not be filled by items. If we don't clip to padding
+ // there is no effective padding.
+ int effectivePaddingTop = 0;
+ int effectivePaddingBottom = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ effectivePaddingTop = listPadding.top;
+ effectivePaddingBottom = listPadding.bottom;
+ }
+
+ // FIXME account for grid vertical spacing too?
+ final int spaceAbove = effectivePaddingTop - firstTop;
+ final int end = getHeight() - effectivePaddingBottom;
+ 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 firstPosition = mFirstPosition;
+
+ // Update our guesses for where the first and last views are
+ if (firstPosition == 0) {
+ mFirstPositionDistanceGuess = firstTop - listPadding.top;
+ } else {
+ mFirstPositionDistanceGuess += incrementalDeltaY;
+ }
+ if (firstPosition + childCount == mItemCount) {
+ mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
+ } else {
+ mLastPositionDistanceGuess += incrementalDeltaY;
+ }
+
+ final boolean cannotScrollDown = (firstPosition == 0 &&
+ firstTop >= listPadding.top && incrementalDeltaY >= 0);
+ final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
+ lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
+
+ if (cannotScrollDown || cannotScrollUp) {
+ return incrementalDeltaY != 0;
+ }
+
+ final boolean down = incrementalDeltaY < 0;
+
+ final boolean inTouchMode = isInTouchMode();
+ if (inTouchMode) {
+ hideSelector();
+ }
+
+ final int headerViewsCount = getHeaderViewsCount();
+ final int footerViewsStart = mItemCount - getFooterViewsCount();
+
+ int start = 0;
+ int count = 0;
+
+ if (down) {
+ int top = -incrementalDeltaY;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ top += listPadding.top;
+ }
+ 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) {
+ // The view will be rebound to new data, clear any
+ // system-managed transient state.
+ child.clearAccessibilityFocus();
+ mRecycler.addScrapView(child, position);
+ }
+ }
+ }
+ } else {
+ int bottom = getHeight() - incrementalDeltaY;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ bottom -= listPadding.bottom;
+ }
+ 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) {
+ // The view will be rebound to new data, clear any
+ // system-managed transient state.
+ child.clearAccessibilityFocus();
+ mRecycler.addScrapView(child, position);
+ }
+ }
+ }
+ }
+
+ mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
+
+ mBlockLayoutRequests = true;
+
+ if (count > 0) {
+ detachViewsFromParent(start, count);
+ mRecycler.removeSkippedScrap();
+ }
+
+ // invalidate before moving the children to avoid unnecessary invalidate
+ // calls to bubble up from the children all the way to the top
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+
+ offsetChildrenTopAndBottom(incrementalDeltaY);
+
+ if (down) {
+ mFirstPosition += count;
+ }
+
+ final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
+ if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
+ fillGap(down);
+ }
+
+ mRecycler.fullyDetachScrapViews();
+ if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
+ final int childIndex = mSelectedPosition - mFirstPosition;
+ if (childIndex >= 0 && childIndex < getChildCount()) {
+ positionSelector(mSelectedPosition, getChildAt(childIndex));
+ }
+ } else if (mSelectorPosition != INVALID_POSITION) {
+ final int childIndex = mSelectorPosition - mFirstPosition;
+ if (childIndex >= 0 && childIndex < getChildCount()) {
+ positionSelector(INVALID_POSITION, getChildAt(childIndex));
+ }
+ } else {
+ mSelectorRect.setEmpty();
+ }
+
+ mBlockLayoutRequests = false;
+
+ invokeOnItemScrollListener();
+
+ return false;
+ }
+
+ /**
+ * Returns the number of header views in the list. Header views are special views
+ * at the top of the list that should not be recycled during a layout.
+ *
+ * @return The number of header views, 0 in the default implementation.
+ */
+ int getHeaderViewsCount() {
+ return 0;
+ }
+
+ /**
+ * Returns the number of footer views in the list. Footer views are special views
+ * at the bottom of the list that should not be recycled during a layout.
+ *
+ * @return The number of footer views, 0 in the default implementation.
+ */
+ int getFooterViewsCount() {
+ return 0;
+ }
+
+ /**
+ * Fills the gap left open by a touch-scroll. During a touch scroll, children that
+ * remain on screen are shifted and the other ones are discarded. The role of this
+ * method is to fill the gap thus created by performing a partial layout in the
+ * empty space.
+ *
+ * @param down true if the scroll is going down, false if it is going up
+ */
+ abstract void fillGap(boolean down);
+
+ void hideSelector() {
+ if (mSelectedPosition != INVALID_POSITION) {
+ if (mLayoutMode != LAYOUT_SPECIFIC) {
+ mResurrectToPosition = mSelectedPosition;
+ }
+ if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
+ mResurrectToPosition = mNextSelectedPosition;
+ }
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ mSelectedTop = 0;
+ }
+ }
+
+ /**
+ * @return A position to select. First we try mSelectedPosition. If that has been clobbered by
+ * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range
+ * of items available in the adapter
+ */
+ int reconcileSelectedPosition() {
+ int position = mSelectedPosition;
+ if (position < 0) {
+ position = mResurrectToPosition;
+ }
+ position = Math.max(0, position);
+ position = Math.min(position, mItemCount - 1);
+ return position;
+ }
+
+ /**
+ * Find the row closest to y. This row will be used as the motion row when scrolling
+ *
+ * @param y Where the user touched
+ * @return The position of the first (or only) item in the row containing y
+ */
+ abstract int findMotionRow(int y);
+
+ /**
+ * Find the row closest to y. This row will be used as the motion row when scrolling.
+ *
+ * @param y Where the user touched
+ * @return The position of the first (or only) item in the row closest to y
+ */
+ int findClosestMotionRow(int y) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return INVALID_POSITION;
+ }
+
+ final int motionRow = findMotionRow(y);
+ return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1;
+ }
+
+ /**
+ * Causes all the views to be rebuilt and redrawn.
+ */
+ public void invalidateViews() {
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * If there is a selection returns false.
+ * Otherwise resurrects the selection and returns true if resurrected.
+ */
+ boolean resurrectSelectionIfNeeded() {
+ if (mSelectedPosition < 0 && resurrectSelection()) {
+ updateSelectorState();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 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);
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ mTouchMode = TOUCH_MODE_REST;
+ clearScrollingCache();
+ mSpecificTop = selectedTop;
+ selectedPos = lookForSelectablePosition(selectedPos, down);
+ if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) {
+ mLayoutMode = LAYOUT_SPECIFIC;
+ updateSelectorState();
+ setSelectionInt(selectedPos);
+ invokeOnItemScrollListener();
+ } else {
+ selectedPos = INVALID_POSITION;
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+
+ return selectedPos >= 0;
+ }
+
+ void confirmCheckedPositionsById() {
+ // Clear out the positional check states, we'll rebuild it below from IDs.
+ mCheckStates.clear();
+
+ boolean checkedCountChanged = false;
+ for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
+ final long id = mCheckedIdStates.keyAt(checkedIndex);
+ final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
+
+ final long lastPosId = mAdapter.getItemId(lastPos);
+ if (id != lastPosId) {
+ // Look around to see if the ID is nearby. If not, uncheck it.
+ final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
+ final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
+ boolean found = false;
+ for (int searchPos = start; searchPos < end; searchPos++) {
+ final long searchId = mAdapter.getItemId(searchPos);
+ if (id == searchId) {
+ found = true;
+ mCheckStates.put(searchPos, true);
+ mCheckedIdStates.setValueAt(checkedIndex, searchPos);
+ break;
+ }
+ }
+
+ if (!found) {
+ mCheckedIdStates.delete(id);
+ checkedIndex--;
+ mCheckedItemCount--;
+ checkedCountChanged = true;
+ if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) {
+ mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+ lastPos, id, false);
+ }
+ }
+ } else {
+ mCheckStates.put(lastPos, true);
+ }
+ }
+
+ if (checkedCountChanged && mChoiceActionMode != null) {
+ mChoiceActionMode.invalidate();
+ }
+ }
+
+ @Override
+ protected void handleDataChanged() {
+ int count = mItemCount;
+ int lastHandledItemCount = mLastHandledItemCount;
+ mLastHandledItemCount = mItemCount;
+
+ if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) {
+ confirmCheckedPositionsById();
+ }
+
+ // TODO: In the future we can recycle these views based on stable ID instead.
+ mRecycler.clearTransientStateViews();
+
+ 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;
+ mPendingSync = null;
+
+ if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ return;
+ } else if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
+ if (mForceTranscriptScroll) {
+ mForceTranscriptScroll = false;
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ return;
+ }
+ final int childCount = getChildCount();
+ final int listBottom = getHeight() - getPaddingBottom();
+ final View lastChild = getChildAt(childCount - 1);
+ final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
+ if (mFirstPosition + childCount >= lastHandledItemCount &&
+ lastBottom <= listBottom) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ return;
+ }
+ // Something new came in and we didn't scroll; give the user a clue that
+ // there's something new.
+ awakenScrollBars();
+ }
+
+ 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;
+ mPendingSync = null;
+ mSelectorPosition = INVALID_POSITION;
+ checkSelectionChanged();
+ }
+
+ @Override
+ protected void onDisplayHint(int hint) {
+ super.onDisplayHint(hint);
+ switch (hint) {
+ case INVISIBLE:
+ if (mPopup != null && mPopup.isShowing()) {
+ dismissPopup();
+ }
+ break;
+ case VISIBLE:
+ if (mFiltered && mPopup != null && !mPopup.isShowing()) {
+ showPopup();
+ }
+ break;
+ }
+ mPopupHidden = hint == INVISIBLE;
+ }
+
+ /**
+ * Removes the filter window
+ */
+ private 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) {
+ createTextFilter(true);
+ positionPopup();
+ // Make sure we get focus if we are showing the popup
+ checkFocus();
+ }
+ }
+
+ private void positionPopup() {
+ int screenHeight = getResources().getDisplayMetrics().heightPixels;
+ final int[] xy = new int[2];
+ getLocationOnScreen(xy);
+ // TODO: The 20 below should come from the theme
+ // TODO: And the gravity should be defined in the theme as well
+ final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20);
+ if (!mPopup.isShowing()) {
+ mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+ xy[0], bottomGap);
+ } else {
+ mPopup.update(xy[0], bottomGap, -1, -1);
+ }
+ }
+
+ /**
+ * 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;
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_BACKWARD:
+ sX = source.right + source.width() / 2;
+ sY = source.top + source.height() / 2;
+ dX = dest.left + dest.width() / 2;
+ dY = dest.top + dest.height() / 2;
+ break;
+ default:
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+ + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
+ }
+ 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 (!acceptFilter()) {
+ 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()) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ handled = true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP
+ && event.isTracking() && !event.isCanceled()) {
+ handled = true;
+ mTextFilter.setText("");
+ }
+ }
+ okToSend = false;
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ // Only send spaces once we are filtered
+ okToSend = mFiltered;
+ break;
+ }
+
+ if (okToSend) {
+ createTextFilter(true);
+
+ KeyEvent forwardEvent = event;
+ if (forwardEvent.getRepeatCount() > 0) {
+ forwardEvent = KeyEvent.changeTimeRepeat(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;
+ }
+
+ /**
+ * Return an InputConnection for editing of the filter text.
+ */
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (isTextFilterEnabled()) {
+ if (mPublicInputConnection == null) {
+ mDefInputConnection = new BaseInputConnection(this, false);
+ mPublicInputConnection = new InputConnectionWrapper(outAttrs);
+ }
+ outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_FILTER;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+ return mPublicInputConnection;
+ }
+ return null;
+ }
+
+ private class InputConnectionWrapper implements InputConnection {
+ private final EditorInfo mOutAttrs;
+ private InputConnection mTarget;
+
+ public InputConnectionWrapper(EditorInfo outAttrs) {
+ mOutAttrs = outAttrs;
+ }
+
+ private InputConnection getTarget() {
+ if (mTarget == null) {
+ mTarget = getTextFilterInput().onCreateInputConnection(mOutAttrs);
+ }
+ return mTarget;
+ }
+
+ @Override
+ public boolean reportFullscreenMode(boolean enabled) {
+ // Use our own input connection, since it is
+ // the "real" one the IME is talking with.
+ return mDefInputConnection.reportFullscreenMode(enabled);
+ }
+
+ @Override
+ public boolean performEditorAction(int editorAction) {
+ // The editor is off in its own window; we need to be
+ // the one that does this.
+ if (editorAction == EditorInfo.IME_ACTION_DONE) {
+ InputMethodManager imm =
+ getContext().getSystemService(InputMethodManager.class);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ // Use our own input connection, since the filter
+ // text view may not be shown in a window so has
+ // no ViewAncestor to dispatch events with.
+ return mDefInputConnection.sendKeyEvent(event);
+ }
+
+ @Override
+ public CharSequence getTextBeforeCursor(int n, int flags) {
+ if (mTarget == null) return "";
+ return mTarget.getTextBeforeCursor(n, flags);
+ }
+
+ @Override
+ public CharSequence getTextAfterCursor(int n, int flags) {
+ if (mTarget == null) return "";
+ return mTarget.getTextAfterCursor(n, flags);
+ }
+
+ @Override
+ public CharSequence getSelectedText(int flags) {
+ if (mTarget == null) return "";
+ return mTarget.getSelectedText(flags);
+ }
+
+ @Override
+ public int getCursorCapsMode(int reqModes) {
+ if (mTarget == null) return InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ return mTarget.getCursorCapsMode(reqModes);
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+ return getTarget().getExtractedText(request, flags);
+ }
+
+ @Override
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ return getTarget().deleteSurroundingText(beforeLength, afterLength);
+ }
+
+ @Override
+ public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+ return getTarget().deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+ }
+
+ @Override
+ public boolean setComposingText(CharSequence text, int newCursorPosition) {
+ return getTarget().setComposingText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean setComposingRegion(int start, int end) {
+ return getTarget().setComposingRegion(start, end);
+ }
+
+ @Override
+ public boolean finishComposingText() {
+ return mTarget == null || mTarget.finishComposingText();
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ return getTarget().commitText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean commitCompletion(CompletionInfo text) {
+ return getTarget().commitCompletion(text);
+ }
+
+ @Override
+ public boolean commitCorrection(CorrectionInfo correctionInfo) {
+ return getTarget().commitCorrection(correctionInfo);
+ }
+
+ @Override
+ public boolean setSelection(int start, int end) {
+ return getTarget().setSelection(start, end);
+ }
+
+ @Override
+ public boolean performContextMenuAction(int id) {
+ return getTarget().performContextMenuAction(id);
+ }
+
+ @Override
+ public boolean beginBatchEdit() {
+ return getTarget().beginBatchEdit();
+ }
+
+ @Override
+ public boolean endBatchEdit() {
+ return getTarget().endBatchEdit();
+ }
+
+ @Override
+ public boolean clearMetaKeyStates(int states) {
+ return getTarget().clearMetaKeyStates(states);
+ }
+
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ return getTarget().performPrivateCommand(action, data);
+ }
+
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+ return getTarget().requestCursorUpdates(cursorUpdateMode);
+ }
+
+ @Override
+ public Handler getHandler() {
+ return getTarget().getHandler();
+ }
+
+ @Override
+ public void closeConnection() {
+ getTarget().closeConnection();
+ }
+
+ @Override
+ public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
+ return getTarget().commitContent(inputContentInfo, flags, opts);
+ }
+ }
+
+ /**
+ * For filtering we proxy an input connection to an internal text editor,
+ * and this allows the proxying to happen.
+ */
+ @Override
+ public boolean checkInputConnectionProxy(View view) {
+ return view == mTextFilter;
+ }
+
+ /**
+ * 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) {
+ PopupWindow p = new PopupWindow(getContext());
+ p.setFocusable(false);
+ p.setTouchable(false);
+ p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ p.setContentView(getTextFilterInput());
+ p.setWidth(LayoutParams.WRAP_CONTENT);
+ p.setHeight(LayoutParams.WRAP_CONTENT);
+ p.setBackgroundDrawable(null);
+ mPopup = p;
+ getViewTreeObserver().addOnGlobalLayoutListener(this);
+ mGlobalLayoutListenerAddedFilter = true;
+ }
+ if (animateEntrance) {
+ mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter);
+ } else {
+ mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore);
+ }
+ }
+
+ private EditText getTextFilterInput() {
+ if (mTextFilter == null) {
+ final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+ mTextFilter = (EditText) layoutInflater.inflate(
+ com.android.internal.R.layout.typing_filter, null);
+ // For some reason setting this as the "real" input type changes
+ // the text view in some way that it doesn't work, and I don't
+ // want to figure out why this is.
+ mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_FILTER);
+ mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+ mTextFilter.addTextChangedListener(this);
+ }
+ return mTextFilter;
+ }
+
+ /**
+ * Clear the text filter.
+ */
+ public void clearTextFilter() {
+ if (mFiltered) {
+ getTextFilterInput().setText("");
+ mFiltered = false;
+ if (mPopup != null && mPopup.isShowing()) {
+ dismissPopup();
+ }
+ }
+ }
+
+ /**
+ * Returns if the ListView currently has a text filter.
+ */
+ public boolean hasTextFilter() {
+ return mFiltered;
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ if (isShown()) {
+ // Show the popup if we are filtered
+ if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) {
+ showPopup();
+ }
+ } else {
+ // Hide the popup when we are no longer visible
+ if (mPopup != null && mPopup.isShowing()) {
+ dismissPopup();
+ }
+ }
+
+ }
+
+ /**
+ * For our text watcher that is associated with the text filter. Does
+ * nothing.
+ */
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ /**
+ * For our text watcher that is associated with the text filter. Performs
+ * the actual filtering as the text changes, and takes care of hiding and
+ * showing the popup displaying the currently entered filter text.
+ */
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (isTextFilterEnabled()) {
+ createTextFilter(true);
+ 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
+ dismissPopup();
+ 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 is associated with the text filter. Does
+ * nothing.
+ */
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ @Override
+ public void onFilterComplete(int count) {
+ if (mSelectedPosition < 0 && count > 0) {
+ mResurrectToPosition = INVALID_POSITION;
+ resurrectSelection();
+ }
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ }
+
+ @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.
+ *
+ * Zero means that what's behind this object is translucent (non solid) or is not made of a
+ * single color. This hint will not affect any existing background drawable set on this view (
+ * typically set via {@link #setBackgroundDrawable(Drawable)}).
+ *
+ * @param color The background color
+ */
+ public void setCacheColorHint(@ColorInt int color) {
+ if (color != mCacheColorHint) {
+ mCacheColorHint = color;
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).setDrawingCacheBackgroundColor(color);
+ }
+ mRecycler.setCacheColorHint(color);
+ }
+ }
+
+ /**
+ * When set to a non-zero value, the cache color hint indicates that this list is always drawn
+ * on top of a solid, single-color, opaque background
+ *
+ * @return The cache color hint
+ */
+ @ViewDebug.ExportedProperty(category = "drawing")
+ @ColorInt
+ 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);
+ child.setAccessibilityDelegate(null);
+ if (listener != null) {
+ // Pretend they went through the scrap heap
+ listener.onMovedToScrapHeap(child);
+ }
+ }
+ }
+ mRecycler.reclaimScrapViews(views);
+ removeAllViewsInLayout();
+ }
+
+ private void finishGlows() {
+ if (mEdgeGlowTop != null) {
+ mEdgeGlowTop.finish();
+ mEdgeGlowBottom.finish();
+ }
+ }
+
+ /**
+ * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+ * through the specified intent.
+ * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+ */
+ public void setRemoteViewsAdapter(Intent intent) {
+ setRemoteViewsAdapter(intent, false);
+ }
+
+ /** @hide **/
+ public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+ return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+ }
+
+ /** @hide **/
+ @Override
+ public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
+ // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+ // service handling the specified intent.
+ if (mRemoteAdapter != null) {
+ Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+ Intent.FilterComparison fcOld = new Intent.FilterComparison(
+ mRemoteAdapter.getRemoteViewsServiceIntent());
+ if (fcNew.equals(fcOld)) {
+ return;
+ }
+ }
+ mDeferNotifyDataSetChanged = false;
+ // Otherwise, create a new RemoteViewsAdapter for binding
+ mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
+ if (mRemoteAdapter.isDataReady()) {
+ setAdapter(mRemoteAdapter);
+ }
+ }
+
+ /**
+ * Sets up the onClickHandler to be used by the RemoteViewsAdapter when inflating RemoteViews
+ *
+ * @param handler The OnClickHandler to use when inflating RemoteViews.
+ *
+ * @hide
+ */
+ public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+ // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+ // service handling the specified intent.
+ if (mRemoteAdapter != null) {
+ mRemoteAdapter.setRemoteViewsOnClickHandler(handler);
+ }
+ }
+
+ /**
+ * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+ * connected yet.
+ */
+ @Override
+ public void deferNotifyDataSetChanged() {
+ mDeferNotifyDataSetChanged = true;
+ }
+
+ /**
+ * Called back when the adapter connects to the RemoteViewsService.
+ */
+ @Override
+ public boolean onRemoteAdapterConnected() {
+ if (mRemoteAdapter != mAdapter) {
+ setAdapter(mRemoteAdapter);
+ if (mDeferNotifyDataSetChanged) {
+ mRemoteAdapter.notifyDataSetChanged();
+ mDeferNotifyDataSetChanged = false;
+ }
+ return false;
+ } else if (mRemoteAdapter != null) {
+ mRemoteAdapter.superNotifyDataSetChanged();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called back when the adapter disconnects from the RemoteViewsService.
+ */
+ @Override
+ public void onRemoteAdapterDisconnected() {
+ // If the remote adapter disconnects, we keep it around
+ // since the currently displayed items are still cached.
+ // Further, we want the service to eventually reconnect
+ // when necessary, as triggered by this view requesting
+ // items from the Adapter.
+ }
+
+ /**
+ * Hints the RemoteViewsAdapter, if it exists, about which views are currently
+ * being displayed by the AbsListView.
+ */
+ void setVisibleRangeHint(int start, int end) {
+ if (mRemoteAdapter != null) {
+ mRemoteAdapter.setVisibleRangeHint(start, end);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ if (mFastScroll != null) {
+ mFastScroll.onSectionsChanged();
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ if (mFastScroll != null) {
+ mFastScroll.onSectionsChanged();
+ }
+ }
+ }
+
+ /**
+ * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}.
+ * It acts as the {@link ActionMode.Callback} for the selection mode and also receives
+ * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user
+ * selects and deselects list items.
+ */
+ public interface MultiChoiceModeListener extends ActionMode.Callback {
+ /**
+ * Called when an item is checked or unchecked during selection mode.
+ *
+ * @param mode The {@link ActionMode} providing the selection mode
+ * @param position Adapter position of the item that was checked or unchecked
+ * @param id Adapter ID of the item that was checked or unchecked
+ * @param checked <code>true</code> if the item is now checked, <code>false</code>
+ * if the item is now unchecked.
+ */
+ public void onItemCheckedStateChanged(ActionMode mode,
+ int position, long id, boolean checked);
+ }
+
+ class MultiChoiceModeWrapper implements MultiChoiceModeListener {
+ private MultiChoiceModeListener mWrapped;
+
+ public void setWrapped(MultiChoiceModeListener wrapped) {
+ mWrapped = wrapped;
+ }
+
+ public boolean hasWrappedCallback() {
+ return mWrapped != null;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ if (mWrapped.onCreateActionMode(mode, menu)) {
+ // Initialize checked graphic state?
+ setLongClickable(false);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return mWrapped.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return mWrapped.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mWrapped.onDestroyActionMode(mode);
+ mChoiceActionMode = null;
+
+ // Ending selection mode means deselecting everything.
+ clearChoices();
+
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+
+ setLongClickable(true);
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode,
+ int position, long id, boolean checked) {
+ mWrapped.onItemCheckedStateChanged(mode, position, id, checked);
+
+ // If there are no items selected we no longer need the selection mode.
+ if (getCheckedItemCount() == 0) {
+ mode.finish();
+ }
+ }
+ }
+
+ /**
+ * AbsListView extends LayoutParams to provide a place to hold the view type.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * View type for this view, as returned by
+ * {@link android.widget.Adapter#getItemViewType(int) }
+ */
+ @ViewDebug.ExportedProperty(category = "list", mapping = {
+ @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"),
+ @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER")
+ })
+ int viewType;
+
+ /**
+ * When this boolean is set, the view has been added to the AbsListView
+ * at least once. It is used to know whether headers/footers have already
+ * been added to the list view and whether they should be treated as
+ * recycled views or not.
+ */
+ @ViewDebug.ExportedProperty(category = "list")
+ boolean recycledHeaderFooter;
+
+ /**
+ * When an AbsListView is measured with an AT_MOST measure spec, it needs
+ * to obtain children views to measure itself. When doing so, the children
+ * are not attached to the window, but put in the recycler which assumes
+ * they've been attached before. Setting this flag will force the reused
+ * view to be attached to the window rather than just attached to the
+ * parent.
+ */
+ @ViewDebug.ExportedProperty(category = "list")
+ boolean forceAdd;
+
+ /**
+ * The position the view was removed from when pulled out of the
+ * scrap heap.
+ * @hide
+ */
+ int scrappedFromPosition;
+
+ /**
+ * The ID the view represents
+ */
+ long itemId = -1;
+
+ /** Whether the adapter considers the item enabled. */
+ boolean isEnabled;
+
+ 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);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("list:viewType", viewType);
+ encoder.addProperty("list:recycledHeaderFooter", recycledHeaderFooter);
+ encoder.addProperty("list:forceAdd", forceAdd);
+ encoder.addProperty("list:isEnabled", isEnabled);
+ }
+ }
+
+ /**
+ * 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;
+
+ private ArrayList<View> mSkippedScrap;
+
+ private SparseArray<View> mTransientStateViews;
+ private LongSparseArray<View> mTransientStateViewsById;
+
+ 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 void markChildrenDirty() {
+ if (mViewTypeCount == 1) {
+ final ArrayList<View> scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ scrap.get(i).forceLayout();
+ }
+ } else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList<View> scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ scrap.get(j).forceLayout();
+ }
+ }
+ }
+ if (mTransientStateViews != null) {
+ final int count = mTransientStateViews.size();
+ for (int i = 0; i < count; i++) {
+ mTransientStateViews.valueAt(i).forceLayout();
+ }
+ }
+ if (mTransientStateViewsById != null) {
+ final int count = mTransientStateViewsById.size();
+ for (int i = 0; i < count; i++) {
+ mTransientStateViewsById.valueAt(i).forceLayout();
+ }
+ }
+ }
+
+ public boolean shouldRecycleViewType(int viewType) {
+ return viewType >= 0;
+ }
+
+ /**
+ * Clears the scrap heap.
+ */
+ void clear() {
+ if (mViewTypeCount == 1) {
+ final ArrayList<View> scrap = mCurrentScrap;
+ clearScrap(scrap);
+ } else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList<View> scrap = mScrapViews[i];
+ clearScrap(scrap);
+ }
+ }
+
+ clearTransientStateViews();
+ }
+
+ /**
+ * 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;
+
+ //noinspection MismatchedReadAndWriteOfArray
+ 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 != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
+ // However, we will NOT place them into scrap views.
+ activeViews[i] = child;
+ // Remember the position so that setupChild() doesn't reset state.
+ lp.scrappedFromPosition = firstActivePosition + 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;
+ }
+
+ View getTransientStateView(int position) {
+ if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
+ long id = mAdapter.getItemId(position);
+ View result = mTransientStateViewsById.get(id);
+ mTransientStateViewsById.remove(id);
+ return result;
+ }
+ if (mTransientStateViews != null) {
+ final int index = mTransientStateViews.indexOfKey(position);
+ if (index >= 0) {
+ View result = mTransientStateViews.valueAt(index);
+ mTransientStateViews.removeAt(index);
+ return result;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Dumps and fully detaches any currently saved views with transient
+ * state.
+ */
+ void clearTransientStateViews() {
+ final SparseArray<View> viewsByPos = mTransientStateViews;
+ if (viewsByPos != null) {
+ final int N = viewsByPos.size();
+ for (int i = 0; i < N; i++) {
+ removeDetachedView(viewsByPos.valueAt(i), false);
+ }
+ viewsByPos.clear();
+ }
+
+ final LongSparseArray<View> viewsById = mTransientStateViewsById;
+ if (viewsById != null) {
+ final int N = viewsById.size();
+ for (int i = 0; i < N; i++) {
+ removeDetachedView(viewsById.valueAt(i), false);
+ }
+ viewsById.clear();
+ }
+ }
+
+ /**
+ * @return A view from the ScrapViews collection. These are unordered.
+ */
+ View getScrapView(int position) {
+ final int whichScrap = mAdapter.getItemViewType(position);
+ if (whichScrap < 0) {
+ return null;
+ }
+ if (mViewTypeCount == 1) {
+ return retrieveFromScrap(mCurrentScrap, position);
+ } else if (whichScrap < mScrapViews.length) {
+ return retrieveFromScrap(mScrapViews[whichScrap], position);
+ }
+ return null;
+ }
+
+ /**
+ * Puts a view into the list of scrap views.
+ * <p>
+ * If the list data hasn't changed or the adapter has stable IDs, views
+ * with transient state will be preserved for later retrieval.
+ *
+ * @param scrap The view to add
+ * @param position The view's position within its parent
+ */
+ void addScrapView(View scrap, int position) {
+ final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
+ if (lp == null) {
+ // Can't recycle, but we don't know anything about the view.
+ // Ignore it completely.
+ return;
+ }
+
+ lp.scrappedFromPosition = position;
+
+ // Remove but don't scrap header or footer views, or views that
+ // should otherwise not be recycled.
+ final int viewType = lp.viewType;
+ if (!shouldRecycleViewType(viewType)) {
+ // Can't recycle. If it's not a header or footer, which have
+ // special handling and should be ignored, then skip the scrap
+ // heap and we'll fully detach the view later.
+ if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ getSkippedScrap().add(scrap);
+ }
+ return;
+ }
+
+ scrap.dispatchStartTemporaryDetach();
+
+ // The the accessibility state of the view may change while temporary
+ // detached and we do not allow detached views to fire accessibility
+ // events. So we are announcing that the subtree changed giving a chance
+ // to clients holding on to a view in this subtree to refresh it.
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+
+ // Don't scrap views that have transient state.
+ final boolean scrapHasTransientState = scrap.hasTransientState();
+ if (scrapHasTransientState) {
+ if (mAdapter != null && mAdapterHasStableIds) {
+ // If the adapter has stable IDs, we can reuse the view for
+ // the same data.
+ if (mTransientStateViewsById == null) {
+ mTransientStateViewsById = new LongSparseArray<>();
+ }
+ mTransientStateViewsById.put(lp.itemId, scrap);
+ } else if (!mDataChanged) {
+ // If the data hasn't changed, we can reuse the views at
+ // their old positions.
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArray<>();
+ }
+ mTransientStateViews.put(position, scrap);
+ } else {
+ // Otherwise, we'll have to remove the view and start over.
+ clearScrapForRebind(scrap);
+ getSkippedScrap().add(scrap);
+ }
+ } else {
+ clearScrapForRebind(scrap);
+ if (mViewTypeCount == 1) {
+ mCurrentScrap.add(scrap);
+ } else {
+ mScrapViews[viewType].add(scrap);
+ }
+
+ if (mRecyclerListener != null) {
+ mRecyclerListener.onMovedToScrapHeap(scrap);
+ }
+ }
+ }
+
+ private ArrayList<View> getSkippedScrap() {
+ if (mSkippedScrap == null) {
+ mSkippedScrap = new ArrayList<>();
+ }
+ return mSkippedScrap;
+ }
+
+ /**
+ * Finish the removal of any views that skipped the scrap heap.
+ */
+ void removeSkippedScrap() {
+ if (mSkippedScrap == null) {
+ return;
+ }
+ final int count = mSkippedScrap.size();
+ for (int i = 0; i < count; i++) {
+ removeDetachedView(mSkippedScrap.get(i), false);
+ }
+ mSkippedScrap.clear();
+ }
+
+ /**
+ * 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 = count - 1; i >= 0; i--) {
+ final View victim = activeViews[i];
+ if (victim != null) {
+ final AbsListView.LayoutParams lp
+ = (AbsListView.LayoutParams) victim.getLayoutParams();
+ final int whichScrap = lp.viewType;
+
+ activeViews[i] = null;
+
+ if (victim.hasTransientState()) {
+ // Store views with transient state for later use.
+ victim.dispatchStartTemporaryDetach();
+
+ if (mAdapter != null && mAdapterHasStableIds) {
+ if (mTransientStateViewsById == null) {
+ mTransientStateViewsById = new LongSparseArray<View>();
+ }
+ long id = mAdapter.getItemId(mFirstActivePosition + i);
+ mTransientStateViewsById.put(id, victim);
+ } else if (!mDataChanged) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArray<View>();
+ }
+ mTransientStateViews.put(mFirstActivePosition + i, victim);
+ } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ // The data has changed, we can't keep this view.
+ removeDetachedView(victim, false);
+ }
+ } else if (!shouldRecycleViewType(whichScrap)) {
+ // Discard non-recyclable views except headers/footers.
+ if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ removeDetachedView(victim, false);
+ }
+ } else {
+ // Store everything else on the appropriate scrap heap.
+ if (multipleScraps) {
+ scrapViews = mScrapViews[whichScrap];
+ }
+
+ lp.scrappedFromPosition = mFirstActivePosition + i;
+ removeDetachedView(victim, false);
+ scrapViews.add(victim);
+
+ if (hasListener) {
+ mRecyclerListener.onMovedToScrapHeap(victim);
+ }
+ }
+ }
+ }
+ pruneScrapViews();
+ }
+
+ /**
+ * At the end of a layout pass, all temp detached views should either be re-attached or
+ * completely detached. This method ensures that any remaining view in the scrap list is
+ * fully detached.
+ */
+ void fullyDetachScrapViews() {
+ final int viewTypeCount = mViewTypeCount;
+ final ArrayList<View>[] scrapViews = mScrapViews;
+ for (int i = 0; i < viewTypeCount; ++i) {
+ final ArrayList<View> scrapPile = scrapViews[i];
+ for (int j = scrapPile.size() - 1; j >= 0; j--) {
+ final View view = scrapPile.get(j);
+ if (view.isTemporarilyDetached()) {
+ removeDetachedView(view, false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Makes sure that the size of mScrapViews does not exceed the size of
+ * mActiveViews, which can happen if an adapter does not recycle its
+ * views. Removes cached transient state views that no longer have
+ * transient state.
+ */
+ 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();
+ while (size > maxViews) {
+ scrapPile.remove(--size);
+ }
+ }
+
+ final SparseArray<View> transViewsByPos = mTransientStateViews;
+ if (transViewsByPos != null) {
+ for (int i = 0; i < transViewsByPos.size(); i++) {
+ final View v = transViewsByPos.valueAt(i);
+ if (!v.hasTransientState()) {
+ removeDetachedView(v, false);
+ transViewsByPos.removeAt(i);
+ i--;
+ }
+ }
+ }
+
+ final LongSparseArray<View> transViewsById = mTransientStateViewsById;
+ if (transViewsById != null) {
+ for (int i = 0; i < transViewsById.size(); i++) {
+ final View v = transViewsById.valueAt(i);
+ if (!v.hasTransientState()) {
+ removeDetachedView(v, false);
+ transViewsById.removeAt(i);
+ i--;
+ }
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /**
+ * Updates the cache color hint of all known views.
+ *
+ * @param color The new cache color hint.
+ */
+ void setCacheColorHint(int color) {
+ if (mViewTypeCount == 1) {
+ final ArrayList<View> scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ scrap.get(i).setDrawingCacheBackgroundColor(color);
+ }
+ } else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList<View> scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ scrap.get(j).setDrawingCacheBackgroundColor(color);
+ }
+ }
+ }
+ // Just in case this is called during a layout pass
+ final View[] activeViews = mActiveViews;
+ final int count = activeViews.length;
+ for (int i = 0; i < count; ++i) {
+ final View victim = activeViews[i];
+ if (victim != null) {
+ victim.setDrawingCacheBackgroundColor(color);
+ }
+ }
+ }
+
+ private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
+ final int size = scrapViews.size();
+ if (size > 0) {
+ // See if we still have a view for this position or ID.
+ // Traverse backwards to find the most recently used scrap view
+ for (int i = size - 1; i >= 0; i--) {
+ final View view = scrapViews.get(i);
+ final AbsListView.LayoutParams params =
+ (AbsListView.LayoutParams) view.getLayoutParams();
+
+ if (mAdapterHasStableIds) {
+ final long id = mAdapter.getItemId(position);
+ if (id == params.itemId) {
+ return scrapViews.remove(i);
+ }
+ } else if (params.scrappedFromPosition == position) {
+ final View scrap = scrapViews.remove(i);
+ clearScrapForRebind(scrap);
+ return scrap;
+ }
+ }
+ final View scrap = scrapViews.remove(size - 1);
+ clearScrapForRebind(scrap);
+ return scrap;
+ } else {
+ return null;
+ }
+ }
+
+ private void clearScrap(final ArrayList<View> scrap) {
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
+ }
+ }
+
+ private void clearScrapForRebind(View view) {
+ view.clearAccessibilityFocus();
+ view.setAccessibilityDelegate(null);
+ }
+
+ private void removeDetachedView(View child, boolean animate) {
+ child.setAccessibilityDelegate(null);
+ AbsListView.this.removeDetachedView(child, animate);
+ }
+ }
+
+ /**
+ * Returns the height of the view for the specified position.
+ *
+ * @param position the item position
+ * @return view height in pixels
+ */
+ int getHeightForPosition(int position) {
+ final int firstVisiblePosition = getFirstVisiblePosition();
+ final int childCount = getChildCount();
+ final int index = position - firstVisiblePosition;
+ if (index >= 0 && index < childCount) {
+ // Position is on-screen, use existing view.
+ final View view = getChildAt(index);
+ return view.getHeight();
+ } else {
+ // Position is off-screen, obtain & recycle view.
+ final View view = obtainView(position, mIsScrap);
+ view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED);
+ final int height = view.getMeasuredHeight();
+ mRecycler.addScrapView(view, position);
+ return height;
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ requestLayout();
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("drawing:cacheColorHint", getCacheColorHint());
+ encoder.addProperty("list:fastScrollEnabled", isFastScrollEnabled());
+ encoder.addProperty("list:scrollingCacheEnabled", isScrollingCacheEnabled());
+ encoder.addProperty("list:smoothScrollbarEnabled", isSmoothScrollbarEnabled());
+ encoder.addProperty("list:stackFromBottom", isStackFromBottom());
+ encoder.addProperty("list:textFilterEnabled", isTextFilterEnabled());
+
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ encoder.addPropertyKey("selectedView");
+ selectedView.encode(encoder);
+ }
+ }
+
+ /**
+ * Abstract positon scroller used to handle smooth scrolling.
+ */
+ static abstract class AbsPositionScroller {
+ public abstract void start(int position);
+ public abstract void start(int position, int boundPosition);
+ public abstract void startWithOffset(int position, int offset);
+ public abstract void startWithOffset(int position, int offset, int duration);
+ public abstract void stop();
+ }
+
+ /**
+ * Default position scroller that simulates a fling.
+ */
+ class PositionScroller extends AbsPositionScroller implements Runnable {
+ private static final int SCROLL_DURATION = 200;
+
+ private static final int MOVE_DOWN_POS = 1;
+ private static final int MOVE_UP_POS = 2;
+ private static final int MOVE_DOWN_BOUND = 3;
+ private static final int MOVE_UP_BOUND = 4;
+ private static final int MOVE_OFFSET = 5;
+
+ private int mMode;
+ private int mTargetPos;
+ private int mBoundPos;
+ private int mLastSeenPos;
+ private int mScrollDuration;
+ private final int mExtraScroll;
+
+ private int mOffsetFromTop;
+
+ PositionScroller() {
+ mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
+ }
+
+ @Override
+ public void start(final int position) {
+ stop();
+
+ if (mDataChanged) {
+ // Wait until we're back in a stable state to try this.
+ mPositionScrollAfterLayout = new Runnable() {
+ @Override public void run() {
+ start(position);
+ }
+ };
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ // Can't scroll without children.
+ return;
+ }
+
+ final int firstPos = mFirstPosition;
+ final int lastPos = firstPos + childCount - 1;
+
+ int viewTravelCount;
+ int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
+ if (clampedPosition < firstPos) {
+ viewTravelCount = firstPos - clampedPosition + 1;
+ mMode = MOVE_UP_POS;
+ } else if (clampedPosition > lastPos) {
+ viewTravelCount = clampedPosition - lastPos + 1;
+ mMode = MOVE_DOWN_POS;
+ } else {
+ scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION);
+ return;
+ }
+
+ if (viewTravelCount > 0) {
+ mScrollDuration = SCROLL_DURATION / viewTravelCount;
+ } else {
+ mScrollDuration = SCROLL_DURATION;
+ }
+ mTargetPos = clampedPosition;
+ mBoundPos = INVALID_POSITION;
+ mLastSeenPos = INVALID_POSITION;
+
+ postOnAnimation(this);
+ }
+
+ @Override
+ public void start(final int position, final int boundPosition) {
+ stop();
+
+ if (boundPosition == INVALID_POSITION) {
+ start(position);
+ return;
+ }
+
+ if (mDataChanged) {
+ // Wait until we're back in a stable state to try this.
+ mPositionScrollAfterLayout = new Runnable() {
+ @Override public void run() {
+ start(position, boundPosition);
+ }
+ };
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ // Can't scroll without children.
+ return;
+ }
+
+ final int firstPos = mFirstPosition;
+ final int lastPos = firstPos + childCount - 1;
+
+ int viewTravelCount;
+ int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
+ if (clampedPosition < firstPos) {
+ final int boundPosFromLast = lastPos - boundPosition;
+ if (boundPosFromLast < 1) {
+ // Moving would shift our bound position off the screen. Abort.
+ return;
+ }
+
+ final int posTravel = firstPos - clampedPosition + 1;
+ final int boundTravel = boundPosFromLast - 1;
+ if (boundTravel < posTravel) {
+ viewTravelCount = boundTravel;
+ mMode = MOVE_UP_BOUND;
+ } else {
+ viewTravelCount = posTravel;
+ mMode = MOVE_UP_POS;
+ }
+ } else if (clampedPosition > lastPos) {
+ final int boundPosFromFirst = boundPosition - firstPos;
+ if (boundPosFromFirst < 1) {
+ // Moving would shift our bound position off the screen. Abort.
+ return;
+ }
+
+ final int posTravel = clampedPosition - lastPos + 1;
+ final int boundTravel = boundPosFromFirst - 1;
+ if (boundTravel < posTravel) {
+ viewTravelCount = boundTravel;
+ mMode = MOVE_DOWN_BOUND;
+ } else {
+ viewTravelCount = posTravel;
+ mMode = MOVE_DOWN_POS;
+ }
+ } else {
+ scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION);
+ return;
+ }
+
+ if (viewTravelCount > 0) {
+ mScrollDuration = SCROLL_DURATION / viewTravelCount;
+ } else {
+ mScrollDuration = SCROLL_DURATION;
+ }
+ mTargetPos = clampedPosition;
+ mBoundPos = boundPosition;
+ mLastSeenPos = INVALID_POSITION;
+
+ postOnAnimation(this);
+ }
+
+ @Override
+ public void startWithOffset(int position, int offset) {
+ startWithOffset(position, offset, SCROLL_DURATION);
+ }
+
+ @Override
+ public void startWithOffset(final int position, int offset, final int duration) {
+ stop();
+
+ if (mDataChanged) {
+ // Wait until we're back in a stable state to try this.
+ final int postOffset = offset;
+ mPositionScrollAfterLayout = new Runnable() {
+ @Override public void run() {
+ startWithOffset(position, postOffset, duration);
+ }
+ };
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ // Can't scroll without children.
+ return;
+ }
+
+ offset += getPaddingTop();
+
+ mTargetPos = Math.max(0, Math.min(getCount() - 1, position));
+ mOffsetFromTop = offset;
+ mBoundPos = INVALID_POSITION;
+ mLastSeenPos = INVALID_POSITION;
+ mMode = MOVE_OFFSET;
+
+ final int firstPos = mFirstPosition;
+ final int lastPos = firstPos + childCount - 1;
+
+ int viewTravelCount;
+ if (mTargetPos < firstPos) {
+ viewTravelCount = firstPos - mTargetPos;
+ } else if (mTargetPos > lastPos) {
+ viewTravelCount = mTargetPos - lastPos;
+ } else {
+ // On-screen, just scroll.
+ final int targetTop = getChildAt(mTargetPos - firstPos).getTop();
+ smoothScrollBy(targetTop - offset, duration, true, false);
+ return;
+ }
+
+ // Estimate how many screens we should travel
+ final float screenTravelCount = (float) viewTravelCount / childCount;
+ mScrollDuration = screenTravelCount < 1 ?
+ duration : (int) (duration / screenTravelCount);
+ mLastSeenPos = INVALID_POSITION;
+
+ postOnAnimation(this);
+ }
+
+ /**
+ * Scroll such that targetPos is in the visible padded region without scrolling
+ * boundPos out of view. Assumes targetPos is onscreen.
+ */
+ private void scrollToVisible(int targetPos, int boundPos, int duration) {
+ final int firstPos = mFirstPosition;
+ final int childCount = getChildCount();
+ final int lastPos = firstPos + childCount - 1;
+ final int paddedTop = mListPadding.top;
+ final int paddedBottom = getHeight() - mListPadding.bottom;
+
+ if (targetPos < firstPos || targetPos > lastPos) {
+ Log.w(TAG, "scrollToVisible called with targetPos " + targetPos +
+ " not visible [" + firstPos + ", " + lastPos + "]");
+ }
+ if (boundPos < firstPos || boundPos > lastPos) {
+ // boundPos doesn't matter, it's already offscreen.
+ boundPos = INVALID_POSITION;
+ }
+
+ final View targetChild = getChildAt(targetPos - firstPos);
+ final int targetTop = targetChild.getTop();
+ final int targetBottom = targetChild.getBottom();
+ int scrollBy = 0;
+
+ if (targetBottom > paddedBottom) {
+ scrollBy = targetBottom - paddedBottom;
+ }
+ if (targetTop < paddedTop) {
+ scrollBy = targetTop - paddedTop;
+ }
+
+ if (scrollBy == 0) {
+ return;
+ }
+
+ if (boundPos >= 0) {
+ final View boundChild = getChildAt(boundPos - firstPos);
+ final int boundTop = boundChild.getTop();
+ final int boundBottom = boundChild.getBottom();
+ final int absScroll = Math.abs(scrollBy);
+
+ if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) {
+ // Don't scroll the bound view off the bottom of the screen.
+ scrollBy = Math.max(0, boundBottom - paddedBottom);
+ } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) {
+ // Don't scroll the bound view off the top of the screen.
+ scrollBy = Math.min(0, boundTop - paddedTop);
+ }
+ }
+
+ smoothScrollBy(scrollBy, duration);
+ }
+
+ @Override
+ public void stop() {
+ removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ final int listHeight = getHeight();
+ final int firstPos = mFirstPosition;
+
+ switch (mMode) {
+ case MOVE_DOWN_POS: {
+ final int lastViewIndex = getChildCount() - 1;
+ final int lastPos = firstPos + lastViewIndex;
+
+ if (lastViewIndex < 0) {
+ return;
+ }
+
+ if (lastPos == mLastSeenPos) {
+ // No new views, let things keep going.
+ postOnAnimation(this);
+ return;
+ }
+
+ final View lastView = getChildAt(lastViewIndex);
+ final int lastViewHeight = lastView.getHeight();
+ final int lastViewTop = lastView.getTop();
+ final int lastViewPixelsShowing = listHeight - lastViewTop;
+ final int extraScroll = lastPos < mItemCount - 1 ?
+ Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom;
+
+ final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll;
+ smoothScrollBy(scrollBy, mScrollDuration, true, lastPos < mTargetPos);
+
+ mLastSeenPos = lastPos;
+ if (lastPos < mTargetPos) {
+ postOnAnimation(this);
+ }
+ break;
+ }
+
+ case MOVE_DOWN_BOUND: {
+ final int nextViewIndex = 1;
+ final int childCount = getChildCount();
+
+ if (firstPos == mBoundPos || childCount <= nextViewIndex
+ || firstPos + childCount >= mItemCount) {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ return;
+ }
+ final int nextPos = firstPos + nextViewIndex;
+
+ if (nextPos == mLastSeenPos) {
+ // No new views, let things keep going.
+ postOnAnimation(this);
+ return;
+ }
+
+ final View nextView = getChildAt(nextViewIndex);
+ final int nextViewHeight = nextView.getHeight();
+ final int nextViewTop = nextView.getTop();
+ final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll);
+ if (nextPos < mBoundPos) {
+ smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll),
+ mScrollDuration, true, true);
+
+ mLastSeenPos = nextPos;
+
+ postOnAnimation(this);
+ } else {
+ if (nextViewTop > extraScroll) {
+ smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true, false);
+ } else {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ }
+ break;
+ }
+
+ case MOVE_UP_POS: {
+ if (firstPos == mLastSeenPos) {
+ // No new views, let things keep going.
+ postOnAnimation(this);
+ return;
+ }
+
+ final View firstView = getChildAt(0);
+ if (firstView == null) {
+ return;
+ }
+ final int firstViewTop = firstView.getTop();
+ final int extraScroll = firstPos > 0 ?
+ Math.max(mExtraScroll, mListPadding.top) : mListPadding.top;
+
+ smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true,
+ firstPos > mTargetPos);
+
+ mLastSeenPos = firstPos;
+
+ if (firstPos > mTargetPos) {
+ postOnAnimation(this);
+ }
+ break;
+ }
+
+ case MOVE_UP_BOUND: {
+ final int lastViewIndex = getChildCount() - 2;
+ if (lastViewIndex < 0) {
+ return;
+ }
+ final int lastPos = firstPos + lastViewIndex;
+
+ if (lastPos == mLastSeenPos) {
+ // No new views, let things keep going.
+ postOnAnimation(this);
+ return;
+ }
+
+ final View lastView = getChildAt(lastViewIndex);
+ final int lastViewHeight = lastView.getHeight();
+ final int lastViewTop = lastView.getTop();
+ final int lastViewPixelsShowing = listHeight - lastViewTop;
+ final int extraScroll = Math.max(mListPadding.top, mExtraScroll);
+ mLastSeenPos = lastPos;
+ if (lastPos > mBoundPos) {
+ smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true,
+ true);
+ postOnAnimation(this);
+ } else {
+ final int bottom = listHeight - extraScroll;
+ final int lastViewBottom = lastViewTop + lastViewHeight;
+ if (bottom > lastViewBottom) {
+ smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true, false);
+ } else {
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ }
+ break;
+ }
+
+ case MOVE_OFFSET: {
+ if (mLastSeenPos == firstPos) {
+ // No new views, let things keep going.
+ postOnAnimation(this);
+ return;
+ }
+
+ mLastSeenPos = firstPos;
+
+ final int childCount = getChildCount();
+ final int position = mTargetPos;
+ final int lastPos = firstPos + childCount - 1;
+
+ // Account for the visible "portion" of the first / last child when we estimate
+ // how many screens we should travel to reach our target
+ final View firstChild = getChildAt(0);
+ final int firstChildHeight = firstChild.getHeight();
+ final View lastChild = getChildAt(childCount - 1);
+ final int lastChildHeight = lastChild.getHeight();
+ final float firstPositionVisiblePart = (firstChildHeight == 0.0f) ? 1.0f
+ : (float) (firstChildHeight + firstChild.getTop()) / firstChildHeight;
+ final float lastPositionVisiblePart = (lastChildHeight == 0.0f) ? 1.0f
+ : (float) (lastChildHeight + getHeight() - lastChild.getBottom())
+ / lastChildHeight;
+
+ float viewTravelCount = 0;
+ if (position < firstPos) {
+ viewTravelCount = firstPos - position + (1.0f - firstPositionVisiblePart) + 1;
+ } else if (position > lastPos) {
+ viewTravelCount = position - lastPos + (1.0f - lastPositionVisiblePart);
+ }
+
+ // Estimate how many screens we should travel
+ final float screenTravelCount = viewTravelCount / childCount;
+
+ final float modifier = Math.min(Math.abs(screenTravelCount), 1.f);
+ if (position < firstPos) {
+ final int distance = (int) (-getHeight() * modifier);
+ final int duration = (int) (mScrollDuration * modifier);
+ smoothScrollBy(distance, duration, true, true);
+ postOnAnimation(this);
+ } else if (position > lastPos) {
+ final int distance = (int) (getHeight() * modifier);
+ final int duration = (int) (mScrollDuration * modifier);
+ smoothScrollBy(distance, duration, true, true);
+ postOnAnimation(this);
+ } else {
+ // On-screen, just scroll.
+ final int targetTop = getChildAt(position - firstPos).getTop();
+ final int distance = targetTop - mOffsetFromTop;
+ final int duration = (int) (mScrollDuration *
+ ((float) Math.abs(distance) / getHeight()));
+ smoothScrollBy(distance, duration, true, false);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+}
diff --git a/android/widget/AbsSeekBar.java b/android/widget/AbsSeekBar.java
new file mode 100644
index 0000000..1d1fcc9
--- /dev/null
+++ b/android/widget/AbsSeekBar.java
@@ -0,0 +1,1016 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+
+/**
+ * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb.
+ */
+public abstract class AbsSeekBar extends ProgressBar {
+ private final Rect mTempRect = new Rect();
+
+ private Drawable mThumb;
+ private ColorStateList mThumbTintList = null;
+ private PorterDuff.Mode mThumbTintMode = null;
+ private boolean mHasThumbTint = false;
+ private boolean mHasThumbTintMode = false;
+
+ private Drawable mTickMark;
+ private ColorStateList mTickMarkTintList = null;
+ private PorterDuff.Mode mTickMarkTintMode = null;
+ private boolean mHasTickMarkTint = false;
+ private boolean mHasTickMarkTintMode = false;
+
+ private int mThumbOffset;
+ private boolean mSplitTrack;
+
+ /**
+ * 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;
+
+ /**
+ * On key presses (right or left), the amount to increment/decrement the
+ * progress.
+ */
+ private int mKeyProgressIncrement = 1;
+
+ private static final int NO_ALPHA = 0xFF;
+ private float mDisabledAlpha;
+
+ private int mScaledTouchSlop;
+ private float mTouchDownX;
+ private boolean mIsDragging;
+
+ public AbsSeekBar(Context context) {
+ super(context);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SeekBar, defStyleAttr, defStyleRes);
+
+ final Drawable thumb = a.getDrawable(R.styleable.SeekBar_thumb);
+ setThumb(thumb);
+
+ if (a.hasValue(R.styleable.SeekBar_thumbTintMode)) {
+ mThumbTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.SeekBar_thumbTintMode, -1), mThumbTintMode);
+ mHasThumbTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.SeekBar_thumbTint)) {
+ mThumbTintList = a.getColorStateList(R.styleable.SeekBar_thumbTint);
+ mHasThumbTint = true;
+ }
+
+ final Drawable tickMark = a.getDrawable(R.styleable.SeekBar_tickMark);
+ setTickMark(tickMark);
+
+ if (a.hasValue(R.styleable.SeekBar_tickMarkTintMode)) {
+ mTickMarkTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.SeekBar_tickMarkTintMode, -1), mTickMarkTintMode);
+ mHasTickMarkTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.SeekBar_tickMarkTint)) {
+ mTickMarkTintList = a.getColorStateList(R.styleable.SeekBar_tickMarkTint);
+ mHasTickMarkTint = true;
+ }
+
+ mSplitTrack = a.getBoolean(R.styleable.SeekBar_splitTrack, false);
+
+ // Guess thumb offset if thumb != null, but allow layout to override.
+ final int thumbOffset = a.getDimensionPixelOffset(
+ R.styleable.SeekBar_thumbOffset, getThumbOffset());
+ setThumbOffset(thumbOffset);
+
+ final boolean useDisabledAlpha = a.getBoolean(R.styleable.SeekBar_useDisabledAlpha, true);
+ a.recycle();
+
+ if (useDisabledAlpha) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Theme, 0, 0);
+ mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f);
+ ta.recycle();
+ } else {
+ mDisabledAlpha = 1.0f;
+ }
+
+ applyThumbTint();
+ applyTickMarkTint();
+
+ mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ /**
+ * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
+ * <p>
+ * If the thumb is a valid drawable (i.e. not null), half its width will be
+ * used as the new thumb offset (@see #setThumbOffset(int)).
+ *
+ * @param thumb Drawable representing the thumb
+ */
+ public void setThumb(Drawable thumb) {
+ final boolean needUpdate;
+ // This way, calling setThumb again with the same bitmap will result in
+ // it recalcuating mThumbOffset (if for example it the bounds of the
+ // drawable changed)
+ if (mThumb != null && thumb != mThumb) {
+ mThumb.setCallback(null);
+ needUpdate = true;
+ } else {
+ needUpdate = false;
+ }
+
+ if (thumb != null) {
+ thumb.setCallback(this);
+ if (canResolveLayoutDirection()) {
+ thumb.setLayoutDirection(getLayoutDirection());
+ }
+
+ // Assuming the thumb drawable is symmetric, set the thumb offset
+ // such that the thumb will hang halfway off either edge of the
+ // progress bar.
+ mThumbOffset = thumb.getIntrinsicWidth() / 2;
+
+ // If we're updating get the new states
+ if (needUpdate &&
+ (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
+ || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
+ requestLayout();
+ }
+ }
+
+ mThumb = thumb;
+
+ applyThumbTint();
+ invalidate();
+
+ if (needUpdate) {
+ updateThumbAndTrackPos(getWidth(), getHeight());
+ if (thumb != null && thumb.isStateful()) {
+ // Note that if the states are different this won't work.
+ // For now, let's consider that an app bug.
+ int[] state = getDrawableState();
+ thumb.setState(state);
+ }
+ }
+ }
+
+ /**
+ * Return the drawable used to represent the scroll thumb - the component that
+ * the user can drag back and forth indicating the current value by its position.
+ *
+ * @return The current thumb drawable
+ */
+ public Drawable getThumb() {
+ return mThumb;
+ }
+
+ /**
+ * Applies a tint to the thumb drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setThumb(Drawable)} will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#SeekBar_thumbTint
+ * @see #getThumbTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setThumbTintList(@Nullable ColorStateList tint) {
+ mThumbTintList = tint;
+ mHasThumbTint = true;
+
+ applyThumbTint();
+ }
+
+ /**
+ * Returns the tint applied to the thumb drawable, if specified.
+ *
+ * @return the tint applied to the thumb drawable
+ * @attr ref android.R.styleable#SeekBar_thumbTint
+ * @see #setThumbTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getThumbTintList() {
+ return mThumbTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The
+ * default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#SeekBar_thumbTintMode
+ * @see #getThumbTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mThumbTintMode = tintMode;
+ mHasThumbTintMode = true;
+
+ applyThumbTint();
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the thumb drawable,
+ * if specified.
+ *
+ * @return the blending mode used to apply the tint to the thumb drawable
+ * @attr ref android.R.styleable#SeekBar_thumbTintMode
+ * @see #setThumbTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getThumbTintMode() {
+ return mThumbTintMode;
+ }
+
+ private void applyThumbTint() {
+ if (mThumb != null && (mHasThumbTint || mHasThumbTintMode)) {
+ mThumb = mThumb.mutate();
+
+ if (mHasThumbTint) {
+ mThumb.setTintList(mThumbTintList);
+ }
+
+ if (mHasThumbTintMode) {
+ mThumb.setTintMode(mThumbTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mThumb.isStateful()) {
+ mThumb.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * @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();
+ }
+
+ /**
+ * Specifies whether the track should be split by the thumb. When true,
+ * the thumb's optical bounds will be clipped out of the track drawable,
+ * then the thumb will be drawn into the resulting gap.
+ *
+ * @param splitTrack Whether the track should be split by the thumb
+ */
+ public void setSplitTrack(boolean splitTrack) {
+ mSplitTrack = splitTrack;
+ invalidate();
+ }
+
+ /**
+ * Returns whether the track should be split by the thumb.
+ */
+ public boolean getSplitTrack() {
+ return mSplitTrack;
+ }
+
+ /**
+ * Sets the drawable displayed at each progress position, e.g. at each
+ * possible thumb position.
+ *
+ * @param tickMark the drawable to display at each progress position
+ */
+ public void setTickMark(Drawable tickMark) {
+ if (mTickMark != null) {
+ mTickMark.setCallback(null);
+ }
+
+ mTickMark = tickMark;
+
+ if (tickMark != null) {
+ tickMark.setCallback(this);
+ tickMark.setLayoutDirection(getLayoutDirection());
+ if (tickMark.isStateful()) {
+ tickMark.setState(getDrawableState());
+ }
+ applyTickMarkTint();
+ }
+
+ invalidate();
+ }
+
+ /**
+ * @return the drawable displayed at each progress position
+ */
+ public Drawable getTickMark() {
+ return mTickMark;
+ }
+
+ /**
+ * Applies a tint to the tick mark drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setTickMark(Drawable)} will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#SeekBar_tickMarkTint
+ * @see #getTickMarkTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setTickMarkTintList(@Nullable ColorStateList tint) {
+ mTickMarkTintList = tint;
+ mHasTickMarkTint = true;
+
+ applyTickMarkTint();
+ }
+
+ /**
+ * Returns the tint applied to the tick mark drawable, if specified.
+ *
+ * @return the tint applied to the tick mark drawable
+ * @attr ref android.R.styleable#SeekBar_tickMarkTint
+ * @see #setTickMarkTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getTickMarkTintList() {
+ return mTickMarkTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The
+ * default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
+ * @see #getTickMarkTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mTickMarkTintMode = tintMode;
+ mHasTickMarkTintMode = true;
+
+ applyTickMarkTint();
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the tick mark drawable,
+ * if specified.
+ *
+ * @return the blending mode used to apply the tint to the tick mark drawable
+ * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
+ * @see #setTickMarkTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getTickMarkTintMode() {
+ return mTickMarkTintMode;
+ }
+
+ private void applyTickMarkTint() {
+ if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) {
+ mTickMark = mTickMark.mutate();
+
+ if (mHasTickMarkTint) {
+ mTickMark.setTintList(mTickMarkTintList);
+ }
+
+ if (mHasTickMarkTintMode) {
+ mTickMark.setTintMode(mTickMarkTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mTickMark.isStateful()) {
+ mTickMark.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * Sets the amount of progress changed via the arrow keys.
+ *
+ * @param increment The amount to increment or decrement when the user
+ * presses the arrow keys.
+ */
+ public void setKeyProgressIncrement(int increment) {
+ mKeyProgressIncrement = increment < 0 ? -increment : increment;
+ }
+
+ /**
+ * Returns the amount of progress changed via the arrow keys.
+ * <p>
+ * By default, this will be a value that is derived from the progress range.
+ *
+ * @return The amount to increment or decrement when the user presses the
+ * arrow keys. This will be positive.
+ */
+ public int getKeyProgressIncrement() {
+ return mKeyProgressIncrement;
+ }
+
+ @Override
+ public synchronized void setMin(int min) {
+ super.setMin(min);
+ int range = getMax() - getMin();
+
+ if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
+
+ // It will take the user too long to change this via keys, change it
+ // to something more reasonable
+ setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
+ }
+ }
+
+ @Override
+ public synchronized void setMax(int max) {
+ super.setMax(max);
+ int range = getMax() - getMin();
+
+ if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
+ // It will take the user too long to change this via keys, change it
+ // to something more reasonable
+ setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return who == mThumb || who == mTickMark || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+
+ if (mThumb != null) {
+ mThumb.jumpToCurrentState();
+ }
+
+ if (mTickMark != null) {
+ mTickMark.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final Drawable progressDrawable = getProgressDrawable();
+ if (progressDrawable != null && mDisabledAlpha < 1.0f) {
+ progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+ }
+
+ final Drawable thumb = mThumb;
+ if (thumb != null && thumb.isStateful()
+ && thumb.setState(getDrawableState())) {
+ invalidateDrawable(thumb);
+ }
+
+ final Drawable tickMark = mTickMark;
+ if (tickMark != null && tickMark.isStateful()
+ && tickMark.setState(getDrawableState())) {
+ invalidateDrawable(tickMark);
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mThumb != null) {
+ mThumb.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ void onVisualProgressChanged(int id, float scale) {
+ super.onVisualProgressChanged(id, scale);
+
+ if (id == R.id.progress) {
+ final Drawable thumb = mThumb;
+ if (thumb != null) {
+ setThumbPos(getWidth(), 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) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ updateThumbAndTrackPos(w, h);
+ }
+
+ private void updateThumbAndTrackPos(int w, int h) {
+ final int paddedHeight = h - mPaddingTop - mPaddingBottom;
+ final Drawable track = getCurrentDrawable();
+ final Drawable thumb = mThumb;
+
+ // The max height does not incorporate padding, whereas the height
+ // parameter does.
+ final int trackHeight = Math.min(mMaxHeight, paddedHeight);
+ final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
+
+ // Apply offset to whichever item is taller.
+ final int trackOffset;
+ final int thumbOffset;
+ if (thumbHeight > trackHeight) {
+ final int offsetHeight = (paddedHeight - thumbHeight) / 2;
+ trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2;
+ thumbOffset = offsetHeight;
+ } else {
+ final int offsetHeight = (paddedHeight - trackHeight) / 2;
+ trackOffset = offsetHeight;
+ thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2;
+ }
+
+ if (track != null) {
+ final int trackWidth = w - mPaddingRight - mPaddingLeft;
+ track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight);
+ }
+
+ if (thumb != null) {
+ setThumbPos(w, thumb, getScale(), thumbOffset);
+ }
+ }
+
+ private float getScale() {
+ int min = getMin();
+ int max = getMax();
+ int range = max - min;
+ return range > 0 ? (getProgress() - min) / (float) range : 0;
+ }
+
+ /**
+ * Updates the thumb drawable bounds.
+ *
+ * @param w Width of the view, including padding
+ * @param thumb Drawable used for the thumb
+ * @param scale Current progress between 0 and 1
+ * @param offset Vertical offset for centering. If set to
+ * {@link Integer#MIN_VALUE}, the current offset will be used.
+ */
+ private void setThumbPos(int w, Drawable thumb, float scale, int offset) {
+ int available = w - mPaddingLeft - mPaddingRight;
+ final int thumbWidth = thumb.getIntrinsicWidth();
+ final int thumbHeight = thumb.getIntrinsicHeight();
+ available -= thumbWidth;
+
+ // The extra space for the thumb to move on the track
+ available += mThumbOffset * 2;
+
+ final int thumbPos = (int) (scale * available + 0.5f);
+
+ final int top, bottom;
+ if (offset == Integer.MIN_VALUE) {
+ final Rect oldBounds = thumb.getBounds();
+ top = oldBounds.top;
+ bottom = oldBounds.bottom;
+ } else {
+ top = offset;
+ bottom = offset + thumbHeight;
+ }
+
+ final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos;
+ final int right = left + thumbWidth;
+
+ final Drawable background = getBackground();
+ if (background != null) {
+ final int offsetX = mPaddingLeft - mThumbOffset;
+ final int offsetY = mPaddingTop;
+ background.setHotspotBounds(left + offsetX, top + offsetY,
+ right + offsetX, bottom + offsetY);
+ }
+
+ // Canvas will be translated, so 0,0 is where we start drawing
+ thumb.setBounds(left, top, right, bottom);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onResolveDrawables(int layoutDirection) {
+ super.onResolveDrawables(layoutDirection);
+
+ if (mThumb != null) {
+ mThumb.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ drawThumb(canvas);
+ }
+
+ @Override
+ void drawTrack(Canvas canvas) {
+ final Drawable thumbDrawable = mThumb;
+ if (thumbDrawable != null && mSplitTrack) {
+ final Insets insets = thumbDrawable.getOpticalInsets();
+ final Rect tempRect = mTempRect;
+ thumbDrawable.copyBounds(tempRect);
+ tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop);
+ tempRect.left += insets.left;
+ tempRect.right -= insets.right;
+
+ final int saveCount = canvas.save();
+ canvas.clipRect(tempRect, Op.DIFFERENCE);
+ super.drawTrack(canvas);
+ drawTickMarks(canvas);
+ canvas.restoreToCount(saveCount);
+ } else {
+ super.drawTrack(canvas);
+ drawTickMarks(canvas);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected void drawTickMarks(Canvas canvas) {
+ if (mTickMark != null) {
+ final int count = getMax() - getMin();
+ if (count > 1) {
+ final int w = mTickMark.getIntrinsicWidth();
+ final int h = mTickMark.getIntrinsicHeight();
+ final int halfW = w >= 0 ? w / 2 : 1;
+ final int halfH = h >= 0 ? h / 2 : 1;
+ mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
+
+ final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count;
+ final int saveCount = canvas.save();
+ canvas.translate(mPaddingLeft, getHeight() / 2);
+ for (int i = 0; i <= count; i++) {
+ mTickMark.draw(canvas);
+ canvas.translate(spacing, 0);
+ }
+ canvas.restoreToCount(saveCount);
+ }
+ }
+ }
+
+ /**
+ * Draw the thumb.
+ */
+ void drawThumb(Canvas canvas) {
+ if (mThumb != null) {
+ final int saveCount = 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.restoreToCount(saveCount);
+ }
+ }
+
+ @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(resolveSizeAndState(dw, widthMeasureSpec, 0),
+ resolveSizeAndState(dh, heightMeasureSpec, 0));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mIsUserSeekable || !isEnabled()) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (isInScrollingContainer()) {
+ mTouchDownX = event.getX();
+ } else {
+ startDrag(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mIsDragging) {
+ trackTouchEvent(event);
+ } else {
+ final float x = event.getX();
+ if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
+ startDrag(event);
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mIsDragging) {
+ trackTouchEvent(event);
+ onStopTrackingTouch();
+ setPressed(false);
+ } else {
+ // Touch up when we never crossed the touch slop threshold should
+ // be interpreted as a tap-seek to that location.
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ onStopTrackingTouch();
+ }
+ // ProgressBar doesn't know to repaint the thumb drawable
+ // in its inactive state when the touch stops (because the
+ // value has not apparently changed)
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsDragging) {
+ onStopTrackingTouch();
+ setPressed(false);
+ }
+ invalidate(); // see above explanation
+ break;
+ }
+ return true;
+ }
+
+ private void startDrag(MotionEvent event) {
+ setPressed(true);
+
+ if (mThumb != null) {
+ // This may be within the padding region.
+ invalidate(mThumb.getBounds());
+ }
+
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ attemptClaimDrag();
+ }
+
+ private void setHotspot(float x, float y) {
+ final Drawable bg = getBackground();
+ if (bg != null) {
+ bg.setHotspot(x, y);
+ }
+ }
+
+ private void trackTouchEvent(MotionEvent event) {
+ final int x = Math.round(event.getX());
+ final int y = Math.round(event.getY());
+ final int width = getWidth();
+ final int availableWidth = width - mPaddingLeft - mPaddingRight;
+
+ final float scale;
+ float progress = 0.0f;
+ if (isLayoutRtl() && mMirrorForRtl) {
+ if (x > width - mPaddingRight) {
+ scale = 0.0f;
+ } else if (x < mPaddingLeft) {
+ scale = 1.0f;
+ } else {
+ scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth;
+ progress = mTouchProgressOffset;
+ }
+ } else {
+ if (x < mPaddingLeft) {
+ scale = 0.0f;
+ } else if (x > width - mPaddingRight) {
+ scale = 1.0f;
+ } else {
+ scale = (x - mPaddingLeft) / (float) availableWidth;
+ progress = mTouchProgressOffset;
+ }
+ }
+
+ final int range = getMax() - getMin();
+ progress += scale * range;
+
+ setHotspot(x, y);
+ setProgressInternal(Math.round(progress), true, false);
+ }
+
+ /**
+ * Tries to claim the user's drag motion, and requests disallowing any
+ * ancestors from stealing events in the drag.
+ */
+ private void attemptClaimDrag() {
+ if (mParent != null) {
+ mParent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ /**
+ * This is called when the user has started touching this widget.
+ */
+ void onStartTrackingTouch() {
+ mIsDragging = true;
+ }
+
+ /**
+ * This is called when the user either releases his touch or the touch is
+ * canceled.
+ */
+ void onStopTrackingTouch() {
+ mIsDragging = false;
+ }
+
+ /**
+ * Called when the user changes the seekbar's progress by using a key event.
+ */
+ void onKeyChange() {
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (isEnabled()) {
+ int increment = mKeyProgressIncrement;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_MINUS:
+ increment = -increment;
+ // fallthrough
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_PLUS:
+ case KeyEvent.KEYCODE_EQUALS:
+ increment = isLayoutRtl() ? -increment : increment;
+
+ if (setProgressInternal(getProgress() + increment, true, true)) {
+ onKeyChange();
+ return true;
+ }
+ break;
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AbsSeekBar.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (isEnabled()) {
+ final int progress = getProgress();
+ if (progress > getMin()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ }
+ if (progress < getMax()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ if (!isEnabled()) {
+ return false;
+ }
+
+ switch (action) {
+ case R.id.accessibilityActionSetProgress: {
+ if (!canUserSetProgress()) {
+ return false;
+ }
+ if (arguments == null || !arguments.containsKey(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
+ return false;
+ }
+ float value = arguments.getFloat(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE);
+ return setProgressInternal((int) value, true, true);
+ }
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ if (!canUserSetProgress()) {
+ return false;
+ }
+ int range = getMax() - getMin();
+ int increment = Math.max(1, Math.round((float) range / 20));
+ if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
+ increment = -increment;
+ }
+
+ // Let progress bar handle clamping values.
+ if (setProgressInternal(getProgress() + increment, true, true)) {
+ onKeyChange();
+ return true;
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return whether user can change progress on the view
+ */
+ boolean canUserSetProgress() {
+ return !isIndeterminate() && isEnabled();
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ final Drawable thumb = mThumb;
+ if (thumb != null) {
+ setThumbPos(getWidth(), thumb, getScale(), 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();
+ }
+ }
+}
diff --git a/android/widget/AbsSpinner.java b/android/widget/AbsSpinner.java
new file mode 100644
index 0000000..816c949
--- /dev/null
+++ b/android/widget/AbsSpinner.java
@@ -0,0 +1,515 @@
+/*
+ * 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.database.DataSetObserver;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+/**
+ * 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> {
+ private static final String LOG_TAG = AbsSpinner.class.getSimpleName();
+
+ SpinnerAdapter mAdapter;
+
+ int mHeightMeasureSpec;
+ int mWidthMeasureSpec;
+
+ int mSelectionLeftPadding = 0;
+ int mSelectionTopPadding = 0;
+ int mSelectionRightPadding = 0;
+ int mSelectionBottomPadding = 0;
+ final Rect mSpinnerPadding = new Rect();
+
+ final 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // Spinner is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ initAbsSpinner();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes);
+
+ final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries);
+ if (entries != null) {
+ final 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 && selectedPosition < mAdapter.getCount()) {
+ // 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.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ if (view != null) {
+ // Put in recycler for re-measuring and/or layout
+ mRecycler.put(selectedPosition, view);
+
+ 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 = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
+ widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
+
+ 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.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ void recycleAllViews() {
+ final int childCount = getChildCount();
+ final AbsSpinner.RecycleBin recycleBin = mRecycler;
+ final int position = mFirstPosition;
+
+ // All views go in recycler
+ for (int i = 0; i < childCount; i++) {
+ View v = getChildAt(i);
+ int index = position + i;
+ recycleBin.put(index, v);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchRestoreInstanceState(container);
+ // Restores the selected position when Spinner gets restored,
+ // rather than wait until the next measure/layout pass to do it.
+ handleDataChanged();
+ }
+
+ 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}
+ */
+ 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 final 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;
+ }
+
+ 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();
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AbsSpinner.class.getName();
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!isEnabled()) return;
+
+ if (!value.isList()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ setSelection(value.getListValue());
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ return isEnabled() ? AutofillValue.forList(getSelectedItemPosition()) : null;
+ }
+}
diff --git a/android/widget/AbsoluteLayout.java b/android/widget/AbsoluteLayout.java
new file mode 100644
index 0000000..4ce0d5d
--- /dev/null
+++ b/android/widget/AbsoluteLayout.java
@@ -0,0 +1,228 @@
+/*
+ * 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>
+ *
+ * @deprecated Use {@link android.widget.FrameLayout}, {@link android.widget.RelativeLayout}
+ * or a custom layout instead.
+ */
+@Deprecated
+@RemoteView
+public class AbsoluteLayout extends ViewGroup {
+ public AbsoluteLayout(Context context) {
+ this(context, null);
+ }
+
+ public AbsoluteLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @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(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
+ resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
+ }
+
+ /**
+ * 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);
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ /**
+ * 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 #MATCH_PARENT},
+ {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #MATCH_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 from 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/android/widget/AccessibilityIterators.java b/android/widget/AccessibilityIterators.java
new file mode 100644
index 0000000..442ffa1
--- /dev/null
+++ b/android/widget/AccessibilityIterators.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2012 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.Rect;
+import android.text.Layout;
+import android.text.Spannable;
+import android.view.AccessibilityIterators.AbstractTextSegmentIterator;
+
+/**
+ * This class contains the implementation of text segment iterators
+ * for accessibility support.
+ */
+final class AccessibilityIterators {
+
+ static class LineTextSegmentIterator extends AbstractTextSegmentIterator {
+ private static LineTextSegmentIterator sLineInstance;
+
+ protected static final int DIRECTION_START = -1;
+ protected static final int DIRECTION_END = 1;
+
+ protected Layout mLayout;
+
+ public static LineTextSegmentIterator getInstance() {
+ if (sLineInstance == null) {
+ sLineInstance = new LineTextSegmentIterator();
+ }
+ return sLineInstance;
+ }
+
+ public void initialize(Spannable text, Layout layout) {
+ mText = text.toString();
+ mLayout = layout;
+ }
+
+ @Override
+ public int[] following(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset >= mText.length()) {
+ return null;
+ }
+ int nextLine;
+ if (offset < 0) {
+ nextLine = mLayout.getLineForOffset(0);
+ } else {
+ final int currentLine = mLayout.getLineForOffset(offset);
+ if (getLineEdgeIndex(currentLine, DIRECTION_START) == offset) {
+ nextLine = currentLine;
+ } else {
+ nextLine = currentLine + 1;
+ }
+ }
+ if (nextLine >= mLayout.getLineCount()) {
+ return null;
+ }
+ final int start = getLineEdgeIndex(nextLine, DIRECTION_START);
+ final int end = getLineEdgeIndex(nextLine, DIRECTION_END) + 1;
+ return getRange(start, end);
+ }
+
+ @Override
+ public int[] preceding(int offset) {
+ final int textLegth = mText.length();
+ if (textLegth <= 0) {
+ return null;
+ }
+ if (offset <= 0) {
+ return null;
+ }
+ int previousLine;
+ if (offset > mText.length()) {
+ previousLine = mLayout.getLineForOffset(mText.length());
+ } else {
+ final int currentLine = mLayout.getLineForOffset(offset);
+ if (getLineEdgeIndex(currentLine, DIRECTION_END) + 1 == offset) {
+ previousLine = currentLine;
+ } else {
+ previousLine = currentLine - 1;
+ }
+ }
+ if (previousLine < 0) {
+ return null;
+ }
+ final int start = getLineEdgeIndex(previousLine, DIRECTION_START);
+ final int end = getLineEdgeIndex(previousLine, DIRECTION_END) + 1;
+ return getRange(start, end);
+ }
+
+ protected int getLineEdgeIndex(int lineNumber, int direction) {
+ final int paragraphDirection = mLayout.getParagraphDirection(lineNumber);
+ if (direction * paragraphDirection < 0) {
+ return mLayout.getLineStart(lineNumber);
+ } else {
+ return mLayout.getLineEnd(lineNumber) - 1;
+ }
+ }
+ }
+
+ static class PageTextSegmentIterator extends LineTextSegmentIterator {
+ private static PageTextSegmentIterator sPageInstance;
+
+ private TextView mView;
+
+ private final Rect mTempRect = new Rect();
+
+ public static PageTextSegmentIterator getInstance() {
+ if (sPageInstance == null) {
+ sPageInstance = new PageTextSegmentIterator();
+ }
+ return sPageInstance;
+ }
+
+ public void initialize(TextView view) {
+ super.initialize((Spannable) view.getIterableTextForAccessibility(), view.getLayout());
+ mView = view;
+ }
+
+ @Override
+ public int[] following(int offset) {
+ final int textLength = mText.length();
+ if (textLength <= 0) {
+ return null;
+ }
+ if (offset >= mText.length()) {
+ return null;
+ }
+ if (!mView.getGlobalVisibleRect(mTempRect)) {
+ return null;
+ }
+
+ final int start = Math.max(0, offset);
+
+ final int currentLine = mLayout.getLineForOffset(start);
+ final int currentLineTop = mLayout.getLineTop(currentLine);
+ final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
+ - mView.getTotalPaddingBottom();
+ final int nextPageStartY = currentLineTop + pageHeight;
+ final int lastLineTop = mLayout.getLineTop(mLayout.getLineCount() - 1);
+ final int currentPageEndLine = (nextPageStartY < lastLineTop)
+ ? mLayout.getLineForVertical(nextPageStartY) - 1 : mLayout.getLineCount() - 1;
+
+ final int end = getLineEdgeIndex(currentPageEndLine, DIRECTION_END) + 1;
+
+ return getRange(start, end);
+ }
+
+ @Override
+ public int[] preceding(int offset) {
+ final int textLength = mText.length();
+ if (textLength <= 0) {
+ return null;
+ }
+ if (offset <= 0) {
+ return null;
+ }
+ if (!mView.getGlobalVisibleRect(mTempRect)) {
+ return null;
+ }
+
+ final int end = Math.min(mText.length(), offset);
+
+ final int currentLine = mLayout.getLineForOffset(end);
+ final int currentLineTop = mLayout.getLineTop(currentLine);
+ final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
+ - mView.getTotalPaddingBottom();
+ final int previousPageEndY = currentLineTop - pageHeight;
+ int currentPageStartLine = (previousPageEndY > 0) ?
+ mLayout.getLineForVertical(previousPageEndY) : 0;
+ // If we're at the end of text, we're at the end of the current line rather than the
+ // start of the next line, so we should move up one fewer lines than we would otherwise.
+ if (end == mText.length() && (currentPageStartLine < currentLine)) {
+ currentPageStartLine += 1;
+ }
+
+ final int start = getLineEdgeIndex(currentPageStartLine, DIRECTION_START);
+
+ return getRange(start, end);
+ }
+ }
+}
diff --git a/android/widget/ActionMenuPresenter.java b/android/widget/ActionMenuPresenter.java
new file mode 100644
index 0000000..46269c6
--- /dev/null
+++ b/android/widget/ActionMenuPresenter.java
@@ -0,0 +1,1070 @@
+/*
+ * Copyright (C) 2011 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.ActionProvider;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.view.ActionBarPolicy;
+import com.android.internal.view.menu.ActionMenuItemView;
+import com.android.internal.view.menu.BaseMenuPresenter;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.ShowableListMenu;
+import com.android.internal.view.menu.SubMenuBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MenuPresenter for building action menus as seen in the action bar and action modes.
+ *
+ * @hide
+ */
+public class ActionMenuPresenter extends BaseMenuPresenter
+ implements ActionProvider.SubUiVisibilityListener {
+ private static final int ITEM_ANIMATION_DURATION = 150;
+ private static final boolean ACTIONBAR_ANIMATIONS_ENABLED = false;
+
+ private OverflowMenuButton mOverflowButton;
+ private Drawable mPendingOverflowIcon;
+ private boolean mPendingOverflowIconSet;
+ private boolean mReserveOverflow;
+ private boolean mReserveOverflowSet;
+ private int mWidthLimit;
+ private int mActionItemWidthLimit;
+ private int mMaxItems;
+ private boolean mMaxItemsSet;
+ private boolean mStrictWidthLimit;
+ private boolean mWidthLimitSet;
+ private boolean mExpandedActionViewsExclusive;
+
+ private int mMinCellSize;
+
+ // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
+ private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
+
+ private OverflowPopup mOverflowPopup;
+ private ActionButtonSubmenu mActionButtonPopup;
+
+ private OpenOverflowRunnable mPostedOpenRunnable;
+ private ActionMenuPopupCallback mPopupCallback;
+
+ final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
+ int mOpenSubMenuId;
+
+ // These collections are used to store pre- and post-layout information for menu items,
+ // which is used to determine appropriate animations to run for changed items.
+ private SparseArray<MenuItemLayoutInfo> mPreLayoutItems = new SparseArray<>();
+ private SparseArray<MenuItemLayoutInfo> mPostLayoutItems = new SparseArray<>();
+
+ // The list of currently running animations on menu items.
+ private List<ItemAnimationInfo> mRunningItemAnimations = new ArrayList<>();
+ private ViewTreeObserver.OnPreDrawListener mItemAnimationPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ computeMenuItemAnimationInfo(false);
+ ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(this);
+ runItemAnimations();
+ return true;
+ }
+ };
+ private View.OnAttachStateChangeListener mAttachStateChangeListener =
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(
+ mItemAnimationPreDrawListener);
+ mPreLayoutItems.clear();
+ mPostLayoutItems.clear();
+ }
+ };
+
+
+ public ActionMenuPresenter(Context context) {
+ super(context, com.android.internal.R.layout.action_menu_layout,
+ com.android.internal.R.layout.action_menu_item_layout);
+ }
+
+ @Override
+ public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+ super.initForMenu(context, menu);
+
+ final Resources res = context.getResources();
+
+ final ActionBarPolicy abp = ActionBarPolicy.get(context);
+ if (!mReserveOverflowSet) {
+ mReserveOverflow = abp.showsOverflowMenuButton();
+ }
+
+ if (!mWidthLimitSet) {
+ mWidthLimit = abp.getEmbeddedMenuWidthLimit();
+ }
+
+ // Measure for initial configuration
+ if (!mMaxItemsSet) {
+ mMaxItems = abp.getMaxActionButtons();
+ }
+
+ int width = mWidthLimit;
+ if (mReserveOverflow) {
+ if (mOverflowButton == null) {
+ mOverflowButton = new OverflowMenuButton(mSystemContext);
+ if (mPendingOverflowIconSet) {
+ mOverflowButton.setImageDrawable(mPendingOverflowIcon);
+ mPendingOverflowIcon = null;
+ mPendingOverflowIconSet = false;
+ }
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ mOverflowButton.measure(spec, spec);
+ }
+ width -= mOverflowButton.getMeasuredWidth();
+ } else {
+ mOverflowButton = null;
+ }
+
+ mActionItemWidthLimit = width;
+
+ mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (!mMaxItemsSet) {
+ mMaxItems = ActionBarPolicy.get(mContext).getMaxActionButtons();
+ }
+ if (mMenu != null) {
+ mMenu.onItemsChanged(true);
+ }
+ }
+
+ public void setWidthLimit(int width, boolean strict) {
+ mWidthLimit = width;
+ mStrictWidthLimit = strict;
+ mWidthLimitSet = true;
+ }
+
+ public void setReserveOverflow(boolean reserveOverflow) {
+ mReserveOverflow = reserveOverflow;
+ mReserveOverflowSet = true;
+ }
+
+ public void setItemLimit(int itemCount) {
+ mMaxItems = itemCount;
+ mMaxItemsSet = true;
+ }
+
+ public void setExpandedActionViewsExclusive(boolean isExclusive) {
+ mExpandedActionViewsExclusive = isExclusive;
+ }
+
+ public void setOverflowIcon(Drawable icon) {
+ if (mOverflowButton != null) {
+ mOverflowButton.setImageDrawable(icon);
+ } else {
+ mPendingOverflowIconSet = true;
+ mPendingOverflowIcon = icon;
+ }
+ }
+
+ public Drawable getOverflowIcon() {
+ if (mOverflowButton != null) {
+ return mOverflowButton.getDrawable();
+ } else if (mPendingOverflowIconSet) {
+ return mPendingOverflowIcon;
+ }
+ return null;
+ }
+
+ @Override
+ public MenuView getMenuView(ViewGroup root) {
+ MenuView oldMenuView = mMenuView;
+ MenuView result = super.getMenuView(root);
+ if (oldMenuView != result) {
+ ((ActionMenuView) result).setPresenter(this);
+ if (oldMenuView != null) {
+ ((View) oldMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
+ }
+ ((View) result).addOnAttachStateChangeListener(mAttachStateChangeListener);
+ }
+ return result;
+ }
+
+ @Override
+ public View getItemView(final MenuItemImpl item, View convertView, ViewGroup parent) {
+ View actionView = item.getActionView();
+ if (actionView == null || item.hasCollapsibleActionView()) {
+ actionView = super.getItemView(item, convertView, parent);
+ }
+ actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
+
+ final ActionMenuView menuParent = (ActionMenuView) parent;
+ final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
+ if (!menuParent.checkLayoutParams(lp)) {
+ actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
+ }
+ return actionView;
+ }
+
+ @Override
+ public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
+ itemView.initialize(item, 0);
+
+ final ActionMenuView menuView = (ActionMenuView) mMenuView;
+ final ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
+ actionItemView.setItemInvoker(menuView);
+
+ if (mPopupCallback == null) {
+ mPopupCallback = new ActionMenuPopupCallback();
+ }
+ actionItemView.setPopupCallback(mPopupCallback);
+ }
+
+ @Override
+ public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
+ return item.isActionButton();
+ }
+
+ /**
+ * Store layout information about current items in the menu. This is stored for
+ * both pre- and post-layout phases and compared in runItemAnimations() to determine
+ * the animations that need to be run on any item changes.
+ *
+ * @param preLayout Whether this is being called in the pre-layout phase. This is passed
+ * into the MenuItemLayoutInfo structure to store the appropriate position values.
+ */
+ private void computeMenuItemAnimationInfo(boolean preLayout) {
+ final ViewGroup menuView = (ViewGroup) mMenuView;
+ final int count = menuView.getChildCount();
+ SparseArray items = preLayout ? mPreLayoutItems : mPostLayoutItems;
+ for (int i = 0; i < count; ++i) {
+ View child = menuView.getChildAt(i);
+ final int id = child.getId();
+ if (id > 0 && child.getWidth() != 0 && child.getHeight() != 0) {
+ MenuItemLayoutInfo info = new MenuItemLayoutInfo(child, preLayout);
+ items.put(id, info);
+ }
+ }
+ }
+
+ /**
+ * This method is called once both the pre-layout and post-layout steps have
+ * happened. It figures out which views are new (didn't exist prior to layout),
+ * gone (existed pre-layout, but are now gone), or changed (exist in both,
+ * but in a different location) and runs appropriate animations on those views.
+ * Items are tracked by ids, since the underlying views that represent items
+ * pre- and post-layout may be different.
+ */
+ private void runItemAnimations() {
+ for (int i = 0; i < mPreLayoutItems.size(); ++i) {
+ int id = mPreLayoutItems.keyAt(i);
+ final MenuItemLayoutInfo menuItemLayoutInfoPre = mPreLayoutItems.get(id);
+ final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
+ if (postLayoutIndex >= 0) {
+ // item exists pre and post: see if it's changed
+ final MenuItemLayoutInfo menuItemLayoutInfoPost =
+ mPostLayoutItems.valueAt(postLayoutIndex);
+ PropertyValuesHolder pvhX = null;
+ PropertyValuesHolder pvhY = null;
+ if (menuItemLayoutInfoPre.left != menuItemLayoutInfoPost.left) {
+ pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
+ (menuItemLayoutInfoPre.left - menuItemLayoutInfoPost.left), 0);
+ }
+ if (menuItemLayoutInfoPre.top != menuItemLayoutInfoPost.top) {
+ pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+ menuItemLayoutInfoPre.top - menuItemLayoutInfoPost.top, 0);
+ }
+ if (pvhX != null || pvhY != null) {
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+ if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.MOVE) {
+ oldInfo.animator.cancel();
+ }
+ }
+ ObjectAnimator anim;
+ if (pvhX != null) {
+ if (pvhY != null) {
+ anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view,
+ pvhX, pvhY);
+ } else {
+ anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhX);
+ }
+ } else {
+ anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhY);
+ }
+ anim.setDuration(ITEM_ANIMATION_DURATION);
+ anim.start();
+ ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPost, anim,
+ ItemAnimationInfo.MOVE);
+ mRunningItemAnimations.add(info);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ if (mRunningItemAnimations.get(j).animator == animation) {
+ mRunningItemAnimations.remove(j);
+ break;
+ }
+ }
+ }
+ });
+ }
+ mPostLayoutItems.remove(id);
+ } else {
+ // item used to be there, is now gone
+ float oldAlpha = 1;
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+ if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_IN) {
+ oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
+ oldInfo.animator.cancel();
+ }
+ }
+ ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfoPre.view, View.ALPHA,
+ oldAlpha, 0);
+ // Re-using the view from pre-layout assumes no view recycling
+ ((ViewGroup) mMenuView).getOverlay().add(menuItemLayoutInfoPre.view);
+ anim.setDuration(ITEM_ANIMATION_DURATION);
+ anim.start();
+ ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPre, anim, ItemAnimationInfo.FADE_OUT);
+ mRunningItemAnimations.add(info);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ if (mRunningItemAnimations.get(j).animator == animation) {
+ mRunningItemAnimations.remove(j);
+ break;
+ }
+ }
+ ((ViewGroup) mMenuView).getOverlay().remove(menuItemLayoutInfoPre.view);
+ }
+ });
+ }
+ }
+ for (int i = 0; i < mPostLayoutItems.size(); ++i) {
+ int id = mPostLayoutItems.keyAt(i);
+ final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
+ if (postLayoutIndex >= 0) {
+ // item is new
+ final MenuItemLayoutInfo menuItemLayoutInfo =
+ mPostLayoutItems.valueAt(postLayoutIndex);
+ float oldAlpha = 0;
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+ if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_OUT) {
+ oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
+ oldInfo.animator.cancel();
+ }
+ }
+ ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfo.view, View.ALPHA,
+ oldAlpha, 1);
+ anim.start();
+ anim.setDuration(ITEM_ANIMATION_DURATION);
+ ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfo, anim, ItemAnimationInfo.FADE_IN);
+ mRunningItemAnimations.add(info);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+ if (mRunningItemAnimations.get(j).animator == animation) {
+ mRunningItemAnimations.remove(j);
+ break;
+ }
+ }
+ }
+ });
+ }
+ }
+ mPreLayoutItems.clear();
+ mPostLayoutItems.clear();
+ }
+
+ /**
+ * Gets position/existence information on menu items before and after layout,
+ * which is then fed into runItemAnimations()
+ */
+ private void setupItemAnimations() {
+ computeMenuItemAnimationInfo(true);
+ ((View) mMenuView).getViewTreeObserver().
+ addOnPreDrawListener(mItemAnimationPreDrawListener);
+ }
+
+ @Override
+ public void updateMenuView(boolean cleared) {
+ final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent();
+ if (menuViewParent != null && ACTIONBAR_ANIMATIONS_ENABLED) {
+ setupItemAnimations();
+ }
+ super.updateMenuView(cleared);
+
+ ((View) mMenuView).requestLayout();
+
+ if (mMenu != null) {
+ final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems();
+ final int count = actionItems.size();
+ for (int i = 0; i < count; i++) {
+ final ActionProvider provider = actionItems.get(i).getActionProvider();
+ if (provider != null) {
+ provider.setSubUiVisibilityListener(this);
+ }
+ }
+ }
+
+ final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ?
+ mMenu.getNonActionItems() : null;
+
+ boolean hasOverflow = false;
+ if (mReserveOverflow && nonActionItems != null) {
+ final int count = nonActionItems.size();
+ if (count == 1) {
+ hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
+ } else {
+ hasOverflow = count > 0;
+ }
+ }
+
+ if (hasOverflow) {
+ if (mOverflowButton == null) {
+ mOverflowButton = new OverflowMenuButton(mSystemContext);
+ }
+ ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
+ if (parent != mMenuView) {
+ if (parent != null) {
+ parent.removeView(mOverflowButton);
+ }
+ ActionMenuView menuView = (ActionMenuView) mMenuView;
+ menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
+ }
+ } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
+ ((ViewGroup) mMenuView).removeView(mOverflowButton);
+ }
+
+ ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
+ }
+
+ @Override
+ public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
+ if (parent.getChildAt(childIndex) == mOverflowButton) return false;
+ return super.filterLeftoverView(parent, childIndex);
+ }
+
+ public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+ if (!subMenu.hasVisibleItems()) return false;
+
+ SubMenuBuilder topSubMenu = subMenu;
+ while (topSubMenu.getParentMenu() != mMenu) {
+ topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
+ }
+ View anchor = findViewForItem(topSubMenu.getItem());
+ if (anchor == null) {
+ // This means the submenu was opened from an overflow menu item, indicating the
+ // MenuPopupHelper will handle opening the submenu via its MenuPopup. Return false to
+ // ensure that the MenuPopup acts as presenter for the submenu, and acts on its
+ // responsibility to display the new submenu.
+ return false;
+ }
+
+ mOpenSubMenuId = subMenu.getItem().getItemId();
+
+ boolean preserveIconSpacing = false;
+ final int count = subMenu.size();
+ for (int i = 0; i < count; i++) {
+ MenuItem childItem = subMenu.getItem(i);
+ if (childItem.isVisible() && childItem.getIcon() != null) {
+ preserveIconSpacing = true;
+ break;
+ }
+ }
+
+ mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu, anchor);
+ mActionButtonPopup.setForceShowIcon(preserveIconSpacing);
+ mActionButtonPopup.show();
+
+ super.onSubMenuSelected(subMenu);
+ return true;
+ }
+
+ private View findViewForItem(MenuItem item) {
+ final ViewGroup parent = (ViewGroup) mMenuView;
+ if (parent == null) return null;
+
+ final int count = parent.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = parent.getChildAt(i);
+ if (child instanceof MenuView.ItemView &&
+ ((MenuView.ItemView) child).getItemData() == item) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Display the overflow menu if one is present.
+ * @return true if the overflow menu was shown, false otherwise.
+ */
+ public boolean showOverflowMenu() {
+ if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null &&
+ mPostedOpenRunnable == null && !mMenu.getNonActionItems().isEmpty()) {
+ OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
+ mPostedOpenRunnable = new OpenOverflowRunnable(popup);
+ // Post this for later; we might still need a layout for the anchor to be right.
+ ((View) mMenuView).post(mPostedOpenRunnable);
+
+ // ActionMenuPresenter uses null as a callback argument here
+ // to indicate overflow is opening.
+ super.onSubMenuSelected(null);
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Hide the overflow menu if it is currently showing.
+ *
+ * @return true if the overflow menu was hidden, false otherwise.
+ */
+ public boolean hideOverflowMenu() {
+ if (mPostedOpenRunnable != null && mMenuView != null) {
+ ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
+ mPostedOpenRunnable = null;
+ return true;
+ }
+
+ MenuPopupHelper popup = mOverflowPopup;
+ if (popup != null) {
+ popup.dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Dismiss all popup menus - overflow and submenus.
+ * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
+ */
+ public boolean dismissPopupMenus() {
+ boolean result = hideOverflowMenu();
+ result |= hideSubMenus();
+ return result;
+ }
+
+ /**
+ * Dismiss all submenu popups.
+ *
+ * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
+ */
+ public boolean hideSubMenus() {
+ if (mActionButtonPopup != null) {
+ mActionButtonPopup.dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return true if the overflow menu is currently showing
+ */
+ public boolean isOverflowMenuShowing() {
+ return mOverflowPopup != null && mOverflowPopup.isShowing();
+ }
+
+ public boolean isOverflowMenuShowPending() {
+ return mPostedOpenRunnable != null || isOverflowMenuShowing();
+ }
+
+ /**
+ * @return true if space has been reserved in the action menu for an overflow item.
+ */
+ public boolean isOverflowReserved() {
+ return mReserveOverflow;
+ }
+
+ public boolean flagActionItems() {
+ final ArrayList<MenuItemImpl> visibleItems;
+ final int itemsSize;
+ if (mMenu != null) {
+ visibleItems = mMenu.getVisibleItems();
+ itemsSize = visibleItems.size();
+ } else {
+ visibleItems = null;
+ itemsSize = 0;
+ }
+
+ int maxActions = mMaxItems;
+ int widthLimit = mActionItemWidthLimit;
+ final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final ViewGroup parent = (ViewGroup) mMenuView;
+
+ int requiredItems = 0;
+ int requestedItems = 0;
+ int firstActionWidth = 0;
+ boolean hasOverflow = false;
+ for (int i = 0; i < itemsSize; i++) {
+ MenuItemImpl item = visibleItems.get(i);
+ if (item.requiresActionButton()) {
+ requiredItems++;
+ } else if (item.requestsActionButton()) {
+ requestedItems++;
+ } else {
+ hasOverflow = true;
+ }
+ if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
+ // Overflow everything if we have an expanded action view and we're
+ // space constrained.
+ maxActions = 0;
+ }
+ }
+
+ // Reserve a spot for the overflow item if needed.
+ if (mReserveOverflow &&
+ (hasOverflow || requiredItems + requestedItems > maxActions)) {
+ maxActions--;
+ }
+ maxActions -= requiredItems;
+
+ final SparseBooleanArray seenGroups = mActionButtonGroups;
+ seenGroups.clear();
+
+ int cellSize = 0;
+ int cellsRemaining = 0;
+ if (mStrictWidthLimit) {
+ cellsRemaining = widthLimit / mMinCellSize;
+ final int cellSizeRemaining = widthLimit % mMinCellSize;
+ cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
+ }
+
+ // Flag as many more requested items as will fit.
+ for (int i = 0; i < itemsSize; i++) {
+ MenuItemImpl item = visibleItems.get(i);
+
+ if (item.requiresActionButton()) {
+ View v = getItemView(item, null, parent);
+ if (mStrictWidthLimit) {
+ cellsRemaining -= ActionMenuView.measureChildForCells(v,
+ cellSize, cellsRemaining, querySpec, 0);
+ } else {
+ v.measure(querySpec, querySpec);
+ }
+ final int measuredWidth = v.getMeasuredWidth();
+ widthLimit -= measuredWidth;
+ if (firstActionWidth == 0) {
+ firstActionWidth = measuredWidth;
+ }
+ final int groupId = item.getGroupId();
+ if (groupId != 0) {
+ seenGroups.put(groupId, true);
+ }
+ item.setIsActionButton(true);
+ } else if (item.requestsActionButton()) {
+ // Items in a group with other items that already have an action slot
+ // can break the max actions rule, but not the width limit.
+ final int groupId = item.getGroupId();
+ final boolean inGroup = seenGroups.get(groupId);
+ boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 &&
+ (!mStrictWidthLimit || cellsRemaining > 0);
+
+ if (isAction) {
+ View v = getItemView(item, null, parent);
+ if (mStrictWidthLimit) {
+ final int cells = ActionMenuView.measureChildForCells(v,
+ cellSize, cellsRemaining, querySpec, 0);
+ cellsRemaining -= cells;
+ if (cells == 0) {
+ isAction = false;
+ }
+ } else {
+ v.measure(querySpec, querySpec);
+ }
+ final int measuredWidth = v.getMeasuredWidth();
+ widthLimit -= measuredWidth;
+ if (firstActionWidth == 0) {
+ firstActionWidth = measuredWidth;
+ }
+
+ if (mStrictWidthLimit) {
+ isAction &= widthLimit >= 0;
+ } else {
+ // Did this push the entire first item past the limit?
+ isAction &= widthLimit + firstActionWidth > 0;
+ }
+ }
+
+ if (isAction && groupId != 0) {
+ seenGroups.put(groupId, true);
+ } else if (inGroup) {
+ // We broke the width limit. Demote the whole group, they all overflow now.
+ seenGroups.put(groupId, false);
+ for (int j = 0; j < i; j++) {
+ MenuItemImpl areYouMyGroupie = visibleItems.get(j);
+ if (areYouMyGroupie.getGroupId() == groupId) {
+ // Give back the action slot
+ if (areYouMyGroupie.isActionButton()) maxActions++;
+ areYouMyGroupie.setIsActionButton(false);
+ }
+ }
+ }
+
+ if (isAction) maxActions--;
+
+ item.setIsActionButton(isAction);
+ } else {
+ // Neither requires nor requests an action button.
+ item.setIsActionButton(false);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ dismissPopupMenus();
+ super.onCloseMenu(menu, allMenusAreClosing);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ SavedState state = new SavedState();
+ state.openSubMenuId = mOpenSubMenuId;
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState saved = (SavedState) state;
+ if (saved.openSubMenuId > 0) {
+ MenuItem item = mMenu.findItem(saved.openSubMenuId);
+ if (item != null) {
+ SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
+ onSubMenuSelected(subMenu);
+ }
+ }
+ }
+
+ @Override
+ public void onSubUiVisibilityChanged(boolean isVisible) {
+ if (isVisible) {
+ // Not a submenu, but treat it like one.
+ super.onSubMenuSelected(null);
+ } else if (mMenu != null) {
+ mMenu.close(false /* closeAllMenus */);
+ }
+ }
+
+ public void setMenuView(ActionMenuView menuView) {
+ if (menuView != mMenuView) {
+ if (mMenuView != null) {
+ ((View) mMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
+ }
+ mMenuView = menuView;
+ menuView.initialize(mMenu);
+ menuView.addOnAttachStateChangeListener(mAttachStateChangeListener);
+ }
+ }
+
+ private static class SavedState implements Parcelable {
+ public int openSubMenuId;
+
+ SavedState() {
+ }
+
+ SavedState(Parcel in) {
+ openSubMenuId = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(openSubMenuId);
+ }
+
+ 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 class OverflowMenuButton extends ImageButton implements ActionMenuView.ActionMenuChildView {
+ public OverflowMenuButton(Context context) {
+ super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
+
+ setClickable(true);
+ setFocusable(true);
+ setVisibility(VISIBLE);
+ setEnabled(true);
+
+ setOnTouchListener(new ForwardingListener(this) {
+ @Override
+ public ShowableListMenu getPopup() {
+ if (mOverflowPopup == null) {
+ return null;
+ }
+
+ return mOverflowPopup.getPopup();
+ }
+
+ @Override
+ public boolean onForwardingStarted() {
+ showOverflowMenu();
+ return true;
+ }
+
+ @Override
+ public boolean onForwardingStopped() {
+ // Displaying the popup occurs asynchronously, so wait for
+ // the runnable to finish before deciding whether to stop
+ // forwarding.
+ if (mPostedOpenRunnable != null) {
+ return false;
+ }
+
+ hideOverflowMenu();
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public boolean performClick() {
+ if (super.performClick()) {
+ return true;
+ }
+
+ playSoundEffect(SoundEffectConstants.CLICK);
+ showOverflowMenu();
+ return true;
+ }
+
+ @Override
+ public boolean needsDividerBefore() {
+ return false;
+ }
+
+ @Override
+ public boolean needsDividerAfter() {
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setCanOpenPopup(true);
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ final boolean changed = super.setFrame(l, t, r, b);
+
+ // Set up the hotspot bounds to square and centered on the image.
+ final Drawable d = getDrawable();
+ final Drawable bg = getBackground();
+ if (d != null && bg != null) {
+ final int width = getWidth();
+ final int height = getHeight();
+ final int halfEdge = Math.max(width, height) / 2;
+ final int offsetX = getPaddingLeft() - getPaddingRight();
+ final int offsetY = getPaddingTop() - getPaddingBottom();
+ final int centerX = (width + offsetX) / 2;
+ final int centerY = (height + offsetY) / 2;
+ bg.setHotspotBounds(centerX - halfEdge, centerY - halfEdge,
+ centerX + halfEdge, centerY + halfEdge);
+ }
+
+ return changed;
+ }
+ }
+
+ private class OverflowPopup extends MenuPopupHelper {
+ public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
+ boolean overflowOnly) {
+ super(context, menu, anchorView, overflowOnly,
+ com.android.internal.R.attr.actionOverflowMenuStyle);
+ setGravity(Gravity.END);
+ setPresenterCallback(mPopupPresenterCallback);
+ }
+
+ @Override
+ protected void onDismiss() {
+ if (mMenu != null) {
+ mMenu.close();
+ }
+ mOverflowPopup = null;
+
+ super.onDismiss();
+ }
+ }
+
+ private class ActionButtonSubmenu extends MenuPopupHelper {
+ public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu, View anchorView) {
+ super(context, subMenu, anchorView, false,
+ com.android.internal.R.attr.actionOverflowMenuStyle);
+
+ MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
+ if (!item.isActionButton()) {
+ // Give a reasonable anchor to nested submenus.
+ setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
+ }
+
+ setPresenterCallback(mPopupPresenterCallback);
+ }
+
+ @Override
+ protected void onDismiss() {
+ mActionButtonPopup = null;
+ mOpenSubMenuId = 0;
+
+ super.onDismiss();
+ }
+ }
+
+ private class PopupPresenterCallback implements Callback {
+
+ @Override
+ public boolean onOpenSubMenu(MenuBuilder subMenu) {
+ if (subMenu == null) return false;
+
+ mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
+ final Callback cb = getCallback();
+ return cb != null ? cb.onOpenSubMenu(subMenu) : false;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ if (menu instanceof SubMenuBuilder) {
+ menu.getRootMenu().close(false /* closeAllMenus */);
+ }
+ final Callback cb = getCallback();
+ if (cb != null) {
+ cb.onCloseMenu(menu, allMenusAreClosing);
+ }
+ }
+ }
+
+ private class OpenOverflowRunnable implements Runnable {
+ private OverflowPopup mPopup;
+
+ public OpenOverflowRunnable(OverflowPopup popup) {
+ mPopup = popup;
+ }
+
+ public void run() {
+ if (mMenu != null) {
+ mMenu.changeMenuMode();
+ }
+ final View menuView = (View) mMenuView;
+ if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
+ mOverflowPopup = mPopup;
+ }
+ mPostedOpenRunnable = null;
+ }
+ }
+
+ private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback {
+ @Override
+ public ShowableListMenu getPopup() {
+ return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null;
+ }
+ }
+
+ /**
+ * This class holds layout information for a menu item. This is used to determine
+ * pre- and post-layout information about menu items, which will then be used to
+ * determine appropriate item animations.
+ */
+ private static class MenuItemLayoutInfo {
+ View view;
+ int left;
+ int top;
+
+ MenuItemLayoutInfo(View view, boolean preLayout) {
+ left = view.getLeft();
+ top = view.getTop();
+ if (preLayout) {
+ // We track translation for pre-layout because a view might be mid-animation
+ // and we need this information to know where to animate from
+ left += view.getTranslationX();
+ top += view.getTranslationY();
+ }
+ this.view = view;
+ }
+ }
+
+ /**
+ * This class is used to store information about currently-running item animations.
+ * This is used when new animations are scheduled to determine whether any existing
+ * animations need to be canceled, based on whether the running animations overlap
+ * with any new animations. For example, if an item is currently animating from
+ * location A to B and another change dictates that it be animated to C, then the current
+ * A-B animation will be canceled and a new animation to C will be started.
+ */
+ private static class ItemAnimationInfo {
+ int id;
+ MenuItemLayoutInfo menuItemLayoutInfo;
+ Animator animator;
+ int animType;
+ static final int MOVE = 0;
+ static final int FADE_IN = 1;
+ static final int FADE_OUT = 2;
+
+ ItemAnimationInfo(int id, MenuItemLayoutInfo info, Animator anim, int animType) {
+ this.id = id;
+ menuItemLayoutInfo = info;
+ animator = anim;
+ this.animType = animType;
+ }
+ }
+}
diff --git a/android/widget/ActionMenuView.java b/android/widget/ActionMenuView.java
new file mode 100644
index 0000000..c4bbdb0
--- /dev/null
+++ b/android/widget/ActionMenuView.java
@@ -0,0 +1,850 @@
+/*
+ * Copyright (C) 2010 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.view.menu.ActionMenuItemView;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+
+/**
+ * ActionMenuView is a presentation of a series of menu options as a View. It provides
+ * several top level options as action buttons while spilling remaining options over as
+ * items in an overflow menu. This allows applications to present packs of actions inline with
+ * specific or repeating content.
+ */
+public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
+ private static final String TAG = "ActionMenuView";
+
+ static final int MIN_CELL_SIZE = 56; // dips
+ static final int GENERATED_ITEM_PADDING = 4; // dips
+
+ private MenuBuilder mMenu;
+
+ /** Context against which to inflate popup menus. */
+ private Context mPopupContext;
+
+ /** Theme resource against which to inflate popup menus. */
+ private int mPopupTheme;
+
+ private boolean mReserveOverflow;
+ private ActionMenuPresenter mPresenter;
+ private MenuPresenter.Callback mActionMenuPresenterCallback;
+ private MenuBuilder.Callback mMenuBuilderCallback;
+ private boolean mFormatItems;
+ private int mFormatItemsWidth;
+ private int mMinCellSize;
+ private int mGeneratedItemPadding;
+
+ private OnMenuItemClickListener mOnMenuItemClickListener;
+
+ public ActionMenuView(Context context) {
+ this(context, null);
+ }
+
+ public ActionMenuView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setBaselineAligned(false);
+ final float density = context.getResources().getDisplayMetrics().density;
+ mMinCellSize = (int) (MIN_CELL_SIZE * density);
+ mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density);
+ mPopupContext = context;
+ mPopupTheme = 0;
+ }
+
+ /**
+ * Specifies the theme to use when inflating popup menus. By default, uses
+ * the same theme as the action menu view itself.
+ *
+ * @param resId theme used to inflate popup menus
+ * @see #getPopupTheme()
+ */
+ public void setPopupTheme(@StyleRes int resId) {
+ if (mPopupTheme != resId) {
+ mPopupTheme = resId;
+ if (resId == 0) {
+ mPopupContext = mContext;
+ } else {
+ mPopupContext = new ContextThemeWrapper(mContext, resId);
+ }
+ }
+ }
+
+ /**
+ * @return resource identifier of the theme used to inflate popup menus, or
+ * 0 if menus are inflated against the action menu view theme
+ * @see #setPopupTheme(int)
+ */
+ public int getPopupTheme() {
+ return mPopupTheme;
+ }
+
+ /**
+ * @param presenter Menu presenter used to display popup menu
+ * @hide
+ */
+ public void setPresenter(ActionMenuPresenter presenter) {
+ mPresenter = presenter;
+ mPresenter.setMenuView(this);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ if (mPresenter != null) {
+ mPresenter.updateMenuView(false);
+
+ if (mPresenter.isOverflowMenuShowing()) {
+ mPresenter.hideOverflowMenu();
+ mPresenter.showOverflowMenu();
+ }
+ }
+ }
+
+ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+ mOnMenuItemClickListener = listener;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // If we've been given an exact size to match, apply special formatting during layout.
+ final boolean wasFormatted = mFormatItems;
+ mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
+
+ if (wasFormatted != mFormatItems) {
+ mFormatItemsWidth = 0; // Reset this when switching modes
+ }
+
+ // Special formatting can change whether items can fit as action buttons.
+ // Kick the menu and update presenters when this changes.
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) {
+ mFormatItemsWidth = widthSize;
+ mMenu.onItemsChanged(true);
+ }
+
+ final int childCount = getChildCount();
+ if (mFormatItems && childCount > 0) {
+ onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec);
+ } else {
+ // Previous measurement at exact format may have set margins - reset them.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.leftMargin = lp.rightMargin = 0;
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) {
+ // We already know the width mode is EXACTLY if we're here.
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ final int widthPadding = getPaddingLeft() + getPaddingRight();
+ final int heightPadding = getPaddingTop() + getPaddingBottom();
+
+ final int itemHeightSpec = getChildMeasureSpec(heightMeasureSpec, heightPadding,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ widthSize -= widthPadding;
+
+ // Divide the view into cells.
+ final int cellCount = widthSize / mMinCellSize;
+ final int cellSizeRemaining = widthSize % mMinCellSize;
+
+ if (cellCount == 0) {
+ // Give up, nothing fits.
+ setMeasuredDimension(widthSize, 0);
+ return;
+ }
+
+ final int cellSize = mMinCellSize + cellSizeRemaining / cellCount;
+
+ int cellsRemaining = cellCount;
+ int maxChildHeight = 0;
+ int maxCellsUsed = 0;
+ int expandableItemCount = 0;
+ int visibleItemCount = 0;
+ boolean hasOverflow = false;
+
+ // This is used as a bitfield to locate the smallest items present. Assumes childCount < 64.
+ long smallestItemsAt = 0;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+
+ final boolean isGeneratedItem = child instanceof ActionMenuItemView;
+ visibleItemCount++;
+
+ if (isGeneratedItem) {
+ // Reset padding for generated menu item views; it may change below
+ // and views are recycled.
+ child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0);
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.expanded = false;
+ lp.extraPixels = 0;
+ lp.cellsUsed = 0;
+ lp.expandable = false;
+ lp.leftMargin = 0;
+ lp.rightMargin = 0;
+ lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText();
+
+ // Overflow always gets 1 cell. No more, no less.
+ final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining;
+
+ final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable,
+ itemHeightSpec, heightPadding);
+
+ maxCellsUsed = Math.max(maxCellsUsed, cellsUsed);
+ if (lp.expandable) expandableItemCount++;
+ if (lp.isOverflowButton) hasOverflow = true;
+
+ cellsRemaining -= cellsUsed;
+ maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
+ if (cellsUsed == 1) smallestItemsAt |= (1 << i);
+ }
+
+ // When we have overflow and a single expanded (text) item, we want to try centering it
+ // visually in the available space even though overflow consumes some of it.
+ final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2;
+
+ // Divide space for remaining cells if we have items that can expand.
+ // Try distributing whole leftover cells to smaller items first.
+
+ boolean needsExpansion = false;
+ while (expandableItemCount > 0 && cellsRemaining > 0) {
+ int minCells = Integer.MAX_VALUE;
+ long minCellsAt = 0; // Bit locations are indices of relevant child views
+ int minCellsItemCount = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ // Don't try to expand items that shouldn't.
+ if (!lp.expandable) continue;
+
+ // Mark indices of children that can receive an extra cell.
+ if (lp.cellsUsed < minCells) {
+ minCells = lp.cellsUsed;
+ minCellsAt = 1 << i;
+ minCellsItemCount = 1;
+ } else if (lp.cellsUsed == minCells) {
+ minCellsAt |= 1 << i;
+ minCellsItemCount++;
+ }
+ }
+
+ // Items that get expanded will always be in the set of smallest items when we're done.
+ smallestItemsAt |= minCellsAt;
+
+ if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop.
+
+ // We have enough cells, all minimum size items will be incremented.
+ minCells++;
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if ((minCellsAt & (1 << i)) == 0) {
+ // If this item is already at our small item count, mark it for later.
+ if (lp.cellsUsed == minCells) smallestItemsAt |= 1 << i;
+ continue;
+ }
+
+ if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) {
+ // Add padding to this item such that it centers.
+ child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0);
+ }
+ lp.cellsUsed++;
+ lp.expanded = true;
+ cellsRemaining--;
+ }
+
+ needsExpansion = true;
+ }
+
+ // Divide any space left that wouldn't divide along cell boundaries
+ // evenly among the smallest items
+
+ final boolean singleItem = !hasOverflow && visibleItemCount == 1;
+ if (cellsRemaining > 0 && smallestItemsAt != 0 &&
+ (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) {
+ float expandCount = Long.bitCount(smallestItemsAt);
+
+ if (!singleItem) {
+ // The items at the far edges may only expand by half in order to pin to either side.
+ if ((smallestItemsAt & 1) != 0) {
+ LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams();
+ if (!lp.preventEdgeOffset) expandCount -= 0.5f;
+ }
+ if ((smallestItemsAt & (1 << (childCount - 1))) != 0) {
+ LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams());
+ if (!lp.preventEdgeOffset) expandCount -= 0.5f;
+ }
+ }
+
+ final int extraPixels = expandCount > 0 ?
+ (int) (cellsRemaining * cellSize / expandCount) : 0;
+
+ for (int i = 0; i < childCount; i++) {
+ if ((smallestItemsAt & (1 << i)) == 0) continue;
+
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (child instanceof ActionMenuItemView) {
+ // If this is one of our views, expand and measure at the larger size.
+ lp.extraPixels = extraPixels;
+ lp.expanded = true;
+ if (i == 0 && !lp.preventEdgeOffset) {
+ // First item gets part of its new padding pushed out of sight.
+ // The last item will get this implicitly from layout.
+ lp.leftMargin = -extraPixels / 2;
+ }
+ needsExpansion = true;
+ } else if (lp.isOverflowButton) {
+ lp.extraPixels = extraPixels;
+ lp.expanded = true;
+ lp.rightMargin = -extraPixels / 2;
+ needsExpansion = true;
+ } else {
+ // If we don't know what it is, give it some margins instead
+ // and let it center within its space. We still want to pin
+ // against the edges.
+ if (i != 0) {
+ lp.leftMargin = extraPixels / 2;
+ }
+ if (i != childCount - 1) {
+ lp.rightMargin = extraPixels / 2;
+ }
+ }
+ }
+
+ cellsRemaining = 0;
+ }
+
+ // Remeasure any items that have had extra space allocated to them.
+ if (needsExpansion) {
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (!lp.expanded) continue;
+
+ final int width = lp.cellsUsed * cellSize + lp.extraPixels;
+ child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ itemHeightSpec);
+ }
+ }
+
+ if (heightMode != MeasureSpec.EXACTLY) {
+ heightSize = maxChildHeight;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ /**
+ * Measure a child view to fit within cell-based formatting. The child's width
+ * will be measured to a whole multiple of cellSize.
+ *
+ * <p>Sets the expandable and cellsUsed fields of LayoutParams.
+ *
+ * @param child Child to measure
+ * @param cellSize Size of one cell
+ * @param cellsRemaining Number of cells remaining that this view can expand to fill
+ * @param parentHeightMeasureSpec MeasureSpec used by the parent view
+ * @param parentHeightPadding Padding present in the parent view
+ * @return Number of cells this child was measured to occupy
+ */
+ static int measureChildForCells(View child, int cellSize, int cellsRemaining,
+ int parentHeightMeasureSpec, int parentHeightPadding) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) -
+ parentHeightPadding;
+ final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec);
+ final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
+
+ final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
+ (ActionMenuItemView) child : null;
+ final boolean hasText = itemView != null && itemView.hasText();
+
+ int cellsUsed = 0;
+ if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) {
+ final int childWidthSpec = MeasureSpec.makeMeasureSpec(
+ cellSize * cellsRemaining, MeasureSpec.AT_MOST);
+ child.measure(childWidthSpec, childHeightSpec);
+
+ final int measuredWidth = child.getMeasuredWidth();
+ cellsUsed = measuredWidth / cellSize;
+ if (measuredWidth % cellSize != 0) cellsUsed++;
+ if (hasText && cellsUsed < 2) cellsUsed = 2;
+ }
+
+ final boolean expandable = !lp.isOverflowButton && hasText;
+ lp.expandable = expandable;
+
+ lp.cellsUsed = cellsUsed;
+ final int targetWidth = cellsUsed * cellSize;
+ child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
+ childHeightSpec);
+ return cellsUsed;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (!mFormatItems) {
+ super.onLayout(changed, left, top, right, bottom);
+ return;
+ }
+
+ final int childCount = getChildCount();
+ final int midVertical = (bottom - top) / 2;
+ final int dividerWidth = getDividerWidth();
+ int overflowWidth = 0;
+ int nonOverflowWidth = 0;
+ int nonOverflowCount = 0;
+ int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
+ boolean hasOverflow = false;
+ final boolean isLayoutRtl = isLayoutRtl();
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ if (v.getVisibility() == GONE) {
+ continue;
+ }
+
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ if (p.isOverflowButton) {
+ overflowWidth = v.getMeasuredWidth();
+ if (hasDividerBeforeChildAt(i)) {
+ overflowWidth += dividerWidth;
+ }
+
+ int height = v.getMeasuredHeight();
+ int r;
+ int l;
+ if (isLayoutRtl) {
+ l = getPaddingLeft() + p.leftMargin;
+ r = l + overflowWidth;
+ } else {
+ r = getWidth() - getPaddingRight() - p.rightMargin;
+ l = r - overflowWidth;
+ }
+ int t = midVertical - (height / 2);
+ int b = t + height;
+ v.layout(l, t, r, b);
+
+ widthRemaining -= overflowWidth;
+ hasOverflow = true;
+ } else {
+ final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin;
+ nonOverflowWidth += size;
+ widthRemaining -= size;
+ if (hasDividerBeforeChildAt(i)) {
+ nonOverflowWidth += dividerWidth;
+ }
+ nonOverflowCount++;
+ }
+ }
+
+ if (childCount == 1 && !hasOverflow) {
+ // Center a single child
+ final View v = getChildAt(0);
+ final int width = v.getMeasuredWidth();
+ final int height = v.getMeasuredHeight();
+ final int midHorizontal = (right - left) / 2;
+ final int l = midHorizontal - width / 2;
+ final int t = midVertical - height / 2;
+ v.layout(l, t, l + width, t + height);
+ return;
+ }
+
+ final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
+ final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0);
+
+ if (isLayoutRtl) {
+ int startRight = getWidth() - getPaddingRight();
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (v.getVisibility() == GONE || lp.isOverflowButton) {
+ continue;
+ }
+
+ startRight -= lp.rightMargin;
+ int width = v.getMeasuredWidth();
+ int height = v.getMeasuredHeight();
+ int t = midVertical - height / 2;
+ v.layout(startRight - width, t, startRight, t + height);
+ startRight -= width + lp.leftMargin + spacerSize;
+ }
+ } else {
+ int startLeft = getPaddingLeft();
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (v.getVisibility() == GONE || lp.isOverflowButton) {
+ continue;
+ }
+
+ startLeft += lp.leftMargin;
+ int width = v.getMeasuredWidth();
+ int height = v.getMeasuredHeight();
+ int t = midVertical - height / 2;
+ v.layout(startLeft, t, startLeft + width, t + height);
+ startLeft += width + lp.rightMargin + spacerSize;
+ }
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ dismissPopupMenus();
+ }
+
+ /**
+ * Set the icon to use for the overflow button.
+ *
+ * @param icon Drawable to set, may be null to clear the icon
+ */
+ public void setOverflowIcon(@Nullable Drawable icon) {
+ getMenu();
+ mPresenter.setOverflowIcon(icon);
+ }
+
+ /**
+ * Return the current drawable used as the overflow icon.
+ *
+ * @return The overflow icon drawable
+ */
+ @Nullable
+ public Drawable getOverflowIcon() {
+ getMenu();
+ return mPresenter.getOverflowIcon();
+ }
+
+ /** @hide */
+ public boolean isOverflowReserved() {
+ return mReserveOverflow;
+ }
+
+ /** @hide */
+ public void setOverflowReserved(boolean reserveOverflow) {
+ mReserveOverflow = reserveOverflow;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ params.gravity = Gravity.CENTER_VERTICAL;
+ return params;
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p != null) {
+ final LayoutParams result = p instanceof LayoutParams
+ ? new LayoutParams((LayoutParams) p)
+ : new LayoutParams(p);
+ if (result.gravity <= Gravity.NO_GRAVITY) {
+ result.gravity = Gravity.CENTER_VERTICAL;
+ }
+ return result;
+ }
+ return generateDefaultLayoutParams();
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p != null && p instanceof LayoutParams;
+ }
+
+ /** @hide */
+ public LayoutParams generateOverflowButtonLayoutParams() {
+ LayoutParams result = generateDefaultLayoutParams();
+ result.isOverflowButton = true;
+ return result;
+ }
+
+ /** @hide */
+ public boolean invokeItem(MenuItemImpl item) {
+ return mMenu.performItemAction(item, 0);
+ }
+
+ /** @hide */
+ public int getWindowAnimations() {
+ return 0;
+ }
+
+ /** @hide */
+ public void initialize(@Nullable MenuBuilder menu) {
+ mMenu = menu;
+ }
+
+ /**
+ * Returns the Menu object that this ActionMenuView is currently presenting.
+ *
+ * <p>Applications should use this method to obtain the ActionMenuView's Menu object
+ * and inflate or add content to it as necessary.</p>
+ *
+ * @return the Menu presented by this view
+ */
+ public Menu getMenu() {
+ if (mMenu == null) {
+ final Context context = getContext();
+ mMenu = new MenuBuilder(context);
+ mMenu.setCallback(new MenuBuilderCallback());
+ mPresenter = new ActionMenuPresenter(context);
+ mPresenter.setReserveOverflow(true);
+ mPresenter.setCallback(mActionMenuPresenterCallback != null
+ ? mActionMenuPresenterCallback : new ActionMenuPresenterCallback());
+ mMenu.addMenuPresenter(mPresenter, mPopupContext);
+ mPresenter.setMenuView(this);
+ }
+
+ return mMenu;
+ }
+
+ /**
+ * Must be called before the first call to getMenu()
+ * @hide
+ */
+ public void setMenuCallbacks(MenuPresenter.Callback pcb, MenuBuilder.Callback mcb) {
+ mActionMenuPresenterCallback = pcb;
+ mMenuBuilderCallback = mcb;
+ }
+
+ /**
+ * Returns the current menu or null if one has not yet been configured.
+ * @hide Internal use only for action bar integration
+ */
+ public MenuBuilder peekMenu() {
+ return mMenu;
+ }
+
+ /**
+ * Show the overflow items from the associated menu.
+ *
+ * @return true if the menu was able to be shown, false otherwise
+ */
+ public boolean showOverflowMenu() {
+ return mPresenter != null && mPresenter.showOverflowMenu();
+ }
+
+ /**
+ * Hide the overflow items from the associated menu.
+ *
+ * @return true if the menu was able to be hidden, false otherwise
+ */
+ public boolean hideOverflowMenu() {
+ return mPresenter != null && mPresenter.hideOverflowMenu();
+ }
+
+ /**
+ * Check whether the overflow menu is currently showing. This may not reflect
+ * a pending show operation in progress.
+ *
+ * @return true if the overflow menu is currently showing
+ */
+ public boolean isOverflowMenuShowing() {
+ return mPresenter != null && mPresenter.isOverflowMenuShowing();
+ }
+
+ /** @hide */
+ public boolean isOverflowMenuShowPending() {
+ return mPresenter != null && mPresenter.isOverflowMenuShowPending();
+ }
+
+ /**
+ * Dismiss any popups associated with this menu view.
+ */
+ public void dismissPopupMenus() {
+ if (mPresenter != null) {
+ mPresenter.dismissPopupMenus();
+ }
+ }
+
+ /**
+ * @hide Private LinearLayout (superclass) API. Un-hide if LinearLayout API is made public.
+ */
+ @Override
+ protected boolean hasDividerBeforeChildAt(int childIndex) {
+ if (childIndex == 0) {
+ return false;
+ }
+ final View childBefore = getChildAt(childIndex - 1);
+ final View child = getChildAt(childIndex);
+ boolean result = false;
+ if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) {
+ result |= ((ActionMenuChildView) childBefore).needsDividerAfter();
+ }
+ if (childIndex > 0 && child instanceof ActionMenuChildView) {
+ result |= ((ActionMenuChildView) child).needsDividerBefore();
+ }
+ return result;
+ }
+
+ /** @hide */
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ return false;
+ }
+
+ /** @hide */
+ public void setExpandedActionViewsExclusive(boolean exclusive) {
+ mPresenter.setExpandedActionViewsExclusive(exclusive);
+ }
+
+ /**
+ * Interface responsible for receiving menu item click events if the items themselves
+ * do not have individual item click listeners.
+ */
+ public interface OnMenuItemClickListener {
+ /**
+ * This method will be invoked when a menu item is clicked if the item itself did
+ * not already handle the event.
+ *
+ * @param item {@link MenuItem} that was clicked
+ * @return <code>true</code> if the event was handled, <code>false</code> otherwise.
+ */
+ public boolean onMenuItemClick(MenuItem item);
+ }
+
+ private class MenuBuilderCallback implements MenuBuilder.Callback {
+ @Override
+ public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+ return mOnMenuItemClickListener != null &&
+ mOnMenuItemClickListener.onMenuItemClick(item);
+ }
+
+ @Override
+ public void onMenuModeChange(MenuBuilder menu) {
+ if (mMenuBuilderCallback != null) {
+ mMenuBuilderCallback.onMenuModeChange(menu);
+ }
+ }
+ }
+
+ private class ActionMenuPresenterCallback implements ActionMenuPresenter.Callback {
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ }
+
+ @Override
+ public boolean onOpenSubMenu(MenuBuilder subMenu) {
+ return false;
+ }
+ }
+
+ /** @hide */
+ public interface ActionMenuChildView {
+ public boolean needsDividerBefore();
+ public boolean needsDividerAfter();
+ }
+
+ public static class LayoutParams extends LinearLayout.LayoutParams {
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public boolean isOverflowButton;
+
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public int cellsUsed;
+
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public int extraPixels;
+
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public boolean expandable;
+
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "layout")
+ public boolean preventEdgeOffset;
+
+ /** @hide */
+ public boolean expanded;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams other) {
+ super(other);
+ }
+
+ public LayoutParams(LayoutParams other) {
+ super((LinearLayout.LayoutParams) other);
+ isOverflowButton = other.isOverflowButton;
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ isOverflowButton = false;
+ }
+
+ /** @hide */
+ public LayoutParams(int width, int height, boolean isOverflowButton) {
+ super(width, height);
+ this.isOverflowButton = isOverflowButton;
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("layout:overFlowButton", isOverflowButton);
+ encoder.addProperty("layout:cellsUsed", cellsUsed);
+ encoder.addProperty("layout:extraPixels", extraPixels);
+ encoder.addProperty("layout:expandable", expandable);
+ encoder.addProperty("layout:preventEdgeOffset", preventEdgeOffset);
+ }
+ }
+}
diff --git a/android/widget/ActivityChooserModel.java b/android/widget/ActivityChooserModel.java
new file mode 100644
index 0000000..75c857c
--- /dev/null
+++ b/android/widget/ActivityChooserModel.java
@@ -0,0 +1,1131 @@
+/*
+ * Copyright (C) 2011 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.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.DataSetObservable;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.content.PackageMonitor;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * This class represents a data model for choosing a component for handing a
+ * given {@link Intent}. The model is responsible for querying the system for
+ * activities that can handle the given intent and order found activities
+ * based on historical data of previous choices. The historical data is stored
+ * in an application private file. If a client does not want to have persistent
+ * choice history the file can be omitted, thus the activities will be ordered
+ * based on historical usage for the current session.
+ * <p>
+ * </p>
+ * For each backing history file there is a singleton instance of this class. Thus,
+ * several clients that specify the same history file will share the same model. Note
+ * that if multiple clients are sharing the same model they should implement semantically
+ * equivalent functionality since setting the model intent will change the found
+ * activities and they may be inconsistent with the functionality of some of the clients.
+ * For example, choosing a share activity can be implemented by a single backing
+ * model and two different views for performing the selection. If however, one of the
+ * views is used for sharing but the other for importing, for example, then each
+ * view should be backed by a separate model.
+ * </p>
+ * <p>
+ * The way clients interact with this class is as follows:
+ * </p>
+ * <p>
+ * <pre>
+ * <code>
+ * // Get a model and set it to a couple of clients with semantically similar function.
+ * ActivityChooserModel dataModel =
+ * ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
+ *
+ * ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
+ * modelClient1.setActivityChooserModel(dataModel);
+ *
+ * ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
+ * modelClient2.setActivityChooserModel(dataModel);
+ *
+ * // Set an intent to choose a an activity for.
+ * dataModel.setIntent(intent);
+ * <pre>
+ * <code>
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class is thread safe.
+ * </p>
+ *
+ * @hide
+ */
+public class ActivityChooserModel extends DataSetObservable {
+
+ /**
+ * Client that utilizes an {@link ActivityChooserModel}.
+ */
+ public interface ActivityChooserModelClient {
+
+ /**
+ * Sets the {@link ActivityChooserModel}.
+ *
+ * @param dataModel The model.
+ */
+ public void setActivityChooserModel(ActivityChooserModel dataModel);
+ }
+
+ /**
+ * Defines a sorter that is responsible for sorting the activities
+ * based on the provided historical choices and an intent.
+ */
+ public interface ActivitySorter {
+
+ /**
+ * Sorts the <code>activities</code> in descending order of relevance
+ * based on previous history and an intent.
+ *
+ * @param intent The {@link Intent}.
+ * @param activities Activities to be sorted.
+ * @param historicalRecords Historical records.
+ */
+ // This cannot be done by a simple comparator since an Activity weight
+ // is computed from history. Note that Activity implements Comparable.
+ public void sort(Intent intent, List<ActivityResolveInfo> activities,
+ List<HistoricalRecord> historicalRecords);
+ }
+
+ /**
+ * Listener for choosing an activity.
+ */
+ public interface OnChooseActivityListener {
+
+ /**
+ * Called when an activity has been chosen. The client can decide whether
+ * an activity can be chosen and if so the caller of
+ * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
+ * for launching it.
+ * <p>
+ * <strong>Note:</strong> Modifying the intent is not permitted and
+ * any changes to the latter will be ignored.
+ * </p>
+ *
+ * @param host The listener's host model.
+ * @param intent The intent for launching the chosen activity.
+ * @return Whether the intent is handled and should not be delivered to clients.
+ *
+ * @see ActivityChooserModel#chooseActivity(int)
+ */
+ public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
+ }
+
+ /**
+ * Flag for selecting debug mode.
+ */
+ private static final boolean DEBUG = false;
+
+ /**
+ * Tag used for logging.
+ */
+ private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
+
+ /**
+ * The root tag in the history file.
+ */
+ private static final String TAG_HISTORICAL_RECORDS = "historical-records";
+
+ /**
+ * The tag for a record in the history file.
+ */
+ private static final String TAG_HISTORICAL_RECORD = "historical-record";
+
+ /**
+ * Attribute for the activity.
+ */
+ private static final String ATTRIBUTE_ACTIVITY = "activity";
+
+ /**
+ * Attribute for the choice time.
+ */
+ private static final String ATTRIBUTE_TIME = "time";
+
+ /**
+ * Attribute for the choice weight.
+ */
+ private static final String ATTRIBUTE_WEIGHT = "weight";
+
+ /**
+ * The default name of the choice history file.
+ */
+ public static final String DEFAULT_HISTORY_FILE_NAME =
+ "activity_choser_model_history.xml";
+
+ /**
+ * The default maximal length of the choice history.
+ */
+ public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
+
+ /**
+ * The amount with which to inflate a chosen activity when set as default.
+ */
+ private static final int DEFAULT_ACTIVITY_INFLATION = 5;
+
+ /**
+ * Default weight for a choice record.
+ */
+ private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
+
+ /**
+ * The extension of the history file.
+ */
+ private static final String HISTORY_FILE_EXTENSION = ".xml";
+
+ /**
+ * An invalid item index.
+ */
+ private static final int INVALID_INDEX = -1;
+
+ /**
+ * Lock to guard the model registry.
+ */
+ private static final Object sRegistryLock = new Object();
+
+ /**
+ * This the registry for data models.
+ */
+ private static final Map<String, ActivityChooserModel> sDataModelRegistry =
+ new HashMap<String, ActivityChooserModel>();
+
+ /**
+ * Lock for synchronizing on this instance.
+ */
+ private final Object mInstanceLock = new Object();
+
+ /**
+ * List of activities that can handle the current intent.
+ */
+ private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
+
+ /**
+ * List with historical choice records.
+ */
+ private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
+
+ /**
+ * Monitor for added and removed packages.
+ */
+ private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor();
+
+ /**
+ * Context for accessing resources.
+ */
+ private final Context mContext;
+
+ /**
+ * The name of the history file that backs this model.
+ */
+ private final String mHistoryFileName;
+
+ /**
+ * The intent for which a activity is being chosen.
+ */
+ private Intent mIntent;
+
+ /**
+ * The sorter for ordering activities based on intent and past choices.
+ */
+ private ActivitySorter mActivitySorter = new DefaultSorter();
+
+ /**
+ * The maximal length of the choice history.
+ */
+ private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
+
+ /**
+ * Flag whether choice history can be read. In general many clients can
+ * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
+ * by arbitrary of them any number of times. Therefore, this class guarantees
+ * that the very first read succeeds and subsequent reads can be performed
+ * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
+ * of the share records.
+ */
+ private boolean mCanReadHistoricalData = true;
+
+ /**
+ * Flag whether the choice history was read. This is used to enforce that
+ * before calling {@link #persistHistoricalDataIfNeeded()} a call to
+ * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
+ * scenario in which a choice history file exits, it is not read yet and
+ * it is overwritten. Note that always all historical records are read in
+ * full and the file is rewritten. This is necessary since we need to
+ * purge old records that are outside of the sliding window of past choices.
+ */
+ private boolean mReadShareHistoryCalled = false;
+
+ /**
+ * Flag whether the choice records have changed. In general many clients can
+ * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
+ * by arbitrary of them any number of times. Therefore, this class guarantees
+ * that choice history will be persisted only if it has changed.
+ */
+ private boolean mHistoricalRecordsChanged = true;
+
+ /**
+ * Flag whether to reload the activities for the current intent.
+ */
+ private boolean mReloadActivities = false;
+
+ /**
+ * Policy for controlling how the model handles chosen activities.
+ */
+ private OnChooseActivityListener mActivityChoserModelPolicy;
+
+ /**
+ * Gets the data model backed by the contents of the provided file with historical data.
+ * Note that only one data model is backed by a given file, thus multiple calls with
+ * the same file name will return the same model instance. If no such instance is present
+ * it is created.
+ * <p>
+ * <strong>Note:</strong> To use the default historical data file clients should explicitly
+ * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
+ * history is desired clients should pass <code>null</code> for the file name. In such
+ * case a new model is returned for each invocation.
+ * </p>
+ *
+ * <p>
+ * <strong>Always use difference historical data files for semantically different actions.
+ * For example, sharing is different from importing.</strong>
+ * </p>
+ *
+ * @param context Context for loading resources.
+ * @param historyFileName File name with choice history, <code>null</code>
+ * if the model should not be backed by a file. In this case the activities
+ * will be ordered only by data from the current session.
+ *
+ * @return The model.
+ */
+ public static ActivityChooserModel get(Context context, String historyFileName) {
+ synchronized (sRegistryLock) {
+ ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
+ if (dataModel == null) {
+ dataModel = new ActivityChooserModel(context, historyFileName);
+ sDataModelRegistry.put(historyFileName, dataModel);
+ }
+ return dataModel;
+ }
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context Context for loading resources.
+ * @param historyFileName The history XML file.
+ */
+ private ActivityChooserModel(Context context, String historyFileName) {
+ mContext = context.getApplicationContext();
+ if (!TextUtils.isEmpty(historyFileName)
+ && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
+ mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
+ } else {
+ mHistoryFileName = historyFileName;
+ }
+ mPackageMonitor.register(mContext, null, true);
+ }
+
+ /**
+ * Sets an intent for which to choose a activity.
+ * <p>
+ * <strong>Note:</strong> Clients must set only semantically similar
+ * intents for each data model.
+ * <p>
+ *
+ * @param intent The intent.
+ */
+ public void setIntent(Intent intent) {
+ synchronized (mInstanceLock) {
+ if (mIntent == intent) {
+ return;
+ }
+ mIntent = intent;
+ mReloadActivities = true;
+ ensureConsistentState();
+ }
+ }
+
+ /**
+ * Gets the intent for which a activity is being chosen.
+ *
+ * @return The intent.
+ */
+ public Intent getIntent() {
+ synchronized (mInstanceLock) {
+ return mIntent;
+ }
+ }
+
+ /**
+ * Gets the number of activities that can handle the intent.
+ *
+ * @return The activity count.
+ *
+ * @see #setIntent(Intent)
+ */
+ public int getActivityCount() {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+ return mActivities.size();
+ }
+ }
+
+ /**
+ * Gets an activity at a given index.
+ *
+ * @return The activity.
+ *
+ * @see ActivityResolveInfo
+ * @see #setIntent(Intent)
+ */
+ public ResolveInfo getActivity(int index) {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+ return mActivities.get(index).resolveInfo;
+ }
+ }
+
+ /**
+ * Gets the index of a the given activity.
+ *
+ * @param activity The activity index.
+ *
+ * @return The index if found, -1 otherwise.
+ */
+ public int getActivityIndex(ResolveInfo activity) {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+ List<ActivityResolveInfo> activities = mActivities;
+ final int activityCount = activities.size();
+ for (int i = 0; i < activityCount; i++) {
+ ActivityResolveInfo currentActivity = activities.get(i);
+ if (currentActivity.resolveInfo == activity) {
+ return i;
+ }
+ }
+ return INVALID_INDEX;
+ }
+ }
+
+ /**
+ * Chooses a activity to handle the current intent. This will result in
+ * adding a historical record for that action and construct intent with
+ * its component name set such that it can be immediately started by the
+ * client.
+ * <p>
+ * <strong>Note:</strong> By calling this method the client guarantees
+ * that the returned intent will be started. This intent is returned to
+ * the client solely to let additional customization before the start.
+ * </p>
+ *
+ * @return An {@link Intent} for launching the activity or null if the
+ * policy has consumed the intent or there is not current intent
+ * set via {@link #setIntent(Intent)}.
+ *
+ * @see HistoricalRecord
+ * @see OnChooseActivityListener
+ */
+ public Intent chooseActivity(int index) {
+ synchronized (mInstanceLock) {
+ if (mIntent == null) {
+ return null;
+ }
+
+ ensureConsistentState();
+
+ ActivityResolveInfo chosenActivity = mActivities.get(index);
+
+ ComponentName chosenName = new ComponentName(
+ chosenActivity.resolveInfo.activityInfo.packageName,
+ chosenActivity.resolveInfo.activityInfo.name);
+
+ Intent choiceIntent = new Intent(mIntent);
+ choiceIntent.setComponent(chosenName);
+
+ if (mActivityChoserModelPolicy != null) {
+ // Do not allow the policy to change the intent.
+ Intent choiceIntentCopy = new Intent(choiceIntent);
+ final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
+ choiceIntentCopy);
+ if (handled) {
+ return null;
+ }
+ }
+
+ HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
+ System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
+ addHisoricalRecord(historicalRecord);
+
+ return choiceIntent;
+ }
+ }
+
+ /**
+ * Sets the listener for choosing an activity.
+ *
+ * @param listener The listener.
+ */
+ public void setOnChooseActivityListener(OnChooseActivityListener listener) {
+ synchronized (mInstanceLock) {
+ mActivityChoserModelPolicy = listener;
+ }
+ }
+
+ /**
+ * Gets the default activity, The default activity is defined as the one
+ * with highest rank i.e. the first one in the list of activities that can
+ * handle the intent.
+ *
+ * @return The default activity, <code>null</code> id not activities.
+ *
+ * @see #getActivity(int)
+ */
+ public ResolveInfo getDefaultActivity() {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+ if (!mActivities.isEmpty()) {
+ return mActivities.get(0).resolveInfo;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the default activity. The default activity is set by adding a
+ * historical record with weight high enough that this activity will
+ * become the highest ranked. Such a strategy guarantees that the default
+ * will eventually change if not used. Also the weight of the record for
+ * setting a default is inflated with a constant amount to guarantee that
+ * it will stay as default for awhile.
+ *
+ * @param index The index of the activity to set as default.
+ */
+ public void setDefaultActivity(int index) {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+
+ ActivityResolveInfo newDefaultActivity = mActivities.get(index);
+ ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
+
+ final float weight;
+ if (oldDefaultActivity != null) {
+ // Add a record with weight enough to boost the chosen at the top.
+ weight = oldDefaultActivity.weight - newDefaultActivity.weight
+ + DEFAULT_ACTIVITY_INFLATION;
+ } else {
+ weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
+ }
+
+ ComponentName defaultName = new ComponentName(
+ newDefaultActivity.resolveInfo.activityInfo.packageName,
+ newDefaultActivity.resolveInfo.activityInfo.name);
+ HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
+ System.currentTimeMillis(), weight);
+ addHisoricalRecord(historicalRecord);
+ }
+ }
+
+ /**
+ * Persists the history data to the backing file if the latter
+ * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
+ * throws an exception. Calling this method more than one without choosing an
+ * activity has not effect.
+ *
+ * @throws IllegalStateException If this method is called before a call to
+ * {@link #readHistoricalDataIfNeeded()}.
+ */
+ private void persistHistoricalDataIfNeeded() {
+ if (!mReadShareHistoryCalled) {
+ throw new IllegalStateException("No preceding call to #readHistoricalData");
+ }
+ if (!mHistoricalRecordsChanged) {
+ return;
+ }
+ mHistoricalRecordsChanged = false;
+ if (!TextUtils.isEmpty(mHistoryFileName)) {
+ new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
+ new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
+ }
+ }
+
+ /**
+ * Sets the sorter for ordering activities based on historical data and an intent.
+ *
+ * @param activitySorter The sorter.
+ *
+ * @see ActivitySorter
+ */
+ public void setActivitySorter(ActivitySorter activitySorter) {
+ synchronized (mInstanceLock) {
+ if (mActivitySorter == activitySorter) {
+ return;
+ }
+ mActivitySorter = activitySorter;
+ if (sortActivitiesIfNeeded()) {
+ notifyChanged();
+ }
+ }
+ }
+
+ /**
+ * Sets the maximal size of the historical data. Defaults to
+ * {@link #DEFAULT_HISTORY_MAX_LENGTH}
+ * <p>
+ * <strong>Note:</strong> Setting this property will immediately
+ * enforce the specified max history size by dropping enough old
+ * historical records to enforce the desired size. Thus, any
+ * records that exceed the history size will be discarded and
+ * irreversibly lost.
+ * </p>
+ *
+ * @param historyMaxSize The max history size.
+ */
+ public void setHistoryMaxSize(int historyMaxSize) {
+ synchronized (mInstanceLock) {
+ if (mHistoryMaxSize == historyMaxSize) {
+ return;
+ }
+ mHistoryMaxSize = historyMaxSize;
+ pruneExcessiveHistoricalRecordsIfNeeded();
+ if (sortActivitiesIfNeeded()) {
+ notifyChanged();
+ }
+ }
+ }
+
+ /**
+ * Gets the history max size.
+ *
+ * @return The history max size.
+ */
+ public int getHistoryMaxSize() {
+ synchronized (mInstanceLock) {
+ return mHistoryMaxSize;
+ }
+ }
+
+ /**
+ * Gets the history size.
+ *
+ * @return The history size.
+ */
+ public int getHistorySize() {
+ synchronized (mInstanceLock) {
+ ensureConsistentState();
+ return mHistoricalRecords.size();
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ mPackageMonitor.unregister();
+ }
+
+ /**
+ * Ensures the model is in a consistent state which is the
+ * activities for the current intent have been loaded, the
+ * most recent history has been read, and the activities
+ * are sorted.
+ */
+ private void ensureConsistentState() {
+ boolean stateChanged = loadActivitiesIfNeeded();
+ stateChanged |= readHistoricalDataIfNeeded();
+ pruneExcessiveHistoricalRecordsIfNeeded();
+ if (stateChanged) {
+ sortActivitiesIfNeeded();
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Sorts the activities if necessary which is if there is a
+ * sorter, there are some activities to sort, and there is some
+ * historical data.
+ *
+ * @return Whether sorting was performed.
+ */
+ private boolean sortActivitiesIfNeeded() {
+ if (mActivitySorter != null && mIntent != null
+ && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
+ mActivitySorter.sort(mIntent, mActivities,
+ Collections.unmodifiableList(mHistoricalRecords));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Loads the activities for the current intent if needed which is
+ * if they are not already loaded for the current intent.
+ *
+ * @return Whether loading was performed.
+ */
+ private boolean loadActivitiesIfNeeded() {
+ if (mReloadActivities && mIntent != null) {
+ mReloadActivities = false;
+ mActivities.clear();
+ List<ResolveInfo> resolveInfos = mContext.getPackageManager()
+ .queryIntentActivities(mIntent, 0);
+ final int resolveInfoCount = resolveInfos.size();
+ for (int i = 0; i < resolveInfoCount; i++) {
+ ResolveInfo resolveInfo = resolveInfos.get(i);
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+ if (ActivityManager.checkComponentPermission(activityInfo.permission,
+ android.os.Process.myUid(), activityInfo.applicationInfo.uid,
+ activityInfo.exported) == PackageManager.PERMISSION_GRANTED) {
+ mActivities.add(new ActivityResolveInfo(resolveInfo));
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Reads the historical data if necessary which is it has
+ * changed, there is a history file, and there is not persist
+ * in progress.
+ *
+ * @return Whether reading was performed.
+ */
+ private boolean readHistoricalDataIfNeeded() {
+ if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
+ !TextUtils.isEmpty(mHistoryFileName)) {
+ mCanReadHistoricalData = false;
+ mReadShareHistoryCalled = true;
+ readHistoricalDataImpl();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adds a historical record.
+ *
+ * @param historicalRecord The record to add.
+ * @return True if the record was added.
+ */
+ private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
+ final boolean added = mHistoricalRecords.add(historicalRecord);
+ if (added) {
+ mHistoricalRecordsChanged = true;
+ pruneExcessiveHistoricalRecordsIfNeeded();
+ persistHistoricalDataIfNeeded();
+ sortActivitiesIfNeeded();
+ notifyChanged();
+ }
+ return added;
+ }
+
+ /**
+ * Prunes older excessive records to guarantee maxHistorySize.
+ */
+ private void pruneExcessiveHistoricalRecordsIfNeeded() {
+ final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
+ if (pruneCount <= 0) {
+ return;
+ }
+ mHistoricalRecordsChanged = true;
+ for (int i = 0; i < pruneCount; i++) {
+ HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Pruned: " + prunedRecord);
+ }
+ }
+ }
+
+ /**
+ * Represents a record in the history.
+ */
+ public final static class HistoricalRecord {
+
+ /**
+ * The activity name.
+ */
+ public final ComponentName activity;
+
+ /**
+ * The choice time.
+ */
+ public final long time;
+
+ /**
+ * The record weight.
+ */
+ public final float weight;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param activityName The activity component name flattened to string.
+ * @param time The time the activity was chosen.
+ * @param weight The weight of the record.
+ */
+ public HistoricalRecord(String activityName, long time, float weight) {
+ this(ComponentName.unflattenFromString(activityName), time, weight);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param activityName The activity name.
+ * @param time The time the activity was chosen.
+ * @param weight The weight of the record.
+ */
+ public HistoricalRecord(ComponentName activityName, long time, float weight) {
+ this.activity = activityName;
+ this.time = time;
+ this.weight = weight;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((activity == null) ? 0 : activity.hashCode());
+ result = prime * result + (int) (time ^ (time >>> 32));
+ result = prime * result + Float.floatToIntBits(weight);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ HistoricalRecord other = (HistoricalRecord) obj;
+ if (activity == null) {
+ if (other.activity != null) {
+ return false;
+ }
+ } else if (!activity.equals(other.activity)) {
+ return false;
+ }
+ if (time != other.time) {
+ return false;
+ }
+ if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ builder.append("; activity:").append(activity);
+ builder.append("; time:").append(time);
+ builder.append("; weight:").append(new BigDecimal(weight));
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * Represents an activity.
+ */
+ public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
+
+ /**
+ * The {@link ResolveInfo} of the activity.
+ */
+ public final ResolveInfo resolveInfo;
+
+ /**
+ * Weight of the activity. Useful for sorting.
+ */
+ public float weight;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param resolveInfo activity {@link ResolveInfo}.
+ */
+ public ActivityResolveInfo(ResolveInfo resolveInfo) {
+ this.resolveInfo = resolveInfo;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 + Float.floatToIntBits(weight);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ActivityResolveInfo other = (ActivityResolveInfo) obj;
+ if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
+ return false;
+ }
+ return true;
+ }
+
+ public int compareTo(ActivityResolveInfo another) {
+ return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ builder.append("resolveInfo:").append(resolveInfo.toString());
+ builder.append("; weight:").append(new BigDecimal(weight));
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * Default activity sorter implementation.
+ */
+ private final class DefaultSorter implements ActivitySorter {
+ private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
+
+ private final Map<ComponentName, ActivityResolveInfo> mPackageNameToActivityMap =
+ new HashMap<ComponentName, ActivityResolveInfo>();
+
+ public void sort(Intent intent, List<ActivityResolveInfo> activities,
+ List<HistoricalRecord> historicalRecords) {
+ Map<ComponentName, ActivityResolveInfo> componentNameToActivityMap =
+ mPackageNameToActivityMap;
+ componentNameToActivityMap.clear();
+
+ final int activityCount = activities.size();
+ for (int i = 0; i < activityCount; i++) {
+ ActivityResolveInfo activity = activities.get(i);
+ activity.weight = 0.0f;
+ ComponentName componentName = new ComponentName(
+ activity.resolveInfo.activityInfo.packageName,
+ activity.resolveInfo.activityInfo.name);
+ componentNameToActivityMap.put(componentName, activity);
+ }
+
+ final int lastShareIndex = historicalRecords.size() - 1;
+ float nextRecordWeight = 1;
+ for (int i = lastShareIndex; i >= 0; i--) {
+ HistoricalRecord historicalRecord = historicalRecords.get(i);
+ ComponentName componentName = historicalRecord.activity;
+ ActivityResolveInfo activity = componentNameToActivityMap.get(componentName);
+ if (activity != null) {
+ activity.weight += historicalRecord.weight * nextRecordWeight;
+ nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
+ }
+ }
+
+ Collections.sort(activities);
+
+ if (DEBUG) {
+ for (int i = 0; i < activityCount; i++) {
+ Log.i(LOG_TAG, "Sorted: " + activities.get(i));
+ }
+ }
+ }
+ }
+
+ private void readHistoricalDataImpl() {
+ FileInputStream fis = null;
+ try {
+ fis = mContext.openFileInput(mHistoryFileName);
+ } catch (FileNotFoundException fnfe) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
+ }
+ return;
+ }
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, StandardCharsets.UTF_8.name());
+
+ int type = XmlPullParser.START_DOCUMENT;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
+ throw new XmlPullParserException("Share records file does not start with "
+ + TAG_HISTORICAL_RECORDS + " tag.");
+ }
+
+ List<HistoricalRecord> historicalRecords = mHistoricalRecords;
+ historicalRecords.clear();
+
+ while (true) {
+ type = parser.next();
+ if (type == XmlPullParser.END_DOCUMENT) {
+ break;
+ }
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ String nodeName = parser.getName();
+ if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
+ throw new XmlPullParserException("Share records file not well-formed.");
+ }
+
+ String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
+ final long time =
+ Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
+ final float weight =
+ Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
+ HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
+ historicalRecords.add(readRecord);
+
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Read " + readRecord.toString());
+ }
+ }
+
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
+ }
+ } catch (XmlPullParserException xppe) {
+ Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException ioe) {
+ /* ignore */
+ }
+ }
+ }
+ }
+
+ /**
+ * Command for persisting the historical records to a file off the UI thread.
+ */
+ private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Void doInBackground(Object... args) {
+ List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
+ String hostoryFileName = (String) args[1];
+
+ FileOutputStream fos = null;
+
+ try {
+ fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE);
+ } catch (FileNotFoundException fnfe) {
+ Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe);
+ return null;
+ }
+
+ XmlSerializer serializer = Xml.newSerializer();
+
+ try {
+ serializer.setOutput(fos, null);
+ serializer.startDocument(StandardCharsets.UTF_8.name(), true);
+ serializer.startTag(null, TAG_HISTORICAL_RECORDS);
+
+ final int recordCount = historicalRecords.size();
+ for (int i = 0; i < recordCount; i++) {
+ HistoricalRecord record = historicalRecords.remove(0);
+ serializer.startTag(null, TAG_HISTORICAL_RECORD);
+ serializer.attribute(null, ATTRIBUTE_ACTIVITY,
+ record.activity.flattenToString());
+ serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
+ serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
+ serializer.endTag(null, TAG_HISTORICAL_RECORD);
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Wrote " + record.toString());
+ }
+ }
+
+ serializer.endTag(null, TAG_HISTORICAL_RECORDS);
+ serializer.endDocument();
+
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
+ }
+ } catch (IllegalArgumentException iae) {
+ Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
+ } catch (IllegalStateException ise) {
+ Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
+ } finally {
+ mCanReadHistoricalData = true;
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ /* ignore */
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Keeps in sync the historical records and activities with the installed applications.
+ */
+ private final class DataModelPackageMonitor extends PackageMonitor {
+
+ @Override
+ public void onSomePackagesChanged() {
+ mReloadActivities = true;
+ }
+ }
+}
diff --git a/android/widget/ActivityChooserView.java b/android/widget/ActivityChooserView.java
new file mode 100644
index 0000000..121a8c5
--- /dev/null
+++ b/android/widget/ActivityChooserView.java
@@ -0,0 +1,864 @@
+/*
+ * Copyright (C) 2011 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.StringRes;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ActionProvider;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ActivityChooserModel.ActivityChooserModelClient;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * This class is a view for choosing an activity for handling a given {@link Intent}.
+ * <p>
+ * The view is composed of two adjacent buttons:
+ * <ul>
+ * <li>
+ * The left button is an immediate action and allows one click activity choosing.
+ * Tapping this button immediately executes the intent without requiring any further
+ * user input. Long press on this button shows a popup for changing the default
+ * activity.
+ * </li>
+ * <li>
+ * The right button is an overflow action and provides an optimized menu
+ * of additional activities. Tapping this button shows a popup anchored to this
+ * view, listing the most frequently used activities. This list is initially
+ * limited to a small number of items in frequency used order. The last item,
+ * "Show all..." serves as an affordance to display all available activities.
+ * </li>
+ * </ul>
+ * </p>
+ *
+ * @hide
+ */
+public class ActivityChooserView extends ViewGroup implements ActivityChooserModelClient {
+
+ private static final String LOG_TAG = "ActivityChooserView";
+
+ /**
+ * An adapter for displaying the activities in an {@link AdapterView}.
+ */
+ private final ActivityChooserViewAdapter mAdapter;
+
+ /**
+ * Implementation of various interfaces to avoid publishing them in the APIs.
+ */
+ private final Callbacks mCallbacks;
+
+ /**
+ * The content of this view.
+ */
+ private final LinearLayout mActivityChooserContent;
+
+ /**
+ * Stores the background drawable to allow hiding and latter showing.
+ */
+ private final Drawable mActivityChooserContentBackground;
+
+ /**
+ * The expand activities action button;
+ */
+ private final FrameLayout mExpandActivityOverflowButton;
+
+ /**
+ * The image for the expand activities action button;
+ */
+ private final ImageView mExpandActivityOverflowButtonImage;
+
+ /**
+ * The default activities action button;
+ */
+ private final FrameLayout mDefaultActivityButton;
+
+ /**
+ * The image for the default activities action button;
+ */
+ private final ImageView mDefaultActivityButtonImage;
+
+ /**
+ * The maximal width of the list popup.
+ */
+ private final int mListPopupMaxWidth;
+
+ /**
+ * The ActionProvider hosting this view, if applicable.
+ */
+ ActionProvider mProvider;
+
+ /**
+ * Observer for the model data.
+ */
+ private final DataSetObserver mModelDataSetOberver = new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ mAdapter.notifyDataSetChanged();
+ }
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ mAdapter.notifyDataSetInvalidated();
+ }
+ };
+
+ private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (isShowingPopup()) {
+ if (!isShown()) {
+ getListPopupWindow().dismiss();
+ } else {
+ getListPopupWindow().show();
+ if (mProvider != null) {
+ mProvider.subUiVisibilityChanged(true);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Popup window for showing the activity overflow list.
+ */
+ private ListPopupWindow mListPopupWindow;
+
+ /**
+ * Listener for the dismissal of the popup/alert.
+ */
+ private PopupWindow.OnDismissListener mOnDismissListener;
+
+ /**
+ * Flag whether a default activity currently being selected.
+ */
+ private boolean mIsSelectingDefaultActivity;
+
+ /**
+ * The count of activities in the popup.
+ */
+ private int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT;
+
+ /**
+ * Flag whether this view is attached to a window.
+ */
+ private boolean mIsAttachedToWindow;
+
+ /**
+ * String resource for formatting content description of the default target.
+ */
+ private int mDefaultActionButtonContentDescription;
+
+ /**
+ * Create a new instance.
+ *
+ * @param context The application environment.
+ */
+ public ActivityChooserView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * @param context The application environment.
+ * @param attrs A collection of attributes.
+ */
+ public ActivityChooserView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * @param context The application environment.
+ * @param attrs A collection of attributes.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * @param context The application environment.
+ * @param attrs A collection of attributes.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
+ */
+ public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+ R.styleable.ActivityChooserView, defStyleAttr, defStyleRes);
+
+ mInitialActivityCount = attributesArray.getInt(
+ R.styleable.ActivityChooserView_initialActivityCount,
+ ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT);
+
+ Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable(
+ R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable);
+
+ attributesArray.recycle();
+
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ inflater.inflate(R.layout.activity_chooser_view, this, true);
+
+ mCallbacks = new Callbacks();
+
+ mActivityChooserContent = (LinearLayout) findViewById(R.id.activity_chooser_view_content);
+ mActivityChooserContentBackground = mActivityChooserContent.getBackground();
+
+ mDefaultActivityButton = (FrameLayout) findViewById(R.id.default_activity_button);
+ mDefaultActivityButton.setOnClickListener(mCallbacks);
+ mDefaultActivityButton.setOnLongClickListener(mCallbacks);
+ mDefaultActivityButtonImage = mDefaultActivityButton.findViewById(R.id.image);
+
+ final FrameLayout expandButton = (FrameLayout) findViewById(R.id.expand_activities_button);
+ expandButton.setOnClickListener(mCallbacks);
+ expandButton.setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setCanOpenPopup(true);
+ }
+ });
+ expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
+ @Override
+ public ShowableListMenu getPopup() {
+ return getListPopupWindow();
+ }
+
+ @Override
+ protected boolean onForwardingStarted() {
+ showPopup();
+ return true;
+ }
+
+ @Override
+ protected boolean onForwardingStopped() {
+ dismissPopup();
+ return true;
+ }
+ });
+ mExpandActivityOverflowButton = expandButton;
+
+ mExpandActivityOverflowButtonImage =
+ expandButton.findViewById(R.id.image);
+ mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable);
+
+ mAdapter = new ActivityChooserViewAdapter();
+ mAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ updateAppearance();
+ }
+ });
+
+ Resources resources = context.getResources();
+ mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2,
+ resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setActivityChooserModel(ActivityChooserModel dataModel) {
+ mAdapter.setDataModel(dataModel);
+ if (isShowingPopup()) {
+ dismissPopup();
+ showPopup();
+ }
+ }
+
+ /**
+ * Sets the background for the button that expands the activity
+ * overflow list.
+ *
+ * <strong>Note:</strong> Clients would like to set this drawable
+ * as a clue about the action the chosen activity will perform. For
+ * example, if a share activity is to be chosen the drawable should
+ * give a clue that sharing is to be performed.
+ *
+ * @param drawable The drawable.
+ */
+ public void setExpandActivityOverflowButtonDrawable(Drawable drawable) {
+ mExpandActivityOverflowButtonImage.setImageDrawable(drawable);
+ }
+
+ /**
+ * Sets the content description for the button that expands the activity
+ * overflow list.
+ *
+ * description as a clue about the action performed by the button.
+ * For example, if a share activity is to be chosen the content
+ * description should be something like "Share with".
+ *
+ * @param resourceId The content description resource id.
+ */
+ public void setExpandActivityOverflowButtonContentDescription(@StringRes int resourceId) {
+ CharSequence contentDescription = mContext.getString(resourceId);
+ mExpandActivityOverflowButtonImage.setContentDescription(contentDescription);
+ }
+
+ /**
+ * Set the provider hosting this view, if applicable.
+ * @hide Internal use only
+ */
+ public void setProvider(ActionProvider provider) {
+ mProvider = provider;
+ }
+
+ /**
+ * Shows the popup window with activities.
+ *
+ * @return True if the popup was shown, false if already showing.
+ */
+ public boolean showPopup() {
+ if (isShowingPopup() || !mIsAttachedToWindow) {
+ return false;
+ }
+ mIsSelectingDefaultActivity = false;
+ showPopupUnchecked(mInitialActivityCount);
+ return true;
+ }
+
+ /**
+ * Shows the popup no matter if it was already showing.
+ *
+ * @param maxActivityCount The max number of activities to display.
+ */
+ private void showPopupUnchecked(int maxActivityCount) {
+ if (mAdapter.getDataModel() == null) {
+ throw new IllegalStateException("No data model. Did you call #setDataModel?");
+ }
+
+ getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
+
+ final boolean defaultActivityButtonShown =
+ mDefaultActivityButton.getVisibility() == VISIBLE;
+
+ final int activityCount = mAdapter.getActivityCount();
+ final int maxActivityCountOffset = defaultActivityButtonShown ? 1 : 0;
+ if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED
+ && activityCount > maxActivityCount + maxActivityCountOffset) {
+ mAdapter.setShowFooterView(true);
+ mAdapter.setMaxActivityCount(maxActivityCount - 1);
+ } else {
+ mAdapter.setShowFooterView(false);
+ mAdapter.setMaxActivityCount(maxActivityCount);
+ }
+
+ ListPopupWindow popupWindow = getListPopupWindow();
+ if (!popupWindow.isShowing()) {
+ if (mIsSelectingDefaultActivity || !defaultActivityButtonShown) {
+ mAdapter.setShowDefaultActivity(true, defaultActivityButtonShown);
+ } else {
+ mAdapter.setShowDefaultActivity(false, false);
+ }
+ final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth);
+ popupWindow.setContentWidth(contentWidth);
+ popupWindow.show();
+ if (mProvider != null) {
+ mProvider.subUiVisibilityChanged(true);
+ }
+ popupWindow.getListView().setContentDescription(mContext.getString(
+ R.string.activitychooserview_choose_application));
+ popupWindow.getListView().setSelector(new ColorDrawable(Color.TRANSPARENT));
+ }
+ }
+
+ /**
+ * Dismisses the popup window with activities.
+ *
+ * @return True if dismissed, false if already dismissed.
+ */
+ public boolean dismissPopup() {
+ if (isShowingPopup()) {
+ getListPopupWindow().dismiss();
+ ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets whether the popup window with activities is shown.
+ *
+ * @return True if the popup is shown.
+ */
+ public boolean isShowingPopup() {
+ return getListPopupWindow().isShowing();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ActivityChooserModel dataModel = mAdapter.getDataModel();
+ if (dataModel != null) {
+ dataModel.registerObserver(mModelDataSetOberver);
+ }
+ mIsAttachedToWindow = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ActivityChooserModel dataModel = mAdapter.getDataModel();
+ if (dataModel != null) {
+ dataModel.unregisterObserver(mModelDataSetOberver);
+ }
+ ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
+ }
+ if (isShowingPopup()) {
+ dismissPopup();
+ }
+ mIsAttachedToWindow = false;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ View child = mActivityChooserContent;
+ // If the default action is not visible we want to be as tall as the
+ // ActionBar so if this widget is used in the latter it will look as
+ // a normal action button.
+ if (mDefaultActivityButton.getVisibility() != VISIBLE) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
+ MeasureSpec.EXACTLY);
+ }
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mActivityChooserContent.layout(0, 0, right - left, bottom - top);
+ if (!isShowingPopup()) {
+ dismissPopup();
+ }
+ }
+
+ public ActivityChooserModel getDataModel() {
+ return mAdapter.getDataModel();
+ }
+
+ /**
+ * Sets a listener to receive a callback when the popup is dismissed.
+ *
+ * @param listener The listener to be notified.
+ */
+ public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
+ mOnDismissListener = listener;
+ }
+
+ /**
+ * Sets the initial count of items shown in the activities popup
+ * i.e. the items before the popup is expanded. This is an upper
+ * bound since it is not guaranteed that such number of intent
+ * handlers exist.
+ *
+ * @param itemCount The initial popup item count.
+ */
+ public void setInitialActivityCount(int itemCount) {
+ mInitialActivityCount = itemCount;
+ }
+
+ /**
+ * Sets a content description of the default action button. This
+ * resource should be a string taking one formatting argument and
+ * will be used for formatting the content description of the button
+ * dynamically as the default target changes. For example, a resource
+ * pointing to the string "share with %1$s" will result in a content
+ * description "share with Bluetooth" for the Bluetooth activity.
+ *
+ * @param resourceId The resource id.
+ */
+ public void setDefaultActionButtonContentDescription(@StringRes int resourceId) {
+ mDefaultActionButtonContentDescription = resourceId;
+ }
+
+ /**
+ * Gets the list popup window which is lazily initialized.
+ *
+ * @return The popup.
+ */
+ private ListPopupWindow getListPopupWindow() {
+ if (mListPopupWindow == null) {
+ mListPopupWindow = new ListPopupWindow(getContext());
+ mListPopupWindow.setAdapter(mAdapter);
+ mListPopupWindow.setAnchorView(ActivityChooserView.this);
+ mListPopupWindow.setModal(true);
+ mListPopupWindow.setOnItemClickListener(mCallbacks);
+ mListPopupWindow.setOnDismissListener(mCallbacks);
+ }
+ return mListPopupWindow;
+ }
+
+ /**
+ * Updates the buttons state.
+ */
+ private void updateAppearance() {
+ // Expand overflow button.
+ if (mAdapter.getCount() > 0) {
+ mExpandActivityOverflowButton.setEnabled(true);
+ } else {
+ mExpandActivityOverflowButton.setEnabled(false);
+ }
+ // Default activity button.
+ final int activityCount = mAdapter.getActivityCount();
+ final int historySize = mAdapter.getHistorySize();
+ if (activityCount==1 || activityCount > 1 && historySize > 0) {
+ mDefaultActivityButton.setVisibility(VISIBLE);
+ ResolveInfo activity = mAdapter.getDefaultActivity();
+ PackageManager packageManager = mContext.getPackageManager();
+ mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager));
+ if (mDefaultActionButtonContentDescription != 0) {
+ CharSequence label = activity.loadLabel(packageManager);
+ String contentDescription = mContext.getString(
+ mDefaultActionButtonContentDescription, label);
+ mDefaultActivityButton.setContentDescription(contentDescription);
+ }
+ } else {
+ mDefaultActivityButton.setVisibility(View.GONE);
+ }
+ // Activity chooser content.
+ if (mDefaultActivityButton.getVisibility() == VISIBLE) {
+ mActivityChooserContent.setBackground(mActivityChooserContentBackground);
+ } else {
+ mActivityChooserContent.setBackground(null);
+ }
+ }
+
+ /**
+ * Interface implementation to avoid publishing them in the APIs.
+ */
+ private class Callbacks implements AdapterView.OnItemClickListener,
+ View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener {
+
+ // AdapterView#OnItemClickListener
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter();
+ final int itemViewType = adapter.getItemViewType(position);
+ switch (itemViewType) {
+ case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: {
+ showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED);
+ } break;
+ case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: {
+ dismissPopup();
+ if (mIsSelectingDefaultActivity) {
+ // The item at position zero is the default already.
+ if (position > 0) {
+ mAdapter.getDataModel().setDefaultActivity(position);
+ }
+ } else {
+ // If the default target is not shown in the list, the first
+ // item in the model is default action => adjust index
+ position = mAdapter.getShowDefaultActivity() ? position : position + 1;
+ Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
+ if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ ResolveInfo resolveInfo = mAdapter.getDataModel().getActivity(position);
+ startActivity(launchIntent, resolveInfo);
+ }
+ }
+ } break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ // View.OnClickListener
+ public void onClick(View view) {
+ if (view == mDefaultActivityButton) {
+ dismissPopup();
+ ResolveInfo defaultActivity = mAdapter.getDefaultActivity();
+ final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
+ Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
+ if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ startActivity(launchIntent, defaultActivity);
+ }
+ } else if (view == mExpandActivityOverflowButton) {
+ mIsSelectingDefaultActivity = false;
+ showPopupUnchecked(mInitialActivityCount);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ // OnLongClickListener#onLongClick
+ @Override
+ public boolean onLongClick(View view) {
+ if (view == mDefaultActivityButton) {
+ if (mAdapter.getCount() > 0) {
+ mIsSelectingDefaultActivity = true;
+ showPopupUnchecked(mInitialActivityCount);
+ }
+ } else {
+ throw new IllegalArgumentException();
+ }
+ return true;
+ }
+
+ // PopUpWindow.OnDismissListener#onDismiss
+ public void onDismiss() {
+ notifyOnDismissListener();
+ if (mProvider != null) {
+ mProvider.subUiVisibilityChanged(false);
+ }
+ }
+
+ private void notifyOnDismissListener() {
+ if (mOnDismissListener != null) {
+ mOnDismissListener.onDismiss();
+ }
+ }
+
+ private void startActivity(Intent intent, ResolveInfo resolveInfo) {
+ try {
+ mContext.startActivity(intent);
+ } catch (RuntimeException re) {
+ CharSequence appLabel = resolveInfo.loadLabel(mContext.getPackageManager());
+ String message = mContext.getString(
+ R.string.activitychooserview_choose_application_error, appLabel);
+ Log.e(LOG_TAG, message);
+ Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ /**
+ * Adapter for backing the list of activities shown in the popup.
+ */
+ private class ActivityChooserViewAdapter extends BaseAdapter {
+
+ public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE;
+
+ public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4;
+
+ private static final int ITEM_VIEW_TYPE_ACTIVITY = 0;
+
+ private static final int ITEM_VIEW_TYPE_FOOTER = 1;
+
+ private static final int ITEM_VIEW_TYPE_COUNT = 3;
+
+ private ActivityChooserModel mDataModel;
+
+ private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
+
+ private boolean mShowDefaultActivity;
+
+ private boolean mHighlightDefaultActivity;
+
+ private boolean mShowFooterView;
+
+ public void setDataModel(ActivityChooserModel dataModel) {
+ ActivityChooserModel oldDataModel = mAdapter.getDataModel();
+ if (oldDataModel != null && isShown()) {
+ oldDataModel.unregisterObserver(mModelDataSetOberver);
+ }
+ mDataModel = dataModel;
+ if (dataModel != null && isShown()) {
+ dataModel.registerObserver(mModelDataSetOberver);
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (mShowFooterView && position == getCount() - 1) {
+ return ITEM_VIEW_TYPE_FOOTER;
+ } else {
+ return ITEM_VIEW_TYPE_ACTIVITY;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return ITEM_VIEW_TYPE_COUNT;
+ }
+
+ public int getCount() {
+ int count = 0;
+ int activityCount = mDataModel.getActivityCount();
+ if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
+ activityCount--;
+ }
+ count = Math.min(activityCount, mMaxActivityCount);
+ if (mShowFooterView) {
+ count++;
+ }
+ return count;
+ }
+
+ public Object getItem(int position) {
+ final int itemViewType = getItemViewType(position);
+ switch (itemViewType) {
+ case ITEM_VIEW_TYPE_FOOTER:
+ return null;
+ case ITEM_VIEW_TYPE_ACTIVITY:
+ if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
+ position++;
+ }
+ return mDataModel.getActivity(position);
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final int itemViewType = getItemViewType(position);
+ switch (itemViewType) {
+ case ITEM_VIEW_TYPE_FOOTER:
+ if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) {
+ convertView = LayoutInflater.from(getContext()).inflate(
+ R.layout.activity_chooser_view_list_item, parent, false);
+ convertView.setId(ITEM_VIEW_TYPE_FOOTER);
+ TextView titleView = convertView.findViewById(R.id.title);
+ titleView.setText(mContext.getString(
+ R.string.activity_chooser_view_see_all));
+ }
+ return convertView;
+ case ITEM_VIEW_TYPE_ACTIVITY:
+ if (convertView == null || convertView.getId() != R.id.list_item) {
+ convertView = LayoutInflater.from(getContext()).inflate(
+ R.layout.activity_chooser_view_list_item, parent, false);
+ }
+ PackageManager packageManager = mContext.getPackageManager();
+ // Set the icon
+ ImageView iconView = convertView.findViewById(R.id.icon);
+ ResolveInfo activity = (ResolveInfo) getItem(position);
+ iconView.setImageDrawable(activity.loadIcon(packageManager));
+ // Set the title.
+ TextView titleView = convertView.findViewById(R.id.title);
+ titleView.setText(activity.loadLabel(packageManager));
+ // Highlight the default.
+ if (mShowDefaultActivity && position == 0 && mHighlightDefaultActivity) {
+ convertView.setActivated(true);
+ } else {
+ convertView.setActivated(false);
+ }
+ return convertView;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public int measureContentWidth() {
+ // The user may have specified some of the target not to be shown but we
+ // want to measure all of them since after expansion they should fit.
+ final int oldMaxActivityCount = mMaxActivityCount;
+ mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED;
+
+ int contentWidth = 0;
+ View itemView = null;
+
+ final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int count = getCount();
+
+ for (int i = 0; i < count; i++) {
+ itemView = getView(i, itemView, null);
+ itemView.measure(widthMeasureSpec, heightMeasureSpec);
+ contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth());
+ }
+
+ mMaxActivityCount = oldMaxActivityCount;
+
+ return contentWidth;
+ }
+
+ public void setMaxActivityCount(int maxActivityCount) {
+ if (mMaxActivityCount != maxActivityCount) {
+ mMaxActivityCount = maxActivityCount;
+ notifyDataSetChanged();
+ }
+ }
+
+ public ResolveInfo getDefaultActivity() {
+ return mDataModel.getDefaultActivity();
+ }
+
+ public void setShowFooterView(boolean showFooterView) {
+ if (mShowFooterView != showFooterView) {
+ mShowFooterView = showFooterView;
+ notifyDataSetChanged();
+ }
+ }
+
+ public int getActivityCount() {
+ return mDataModel.getActivityCount();
+ }
+
+ public int getHistorySize() {
+ return mDataModel.getHistorySize();
+ }
+
+ public ActivityChooserModel getDataModel() {
+ return mDataModel;
+ }
+
+ public void setShowDefaultActivity(boolean showDefaultActivity,
+ boolean highlightDefaultActivity) {
+ if (mShowDefaultActivity != showDefaultActivity
+ || mHighlightDefaultActivity != highlightDefaultActivity) {
+ mShowDefaultActivity = showDefaultActivity;
+ mHighlightDefaultActivity = highlightDefaultActivity;
+ notifyDataSetChanged();
+ }
+ }
+
+ public boolean getShowDefaultActivity() {
+ return mShowDefaultActivity;
+ }
+ }
+}
diff --git a/android/widget/Adapter.java b/android/widget/Adapter.java
new file mode 100644
index 0000000..4e463dd
--- /dev/null
+++ b/android/widget/Adapter.java
@@ -0,0 +1,168 @@
+/*
+ * 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.annotation.Nullable;
+import android.database.DataSetObserver;
+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);
+
+ /**
+ * Indicates 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.
+ * Heterogeneous lists can specify their number of view types, so that this View is
+ * always of the right type (see {@link #getViewTypeCount()} and
+ * {@link #getItemViewType(int)}).
+ * @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 the adapter is set on 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();
+
+ /**
+ * Gets a string representation of the adapter data that can help
+ * {@link android.service.autofill.AutofillService} autofill the view backed by the adapter.
+ *
+ * <p>
+ * It should only be set (i.e., non-{@code null} if the values do not represent PII
+ * (Personally Identifiable Information - sensitive data such as email addresses,
+ * credit card numbers, passwords, etc...). For
+ * example, it's ok to return a list of month names, but not a list of usernames. A good rule of
+ * thumb is that if the adapter data comes from static resources, such data is not PII - see
+ * {@link android.view.ViewStructure#setDataIsSensitive(boolean)} for more info.
+ *
+ * @return {@code null} by default, unless implementations override it.
+ */
+ default @Nullable CharSequence[] getAutofillOptions() {
+ return null;
+ }
+}
diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java
new file mode 100644
index 0000000..dd01251
--- /dev/null
+++ b/android/widget/AdapterView.java
@@ -0,0 +1,1307 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillManager;
+
+/**
+ * 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.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using AdapterView, read the
+ * <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a>
+ * developer guide.</p></div>
+ */
+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(category = "scrolling")
+ 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(category = "list")
+ 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(category = "list")
+ int mSelectedPosition = INVALID_POSITION;
+
+ /**
+ * The item id of the currently selected item.
+ */
+ long mSelectedRowId = INVALID_ROW_ID;
+
+ /**
+ * View to show if there are no items to show.
+ */
+ private View mEmptyView;
+
+ /**
+ * The number of items in the current adapter.
+ */
+ @ViewDebug.ExportedProperty(category = "list")
+ int mItemCount;
+
+ /**
+ * The number of items in the adapter before a data changed event occurred.
+ */
+ 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 int mDesiredFocusableState = FOCUSABLE_AUTO;
+ private boolean mDesiredFocusableInTouchModeState;
+
+ /** Lazily-constructed runnable for dispatching selection events. */
+ private SelectionNotifier mSelectionNotifier;
+
+ /** Selection notifier that's waiting for the next layout pass. */
+ private SelectionNotifier mPendingSelectionNotifier;
+
+ /**
+ * 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) {
+ this(context, null);
+ }
+
+ public AdapterView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AdapterView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AdapterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // If not explicitly specified this view is important for accessibility.
+ if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ mDesiredFocusableState = getFocusable();
+ if (mDesiredFocusableState == FOCUSABLE_AUTO) {
+ // Starts off without an adapter, so NOT_FOCUSABLE by default.
+ super.setFocusable(NOT_FOCUSABLE);
+ }
+ }
+
+ /**
+ * 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(@Nullable 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.
+ */
+ @Nullable
+ public final OnItemClickListener getOnItemClickListener() {
+ return mOnItemClickListener;
+ }
+
+ /**
+ * Call the OnItemClickListener, if it is defined. Performs all normal
+ * actions associated with clicking: reporting accessibility event, playing
+ * a sound, etc.
+ *
+ * @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) {
+ final boolean result;
+ if (mOnItemClickListener != null) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ mOnItemClickListener.onItemClick(this, view, position, id);
+ result = true;
+ } else {
+ result = false;
+ }
+
+ if (view != null) {
+ view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+ }
+ return result;
+ }
+
+ /**
+ * 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 {
+ /**
+ * <p>Callback method to be invoked when an item in this view has been
+ * selected. This callback is invoked only when the newly selected
+ * position is different from the previously selected position or if
+ * there was no selected item.</p>
+ *
+ * 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(@Nullable OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ @Nullable
+ public final OnItemSelectedListener getOnItemSelectedListener() {
+ return mOnItemSelectedListener;
+ }
+
+ /**
+ * Extra menu information provided to the
+ * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
+ * callback when a context menu is brought up for this AdapterView.
+ *
+ */
+ public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
+
+ public AdapterContextMenuInfo(View targetView, int position, long id) {
+ this.targetView = targetView;
+ this.position = position;
+ this.id = id;
+ }
+
+ /**
+ * The child view for which the context menu is being displayed. This
+ * will be one of the children of this AdapterView.
+ */
+ public View targetView;
+
+ /**
+ * The position in the adapter for which the context menu is being
+ * displayed.
+ */
+ public int position;
+
+ /**
+ * The row id of the item for which the context menu is being displayed.
+ */
+ public long id;
+ }
+
+ /**
+ * Returns the adapter currently associated with this widget.
+ *
+ * @return The adapter used to provide this view's content.
+ */
+ public abstract T getAdapter();
+
+ /**
+ * Sets the adapter that provides the data and the views to represent the data
+ * in this widget.
+ *
+ * @param adapter The adapter to use to create this view's content.
+ */
+ public abstract void setAdapter(T adapter);
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child) {
+ throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param index Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, int index) {
+ throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param params Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, LayoutParams params) {
+ throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ + "is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param index Ignored.
+ * @param params Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, int index, LayoutParams params) {
+ throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+ + "is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeView(View child) {
+ throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param index Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeViewAt(int index) {
+ throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeAllViews() {
+ throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mLayoutHeight = getHeight();
+ }
+
+ /**
+ * Return the position of the currently selected item within the adapter's data set
+ *
+ * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
+ */
+ @ViewDebug.CapturedViewProperty
+ public int getSelectedItemPosition() {
+ return mNextSelectedPosition;
+ }
+
+ /**
+ * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
+ * if nothing is selected.
+ */
+ @ViewDebug.CapturedViewProperty
+ public long getSelectedItemId() {
+ return mNextSelectedRowId;
+ }
+
+ /**
+ * @return The view corresponding to the currently selected item, or null
+ * if nothing is selected
+ */
+ public abstract View getSelectedView();
+
+ /**
+ * @return The data corresponding to the currently selected item, or
+ * null if there is nothing selected.
+ */
+ public Object getSelectedItem() {
+ T adapter = getAdapter();
+ int selection = getSelectedItemPosition();
+ if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
+ return adapter.getItem(selection);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return The number of items owned by the Adapter associated with this
+ * AdapterView. (This is the number of data items, which may be
+ * larger than the number of visible views.)
+ */
+ @ViewDebug.CapturedViewProperty
+ public int getCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Returns 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.
+ * <p>
+ * <strong>Note:</strong> The result of this method only reflects the
+ * position of the data bound to <var>view</var> during the most recent
+ * layout pass. If the adapter's data set has changed without a subsequent
+ * layout pass, the position returned by this method may not match the
+ * current position of the data within the adapter.
+ *
+ * @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()) != null && !v.equals(this)) {
+ listItem = v;
+ }
+ } catch (ClassCastException e) {
+ // We made it up to the window without find this list view
+ return INVALID_POSITION;
+ }
+
+ if (listItem != null) {
+ // Search the children for the list item
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i).equals(listItem)) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+
+ // Child not found!
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Returns the position within the adapter's data set for the first item
+ * displayed on screen.
+ *
+ * @return The position within the adapter's data set
+ */
+ public int getFirstVisiblePosition() {
+ return mFirstPosition;
+ }
+
+ /**
+ * Returns the position within the adapter's data set for the last item
+ * displayed on screen.
+ *
+ * @return The position within the adapter's data set
+ */
+ public int getLastVisiblePosition() {
+ return mFirstPosition + getChildCount() - 1;
+ }
+
+ /**
+ * Sets the currently selected item. To support accessibility subclasses that
+ * override this method must invoke the overriden super method first.
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ */
+ public abstract void setSelection(int position);
+
+ /**
+ * Sets the view to show if the adapter is empty
+ */
+ @android.view.RemotableViewMethod
+ public void setEmptyView(View emptyView) {
+ mEmptyView = emptyView;
+
+ // If not explicitly specified this view is important for accessibility.
+ if (emptyView != null
+ && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ 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
+ * called 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(@Focusable int focusable) {
+ final T adapter = getAdapter();
+ final boolean empty = adapter == null || adapter.getCount() == 0;
+
+ mDesiredFocusableState = focusable;
+ if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
+ mDesiredFocusableInTouchModeState = false;
+ }
+
+ super.setFocusable((!empty || isInFilterMode()) ? focusable : NOT_FOCUSABLE);
+ }
+
+ @Override
+ public void setFocusableInTouchMode(boolean focusable) {
+ final T adapter = getAdapter();
+ final boolean empty = adapter == null || adapter.getCount() == 0;
+
+ mDesiredFocusableInTouchModeState = focusable;
+ if (focusable) {
+ mDesiredFocusableState = FOCUSABLE;
+ }
+
+ 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 : NOT_FOCUSABLE);
+ 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;
+
+ checkFocus();
+ requestLayout();
+ }
+
+ public void clearSavedState() {
+ mInstanceState = null;
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeCallbacks(mSelectionNotifier);
+ }
+
+ private class SelectionNotifier implements Runnable {
+ public void run() {
+ mPendingSelectionNotifier = null;
+
+ if (mDataChanged && getViewRootImpl() != null
+ && getViewRootImpl().isLayoutRequested()) {
+ // Data has changed between when this SelectionNotifier was
+ // posted and now. Postpone the notification until the next
+ // layout is complete and we run checkSelectionChanged().
+ if (getAdapter() != null) {
+ mPendingSelectionNotifier = this;
+ }
+ } else {
+ dispatchOnItemSelected();
+ }
+ }
+ }
+
+ void selectionChanged() {
+ // We're about to post or run the selection notifier, so we don't need
+ // a pending notifier.
+ mPendingSelectionNotifier = null;
+
+ if (mOnItemSelectedListener != null
+ || AccessibilityManager.getInstance(mContext).isEnabled()) {
+ 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 accommodate
+ // new layout or invalidate requests.
+ if (mSelectionNotifier == null) {
+ mSelectionNotifier = new SelectionNotifier();
+ } else {
+ removeCallbacks(mSelectionNotifier);
+ }
+ post(mSelectionNotifier);
+ } else {
+ dispatchOnItemSelected();
+ }
+ }
+ // Always notify AutoFillManager - it will return right away if autofill is disabled.
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.notifyValueChanged(this);
+ }
+ }
+
+ private void dispatchOnItemSelected() {
+ fireOnSelected();
+ performAccessibilityActionsOnSelected();
+ }
+
+ private void fireOnSelected() {
+ if (mOnItemSelectedListener == null) {
+ return;
+ }
+ final int selection = getSelectedItemPosition();
+ if (selection >= 0) {
+ View v = getSelectedView();
+ mOnItemSelectedListener.onItemSelected(this, v, selection,
+ getAdapter().getItemId(selection));
+ } else {
+ mOnItemSelectedListener.onNothingSelected(this);
+ }
+ }
+
+ private void performAccessibilityActionsOnSelected() {
+ if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+ return;
+ }
+ final int position = getSelectedItemPosition();
+ if (position >= 0) {
+ // we fire selection events here not in View
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ View selectedView = getSelectedView();
+ if (selectedView != null && selectedView.getVisibility() == VISIBLE
+ && selectedView.dispatchPopulateAccessibilityEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+ if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+ // Add a record for ourselves as well.
+ AccessibilityEvent record = AccessibilityEvent.obtain();
+ onInitializeAccessibilityEvent(record);
+ // Populate with the text of the requesting child.
+ child.dispatchPopulateAccessibilityEvent(record);
+ event.appendRecord(record);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AdapterView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setScrollable(isScrollableForAccessibility());
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ info.setEnabled(selectedView.isEnabled());
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setScrollable(isScrollableForAccessibility());
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ event.setEnabled(selectedView.isEnabled());
+ }
+ event.setCurrentItemIndex(getSelectedItemPosition());
+ event.setFromIndex(getFirstVisiblePosition());
+ event.setToIndex(getLastVisiblePosition());
+ event.setItemCount(getCount());
+ }
+
+ private boolean isScrollableForAccessibility() {
+ T adapter = getAdapter();
+ if (adapter != null) {
+ final int itemCount = adapter.getCount();
+ return itemCount > 0
+ && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
+ }
+ return false;
+ }
+
+ @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();
+ }
+
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
+
+ /**
+ * Called after layout to determine whether the selection position needs to
+ * be updated. Also used to fire any pending selection events.
+ */
+ void checkSelectionChanged() {
+ if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
+ selectionChanged();
+ mOldSelectedPosition = mSelectedPosition;
+ mOldSelectedRowId = mSelectedRowId;
+ }
+
+ // If we have a pending selection notification -- and we won't if we
+ // just fired one in selectionChanged() -- run it now.
+ if (mPendingSelectionNotifier != null) {
+ mPendingSelectionNotifier.run();
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("scrolling:firstPosition", mFirstPosition);
+ encoder.addProperty("list:nextSelectedPosition", mNextSelectedPosition);
+ encoder.addProperty("list:nextSelectedRowId", mNextSelectedRowId);
+ encoder.addProperty("list:selectedPosition", mSelectedPosition);
+ encoder.addProperty("list:itemCount", mItemCount);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>It also sets the autofill options in the structure; when overridden, it should set it as
+ * well, either explicitly by calling {@link ViewStructure#setAutofillOptions(CharSequence[])}
+ * or implicitly by calling {@code super.onProvideAutofillStructure(structure, flags)}.
+ */
+ @Override
+ public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+ super.onProvideAutofillStructure(structure, flags);
+
+ final Adapter adapter = getAdapter();
+ if (adapter == null) return;
+
+ final CharSequence[] options = adapter.getAutofillOptions();
+ if (options != null) {
+ structure.setAutofillOptions(options);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/widget/AdapterViewAnimator.java b/android/widget/AdapterViewAnimator.java
new file mode 100644
index 0000000..6f29368
--- /dev/null
+++ b/android/widget/AdapterViewAnimator.java
@@ -0,0 +1,1099 @@
+/*
+ * Copyright (C) 2010 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.animation.AnimatorInflater;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.OnClickHandler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Base class for a {@link AdapterView} that will perform animations
+ * when switching between its views.
+ *
+ * @attr ref android.R.styleable#AdapterViewAnimator_inAnimation
+ * @attr ref android.R.styleable#AdapterViewAnimator_outAnimation
+ * @attr ref android.R.styleable#AdapterViewAnimator_animateFirstView
+ * @attr ref android.R.styleable#AdapterViewAnimator_loopViews
+ */
+public abstract class AdapterViewAnimator extends AdapterView<Adapter>
+ implements RemoteViewsAdapter.RemoteAdapterConnectionCallback, Advanceable {
+ private static final String TAG = "RemoteViewAnimator";
+
+ /**
+ * The index of the current child, which appears anywhere from the beginning
+ * to the end of the current set of children, as specified by {@link #mActiveOffset}
+ */
+ int mWhichChild = 0;
+
+ /**
+ * The index of the child to restore after the asynchronous connection from the
+ * RemoteViewsAdapter has been.
+ */
+ private int mRestoreWhichChild = -1;
+
+ /**
+ * Whether or not the first view(s) should be animated in
+ */
+ boolean mAnimateFirstTime = true;
+
+ /**
+ * Represents where the in the current window of
+ * views the current <code>mDisplayedChild</code> sits
+ */
+ int mActiveOffset = 0;
+
+ /**
+ * The number of views that the {@link AdapterViewAnimator} keeps as children at any
+ * given time (not counting views that are pending removal, see {@link #mPreviousViews}).
+ */
+ int mMaxNumActiveViews = 1;
+
+ /**
+ * Map of the children of the {@link AdapterViewAnimator}.
+ */
+ HashMap<Integer, ViewAndMetaData> mViewsMap = new HashMap<Integer, ViewAndMetaData>();
+
+ /**
+ * List of views pending removal from the {@link AdapterViewAnimator}
+ */
+ ArrayList<Integer> mPreviousViews;
+
+ /**
+ * The index, relative to the adapter, of the beginning of the window of views
+ */
+ int mCurrentWindowStart = 0;
+
+ /**
+ * The index, relative to the adapter, of the end of the window of views
+ */
+ int mCurrentWindowEnd = -1;
+
+ /**
+ * The same as {@link #mCurrentWindowStart}, except when the we have bounded
+ * {@link #mCurrentWindowStart} to be non-negative
+ */
+ int mCurrentWindowStartUnbounded = 0;
+
+ /**
+ * Listens for data changes from the adapter
+ */
+ AdapterDataSetObserver mDataSetObserver;
+
+ /**
+ * The {@link Adapter} for this {@link AdapterViewAnimator}
+ */
+ Adapter mAdapter;
+
+ /**
+ * The {@link RemoteViewsAdapter} for this {@link AdapterViewAnimator}
+ */
+ RemoteViewsAdapter mRemoteViewsAdapter;
+
+ /**
+ * The remote adapter containing the data to be displayed by this view to be set
+ */
+ boolean mDeferNotifyDataSetChanged = false;
+
+ /**
+ * Specifies whether this is the first time the animator is showing views
+ */
+ boolean mFirstTime = true;
+
+ /**
+ * Specifies if the animator should wrap from 0 to the end and vice versa
+ * or have hard boundaries at the beginning and end
+ */
+ boolean mLoopViews = true;
+
+ /**
+ * The width and height of some child, used as a size reference in-case our
+ * dimensions are unspecified by the parent.
+ */
+ int mReferenceChildWidth = -1;
+ int mReferenceChildHeight = -1;
+
+ /**
+ * In and out animations.
+ */
+ ObjectAnimator mInAnimation;
+ ObjectAnimator mOutAnimation;
+
+ /**
+ * Current touch state.
+ */
+ private int mTouchMode = TOUCH_MODE_NONE;
+
+ /**
+ * Private touch states.
+ */
+ static final int TOUCH_MODE_NONE = 0;
+ static final int TOUCH_MODE_DOWN_IN_CURRENT_VIEW = 1;
+ static final int TOUCH_MODE_HANDLED = 2;
+
+ private Runnable mPendingCheckForTap;
+
+ private static final int DEFAULT_ANIMATION_DURATION = 200;
+
+ public AdapterViewAnimator(Context context) {
+ this(context, null);
+ }
+
+ public AdapterViewAnimator(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AdapterViewAnimator(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, defStyleRes);
+ int resource = a.getResourceId(
+ com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0);
+ if (resource > 0) {
+ setInAnimation(context, resource);
+ } else {
+ setInAnimation(getDefaultInAnimation());
+ }
+
+ resource = a.getResourceId(com.android.internal.R.styleable.AdapterViewAnimator_outAnimation, 0);
+ if (resource > 0) {
+ setOutAnimation(context, resource);
+ } else {
+ setOutAnimation(getDefaultOutAnimation());
+ }
+
+ boolean flag = a.getBoolean(
+ com.android.internal.R.styleable.AdapterViewAnimator_animateFirstView, true);
+ setAnimateFirstView(flag);
+
+ mLoopViews = a.getBoolean(
+ com.android.internal.R.styleable.AdapterViewAnimator_loopViews, false);
+
+ a.recycle();
+
+ initViewAnimator();
+ }
+
+ /**
+ * Initialize this {@link AdapterViewAnimator}
+ */
+ private void initViewAnimator() {
+ mPreviousViews = new ArrayList<Integer>();
+ }
+
+ class ViewAndMetaData {
+ View view;
+ int relativeIndex;
+ int adapterPosition;
+ long itemId;
+
+ ViewAndMetaData(View view, int relativeIndex, int adapterPosition, long itemId) {
+ this.view = view;
+ this.relativeIndex = relativeIndex;
+ this.adapterPosition = adapterPosition;
+ this.itemId = itemId;
+ }
+ }
+
+ /**
+ * This method is used by subclasses to configure the animator to display the
+ * desired number of views, and specify the offset
+ *
+ * @param numVisibleViews The number of views the animator keeps in the {@link ViewGroup}
+ * @param activeOffset This parameter specifies where the current index ({@link #mWhichChild})
+ * sits within the window. For example if activeOffset is 1, and numVisibleViews is 3,
+ * and {@link #setDisplayedChild(int)} is called with 10, then the effective window will
+ * be the indexes 9, 10, and 11. In the same example, if activeOffset were 0, then the
+ * window would instead contain indexes 10, 11 and 12.
+ * @param shouldLoop If the animator is show view 0, and setPrevious() is called, do we
+ * we loop back to the end, or do we do nothing
+ */
+ void configureViewAnimator(int numVisibleViews, int activeOffset) {
+ if (activeOffset > numVisibleViews - 1) {
+ // Throw an exception here.
+ }
+ mMaxNumActiveViews = numVisibleViews;
+ mActiveOffset = activeOffset;
+ mPreviousViews.clear();
+ mViewsMap.clear();
+ removeAllViewsInLayout();
+ mCurrentWindowStart = 0;
+ mCurrentWindowEnd = -1;
+ }
+
+ /**
+ * This class should be overridden by subclasses to customize view transitions within
+ * the set of visible views
+ *
+ * @param fromIndex The relative index within the window that the view was in, -1 if it wasn't
+ * in the window
+ * @param toIndex The relative index within the window that the view is going to, -1 if it is
+ * being removed
+ * @param view The view that is being animated
+ */
+ void transformViewForTransition(int fromIndex, int toIndex, View view, boolean animate) {
+ if (fromIndex == -1) {
+ mInAnimation.setTarget(view);
+ mInAnimation.start();
+ } else if (toIndex == -1) {
+ mOutAnimation.setTarget(view);
+ mOutAnimation.start();
+ }
+ }
+
+ ObjectAnimator getDefaultInAnimation() {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 0.0f, 1.0f);
+ anim.setDuration(DEFAULT_ANIMATION_DURATION);
+ return anim;
+ }
+
+ ObjectAnimator getDefaultOutAnimation() {
+ ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 1.0f, 0.0f);
+ anim.setDuration(DEFAULT_ANIMATION_DURATION);
+ return anim;
+ }
+
+ /**
+ * Sets which child view will be displayed.
+ *
+ * @param whichChild the index of the child view to display
+ */
+ @android.view.RemotableViewMethod
+ public void setDisplayedChild(int whichChild) {
+ setDisplayedChild(whichChild, true);
+ }
+
+ private void setDisplayedChild(int whichChild, boolean animate) {
+ if (mAdapter != null) {
+ mWhichChild = whichChild;
+ if (whichChild >= getWindowSize()) {
+ mWhichChild = mLoopViews ? 0 : getWindowSize() - 1;
+ } else if (whichChild < 0) {
+ mWhichChild = mLoopViews ? getWindowSize() - 1 : 0;
+ }
+
+ boolean hasFocus = getFocusedChild() != null;
+ // This will clear old focus if we had it
+ showOnly(mWhichChild, animate);
+ if (hasFocus) {
+ // Try to retake focus if we had it
+ requestFocus(FOCUS_FORWARD);
+ }
+ }
+ }
+
+ /**
+ * To be overridden by subclasses. This method applies a view / index specific
+ * transform to the child view.
+ *
+ * @param child
+ * @param relativeIndex
+ */
+ void applyTransformForChildAtIndex(View child, int relativeIndex) {
+ }
+
+ /**
+ * 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);
+ }
+
+ int modulo(int pos, int size) {
+ if (size > 0) {
+ return (size + (pos % size)) % size;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the view at this index relative to the current window's start
+ *
+ * @param relativeIndex Position relative to the current window's start
+ * @return View at this index, null if the index is outside the bounds
+ */
+ View getViewAtRelativeIndex(int relativeIndex) {
+ if (relativeIndex >= 0 && relativeIndex <= getNumActiveViews() - 1 && mAdapter != null) {
+ int i = modulo(mCurrentWindowStartUnbounded + relativeIndex, getWindowSize());
+ if (mViewsMap.get(i) != null) {
+ return mViewsMap.get(i).view;
+ }
+ }
+ return null;
+ }
+
+ int getNumActiveViews() {
+ if (mAdapter != null) {
+ return Math.min(getCount() + 1, mMaxNumActiveViews);
+ } else {
+ return mMaxNumActiveViews;
+ }
+ }
+
+ int getWindowSize() {
+ if (mAdapter != null) {
+ int adapterCount = getCount();
+ if (adapterCount <= getNumActiveViews() && mLoopViews) {
+ return adapterCount*mMaxNumActiveViews;
+ } else {
+ return adapterCount;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ private ViewAndMetaData getMetaDataForChild(View child) {
+ for (ViewAndMetaData vm: mViewsMap.values()) {
+ if (vm.view == child) {
+ return vm;
+ }
+ }
+ return null;
+ }
+
+ LayoutParams createOrReuseLayoutParams(View v) {
+ final LayoutParams currentLp = v.getLayoutParams();
+ if (currentLp != null) {
+ return currentLp;
+ }
+ return new LayoutParams(0, 0);
+ }
+
+ void refreshChildren() {
+ if (mAdapter == null) return;
+ for (int i = mCurrentWindowStart; i <= mCurrentWindowEnd; i++) {
+ int index = modulo(i, getWindowSize());
+
+ int adapterCount = getCount();
+ // get the fresh child from the adapter
+ final View updatedChild = mAdapter.getView(modulo(i, adapterCount), null, this);
+
+ if (updatedChild.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ updatedChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ if (mViewsMap.containsKey(index)) {
+ final FrameLayout fl = (FrameLayout) mViewsMap.get(index).view;
+ // add the new child to the frame, if it exists
+ if (updatedChild != null) {
+ // flush out the old child
+ fl.removeAllViewsInLayout();
+ fl.addView(updatedChild);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method can be overridden so that subclasses can provide a custom frame in which their
+ * children can live. For example, StackView adds padding to its childrens' frames so as to
+ * accomodate for the highlight effect.
+ *
+ * @return The FrameLayout into which children can be placed.
+ */
+ FrameLayout getFrameForChild() {
+ return new FrameLayout(mContext);
+ }
+
+ /**
+ * Shows only the specified child. The other displays Views exit the screen,
+ * optionally with the with the {@link #getOutAnimation() out animation} and
+ * the specified child enters the screen, optionally with the
+ * {@link #getInAnimation() in animation}.
+ *
+ * @param childIndex The index of the child to be shown.
+ * @param animate Whether or not to use the in and out animations, defaults
+ * to true.
+ */
+ void showOnly(int childIndex, boolean animate) {
+ if (mAdapter == null) return;
+ final int adapterCount = getCount();
+ if (adapterCount == 0) return;
+
+ for (int i = 0; i < mPreviousViews.size(); i++) {
+ View viewToRemove = mViewsMap.get(mPreviousViews.get(i)).view;
+ mViewsMap.remove(mPreviousViews.get(i));
+ viewToRemove.clearAnimation();
+ if (viewToRemove instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) viewToRemove;
+ vg.removeAllViewsInLayout();
+ }
+ // applyTransformForChildAtIndex here just allows for any cleanup
+ // associated with this view that may need to be done by a subclass
+ applyTransformForChildAtIndex(viewToRemove, -1);
+
+ removeViewInLayout(viewToRemove);
+ }
+ mPreviousViews.clear();
+ int newWindowStartUnbounded = childIndex - mActiveOffset;
+ int newWindowEndUnbounded = newWindowStartUnbounded + getNumActiveViews() - 1;
+ int newWindowStart = Math.max(0, newWindowStartUnbounded);
+ int newWindowEnd = Math.min(adapterCount - 1, newWindowEndUnbounded);
+
+ if (mLoopViews) {
+ newWindowStart = newWindowStartUnbounded;
+ newWindowEnd = newWindowEndUnbounded;
+ }
+ int rangeStart = modulo(newWindowStart, getWindowSize());
+ int rangeEnd = modulo(newWindowEnd, getWindowSize());
+
+ boolean wrap = false;
+ if (rangeStart > rangeEnd) {
+ wrap = true;
+ }
+
+ // This section clears out any items that are in our active views list
+ // but are outside the effective bounds of our window (this is becomes an issue
+ // at the extremities of the list, eg. where newWindowStartUnbounded < 0 or
+ // newWindowEndUnbounded > adapterCount - 1
+ for (Integer index : mViewsMap.keySet()) {
+ boolean remove = false;
+ if (!wrap && (index < rangeStart || index > rangeEnd)) {
+ remove = true;
+ } else if (wrap && (index > rangeEnd && index < rangeStart)) {
+ remove = true;
+ }
+
+ if (remove) {
+ View previousView = mViewsMap.get(index).view;
+ int oldRelativeIndex = mViewsMap.get(index).relativeIndex;
+
+ mPreviousViews.add(index);
+ transformViewForTransition(oldRelativeIndex, -1, previousView, animate);
+ }
+ }
+
+ // If the window has changed
+ if (!(newWindowStart == mCurrentWindowStart && newWindowEnd == mCurrentWindowEnd &&
+ newWindowStartUnbounded == mCurrentWindowStartUnbounded)) {
+ // Run through the indices in the new range
+ for (int i = newWindowStart; i <= newWindowEnd; i++) {
+
+ int index = modulo(i, getWindowSize());
+ int oldRelativeIndex;
+ if (mViewsMap.containsKey(index)) {
+ oldRelativeIndex = mViewsMap.get(index).relativeIndex;
+ } else {
+ oldRelativeIndex = -1;
+ }
+ int newRelativeIndex = i - newWindowStartUnbounded;
+
+ // If this item is in the current window, great, we just need to apply
+ // the transform for it's new relative position in the window, and animate
+ // between it's current and new relative positions
+ boolean inOldRange = mViewsMap.containsKey(index) && !mPreviousViews.contains(index);
+
+ if (inOldRange) {
+ View view = mViewsMap.get(index).view;
+ mViewsMap.get(index).relativeIndex = newRelativeIndex;
+ applyTransformForChildAtIndex(view, newRelativeIndex);
+ transformViewForTransition(oldRelativeIndex, newRelativeIndex, view, animate);
+
+ // Otherwise this view is new to the window
+ } else {
+ // Get the new view from the adapter, add it and apply any transform / animation
+ final int adapterPosition = modulo(i, adapterCount);
+ View newView = mAdapter.getView(adapterPosition, null, this);
+ long itemId = mAdapter.getItemId(adapterPosition);
+
+ // We wrap the new view in a FrameLayout so as to respect the contract
+ // with the adapter, that is, that we don't modify this view directly
+ FrameLayout fl = getFrameForChild();
+
+ // If the view from the adapter is null, we still keep an empty frame in place
+ if (newView != null) {
+ fl.addView(newView);
+ }
+ mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex,
+ adapterPosition, itemId));
+ addChild(fl);
+ applyTransformForChildAtIndex(fl, newRelativeIndex);
+ transformViewForTransition(-1, newRelativeIndex, fl, animate);
+ }
+ mViewsMap.get(index).view.bringToFront();
+ }
+ mCurrentWindowStart = newWindowStart;
+ mCurrentWindowEnd = newWindowEnd;
+ mCurrentWindowStartUnbounded = newWindowStartUnbounded;
+ if (mRemoteViewsAdapter != null) {
+ int adapterStart = modulo(mCurrentWindowStart, adapterCount);
+ int adapterEnd = modulo(mCurrentWindowEnd, adapterCount);
+ mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd);
+ }
+ }
+ requestLayout();
+ invalidate();
+ }
+
+ private void addChild(View child) {
+ addViewInLayout(child, -1, createOrReuseLayoutParams(child));
+
+ // This code is used to obtain a reference width and height of a child in case we need
+ // to decide our own size. TODO: Do we want to update the size of the child that we're
+ // using for reference size? If so, when?
+ if (mReferenceChildWidth == -1 || mReferenceChildHeight == -1) {
+ int measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ child.measure(measureSpec, measureSpec);
+ mReferenceChildWidth = child.getMeasuredWidth();
+ mReferenceChildHeight = child.getMeasuredHeight();
+ }
+ }
+
+ void showTapFeedback(View v) {
+ v.setPressed(true);
+ }
+
+ void hideTapFeedback(View v) {
+ v.setPressed(false);
+ }
+
+ void cancelHandleClick() {
+ View v = getCurrentView();
+ if (v != null) {
+ hideTapFeedback(v);
+ }
+ mTouchMode = TOUCH_MODE_NONE;
+ }
+
+ final class CheckForTap implements Runnable {
+ public void run() {
+ if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) {
+ View v = getCurrentView();
+ showTapFeedback(v);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ boolean handled = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ View v = getCurrentView();
+ if (v != null) {
+ if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
+ if (mPendingCheckForTap == null) {
+ mPendingCheckForTap = new CheckForTap();
+ }
+ mTouchMode = TOUCH_MODE_DOWN_IN_CURRENT_VIEW;
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: break;
+ case MotionEvent.ACTION_POINTER_UP: break;
+ case MotionEvent.ACTION_UP: {
+ if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) {
+ final View v = getCurrentView();
+ final ViewAndMetaData viewData = getMetaDataForChild(v);
+ if (v != null) {
+ if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForTap);
+ }
+ showTapFeedback(v);
+ postDelayed(new Runnable() {
+ public void run() {
+ hideTapFeedback(v);
+ post(new Runnable() {
+ public void run() {
+ if (viewData != null) {
+ performItemClick(v, viewData.adapterPosition,
+ viewData.itemId);
+ } else {
+ performItemClick(v, 0, 0);
+ }
+ }
+ });
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+ handled = true;
+ }
+ }
+ }
+ mTouchMode = TOUCH_MODE_NONE;
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ View v = getCurrentView();
+ if (v != null) {
+ hideTapFeedback(v);
+ }
+ mTouchMode = TOUCH_MODE_NONE;
+ }
+ }
+ return handled;
+ }
+
+ private void measureChildren() {
+ final int count = getChildCount();
+ final int childWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
+ final int childHeight = getMeasuredHeight() - mPaddingTop - mPaddingBottom;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
+
+ // We need to deal with the case where our parent hasn't told us how
+ // big we should be. In this case we try to use the desired size of the first
+ // child added.
+ if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ heightSpecSize = haveChildRefSize ? mReferenceChildHeight + mPaddingTop +
+ mPaddingBottom : 0;
+ } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+ if (haveChildRefSize) {
+ int height = mReferenceChildHeight + mPaddingTop + mPaddingBottom;
+ if (height > heightSpecSize) {
+ heightSpecSize |= MEASURED_STATE_TOO_SMALL;
+ } else {
+ heightSpecSize = height;
+ }
+ }
+ }
+
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
+ widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft +
+ mPaddingRight : 0;
+ } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+ if (haveChildRefSize) {
+ int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
+ if (width > widthSpecSize) {
+ widthSpecSize |= MEASURED_STATE_TOO_SMALL;
+ } else {
+ widthSpecSize = width;
+ }
+ }
+ }
+
+ setMeasuredDimension(widthSpecSize, heightSpecSize);
+ measureChildren();
+ }
+
+ void checkForAndHandleDataChanged() {
+ boolean dataChanged = mDataChanged;
+ if (dataChanged) {
+ post(new Runnable() {
+ public void run() {
+ handleDataChanged();
+ // if the data changes, mWhichChild might be out of the bounds of the adapter
+ // in this case, we reset mWhichChild to the beginning
+ if (mWhichChild >= getWindowSize()) {
+ mWhichChild = 0;
+
+ showOnly(mWhichChild, false);
+ } else if (mOldItemCount != getCount()) {
+ showOnly(mWhichChild, false);
+ }
+ refreshChildren();
+ requestLayout();
+ }
+ });
+ }
+ mDataChanged = false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ checkForAndHandleDataChanged();
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ int childRight = mPaddingLeft + child.getMeasuredWidth();
+ int childBottom = mPaddingTop + child.getMeasuredHeight();
+
+ child.layout(mPaddingLeft, mPaddingTop, childRight, childBottom);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ int whichChild;
+
+ /**
+ * Constructor called from {@link AdapterViewAnimator#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState, int whichChild) {
+ super(superState);
+ this.whichChild = whichChild;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ this.whichChild = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(this.whichChild);
+ }
+
+ @Override
+ public String toString() {
+ return "AdapterViewAnimator.SavedState{ whichChild = " + this.whichChild + " }";
+ }
+
+ 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();
+ if (mRemoteViewsAdapter != null) {
+ mRemoteViewsAdapter.saveRemoteViewsCache();
+ }
+ return new SavedState(superState, mWhichChild);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ // Here we set mWhichChild in addition to setDisplayedChild
+ // We do the former in case mAdapter is null, and hence setDisplayedChild won't
+ // set mWhichChild
+ mWhichChild = ss.whichChild;
+
+ // When using RemoteAdapters, the async connection process can lead to
+ // onRestoreInstanceState to be called before setAdapter(), so we need to save the previous
+ // values to restore the list position after we connect, and can skip setting the displayed
+ // child until then.
+ if (mRemoteViewsAdapter != null && mAdapter == null) {
+ mRestoreWhichChild = mWhichChild;
+ } else {
+ setDisplayedChild(mWhichChild, false);
+ }
+ }
+
+ /**
+ * Returns the View corresponding to the currently displayed child.
+ *
+ * @return The View currently displayed.
+ *
+ * @see #getDisplayedChild()
+ */
+ public View getCurrentView() {
+ return getViewAtRelativeIndex(mActiveOffset);
+ }
+
+ /**
+ * 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.animation.ObjectAnimator)
+ * @see #setInAnimation(android.content.Context, int)
+ */
+ public ObjectAnimator 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(ObjectAnimator 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.animation.ObjectAnimator)
+ * @see #setOutAnimation(android.content.Context, int)
+ */
+ public ObjectAnimator 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(ObjectAnimator 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.animation.ObjectAnimator)
+ */
+ public void setInAnimation(Context context, int resourceID) {
+ setInAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(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.animation.ObjectAnimator)
+ */
+ public void setOutAnimation(Context context, int resourceID) {
+ setOutAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(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();
+ }
+
+ @Override
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ if (mAdapter != null && mDataSetObserver != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ mAdapter = adapter;
+ checkFocus();
+
+ if (mAdapter != null) {
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+ mItemCount = mAdapter.getCount();
+ }
+ setFocusable(true);
+ mWhichChild = 0;
+ showOnly(mWhichChild, false);
+ }
+
+ /**
+ * Sets up this AdapterViewAnimator to use a remote views adapter which connects to a
+ * RemoteViewsService through the specified intent.
+ *
+ * @param intent the intent used to identify the RemoteViewsService for the adapter to
+ * connect to.
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+ public void setRemoteViewsAdapter(Intent intent) {
+ setRemoteViewsAdapter(intent, false);
+ }
+
+ /** @hide **/
+ public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+ return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+ }
+
+ /** @hide **/
+ @Override
+ public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
+ // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+ // service handling the specified intent.
+ if (mRemoteViewsAdapter != null) {
+ Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+ Intent.FilterComparison fcOld = new Intent.FilterComparison(
+ mRemoteViewsAdapter.getRemoteViewsServiceIntent());
+ if (fcNew.equals(fcOld)) {
+ return;
+ }
+ }
+ mDeferNotifyDataSetChanged = false;
+ // Otherwise, create a new RemoteViewsAdapter for binding
+ mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
+ if (mRemoteViewsAdapter.isDataReady()) {
+ setAdapter(mRemoteViewsAdapter);
+ }
+ }
+
+ /**
+ * Sets up the onClickHandler to be used by the RemoteViewsAdapter when inflating RemoteViews
+ *
+ * @param handler The OnClickHandler to use when inflating RemoteViews.
+ *
+ * @hide
+ */
+ public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+ // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+ // service handling the specified intent.
+ if (mRemoteViewsAdapter != null) {
+ mRemoteViewsAdapter.setRemoteViewsOnClickHandler(handler);
+ }
+ }
+
+ @Override
+ public void setSelection(int position) {
+ setDisplayedChild(position);
+ }
+
+ @Override
+ public View getSelectedView() {
+ return getViewAtRelativeIndex(mActiveOffset);
+ }
+
+ /**
+ * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+ * connected yet.
+ */
+ public void deferNotifyDataSetChanged() {
+ mDeferNotifyDataSetChanged = true;
+ }
+
+ /**
+ * Called back when the adapter connects to the RemoteViewsService.
+ */
+ public boolean onRemoteAdapterConnected() {
+ if (mRemoteViewsAdapter != mAdapter) {
+ setAdapter(mRemoteViewsAdapter);
+
+ if (mDeferNotifyDataSetChanged) {
+ mRemoteViewsAdapter.notifyDataSetChanged();
+ mDeferNotifyDataSetChanged = false;
+ }
+
+ // Restore the previous position (see onRestoreInstanceState)
+ if (mRestoreWhichChild > -1) {
+ setDisplayedChild(mRestoreWhichChild, false);
+ mRestoreWhichChild = -1;
+ }
+ return false;
+ } else if (mRemoteViewsAdapter != null) {
+ mRemoteViewsAdapter.superNotifyDataSetChanged();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called back when the adapter disconnects from the RemoteViewsService.
+ */
+ public void onRemoteAdapterDisconnected() {
+ // If the remote adapter disconnects, we keep it around
+ // since the currently displayed items are still cached.
+ // Further, we want the service to eventually reconnect
+ // when necessary, as triggered by this view requesting
+ // items from the Adapter.
+ }
+
+ /**
+ * Called by an {@link android.appwidget.AppWidgetHost} in order to advance the current view when
+ * it is being used within an app widget.
+ */
+ public void advance() {
+ showNext();
+ }
+
+ /**
+ * Called by an {@link android.appwidget.AppWidgetHost} to indicate that it will be
+ * automatically advancing the views of this {@link AdapterViewAnimator} by calling
+ * {@link AdapterViewAnimator#advance()} at some point in the future. This allows subclasses to
+ * perform any required setup, for example, to stop automatically advancing their children.
+ */
+ public void fyiWillBeAdvancedByHostKThx() {
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AdapterViewAnimator.class.getName();
+ }
+}
diff --git a/android/widget/AdapterViewFlipper.java b/android/widget/AdapterViewFlipper.java
new file mode 100644
index 0000000..18d7470
--- /dev/null
+++ b/android/widget/AdapterViewFlipper.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2010 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.RemotableViewMethod;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * 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.
+ *
+ * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+ * @attr ref android.R.styleable#AdapterViewFlipper_autoStart
+ */
+@RemoteView
+public class AdapterViewFlipper extends AdapterViewAnimator {
+ private static final String TAG = "ViewFlipper";
+ private static final boolean LOGD = false;
+
+ private static final int DEFAULT_INTERVAL = 10000;
+
+ private int mFlipInterval = DEFAULT_INTERVAL;
+ private boolean mAutoStart = false;
+
+ private boolean mRunning = false;
+ private boolean mStarted = false;
+ private boolean mVisible = false;
+ private boolean mUserPresent = true;
+ private boolean mAdvancedByHost = false;
+
+ public AdapterViewFlipper(Context context) {
+ super(context);
+ }
+
+ public AdapterViewFlipper(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AdapterViewFlipper(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AdapterViewFlipper(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.AdapterViewFlipper, defStyleAttr, defStyleRes);
+ mFlipInterval = a.getInt(
+ com.android.internal.R.styleable.AdapterViewFlipper_flipInterval, DEFAULT_INTERVAL);
+ mAutoStart = a.getBoolean(
+ com.android.internal.R.styleable.AdapterViewFlipper_autoStart, false);
+
+ // A view flipper should cycle through the views
+ mLoopViews = true;
+
+ a.recycle();
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ mUserPresent = false;
+ updateRunning();
+ } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+ mUserPresent = true;
+ updateRunning(false);
+ }
+ }
+ };
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // Listen for broadcasts related to user-presence
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+
+ // OK, this is gross but needed. This class is supported by the
+ // remote views machanism and as a part of that the remote views
+ // can be inflated by a context for another user without the app
+ // having interact users permission - just for loading resources.
+ // For exmaple, when adding widgets from a user profile to the
+ // home screen. Therefore, we register the receiver as the current
+ // user not the one the context is for.
+ getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
+ filter, null, getHandler());
+
+
+ if (mAutoStart) {
+ // Automatically start when requested
+ startFlipping();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mVisible = false;
+
+ getContext().unregisterReceiver(mReceiver);
+ updateRunning();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mVisible = (visibility == VISIBLE);
+ updateRunning(false);
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ super.setAdapter(adapter);
+ updateRunning();
+ }
+
+ /**
+ * Returns the flip interval, in milliseconds.
+ *
+ * @return the flip interval in milliseconds
+ *
+ * @see #setFlipInterval(int)
+ *
+ * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+ */
+ public int getFlipInterval() {
+ return mFlipInterval;
+ }
+
+ /**
+ * How long to wait before flipping to the next view.
+ *
+ * @param flipInterval flip interval in milliseconds
+ *
+ * @see #getFlipInterval()
+ *
+ * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+ */
+ public void setFlipInterval(int flipInterval) {
+ mFlipInterval = flipInterval;
+ }
+
+ /**
+ * Start a timer to cycle through child views
+ */
+ public void startFlipping() {
+ mStarted = true;
+ updateRunning();
+ }
+
+ /**
+ * No more flips
+ */
+ public void stopFlipping() {
+ mStarted = false;
+ updateRunning();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RemotableViewMethod
+ public void showNext() {
+ // if the flipper is currently flipping automatically, and showNext() is called
+ // we should we should make sure to reset the timer
+ if (mRunning) {
+ removeCallbacks(mFlipRunnable);
+ postDelayed(mFlipRunnable, mFlipInterval);
+ }
+ super.showNext();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RemotableViewMethod
+ public void showPrevious() {
+ // if the flipper is currently flipping automatically, and showPrevious() is called
+ // we should we should make sure to reset the timer
+ if (mRunning) {
+ removeCallbacks(mFlipRunnable);
+ postDelayed(mFlipRunnable, mFlipInterval);
+ }
+ super.showPrevious();
+ }
+
+ /**
+ * Internal method to start or stop dispatching flip {@link Message} based
+ * on {@link #mRunning} and {@link #mVisible} state.
+ */
+ private void updateRunning() {
+ // by default when we update running, we want the
+ // current view to animate in
+ updateRunning(true);
+ }
+
+ /**
+ * Internal method to start or stop dispatching flip {@link Message} based
+ * on {@link #mRunning} and {@link #mVisible} state.
+ *
+ * @param flipNow Determines whether or not to execute the animation now, in
+ * addition to queuing future flips. If omitted, defaults to
+ * true.
+ */
+ private void updateRunning(boolean flipNow) {
+ boolean running = !mAdvancedByHost && mVisible && mStarted && mUserPresent
+ && mAdapter != null;
+ if (running != mRunning) {
+ if (running) {
+ showOnly(mWhichChild, flipNow);
+ postDelayed(mFlipRunnable, mFlipInterval);
+ } else {
+ removeCallbacks(mFlipRunnable);
+ }
+ mRunning = running;
+ }
+ if (LOGD) {
+ Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+ + ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
+ }
+ }
+
+ /**
+ * Returns true if the child views are flipping.
+ */
+ public boolean isFlipping() {
+ return mStarted;
+ }
+
+ /**
+ * Set if this view automatically calls {@link #startFlipping()} when it
+ * becomes attached to a window.
+ */
+ public void setAutoStart(boolean autoStart) {
+ mAutoStart = autoStart;
+ }
+
+ /**
+ * Returns true if this view automatically calls {@link #startFlipping()}
+ * when it becomes attached to a window.
+ */
+ public boolean isAutoStart() {
+ return mAutoStart;
+ }
+
+ private final Runnable mFlipRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mRunning) {
+ showNext();
+ }
+ }
+ };
+
+ /**
+ * Called by an {@link android.appwidget.AppWidgetHost} to indicate that it will be
+ * automatically advancing the views of this {@link AdapterViewFlipper} by calling
+ * {@link AdapterViewFlipper#advance()} at some point in the future. This allows
+ * {@link AdapterViewFlipper} to prepare by no longer Advancing its children.
+ */
+ @Override
+ public void fyiWillBeAdvancedByHostKThx() {
+ mAdvancedByHost = true;
+ updateRunning(false);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return AdapterViewFlipper.class.getName();
+ }
+}
diff --git a/android/widget/Advanceable.java b/android/widget/Advanceable.java
new file mode 100644
index 0000000..ad8e101
--- /dev/null
+++ b/android/widget/Advanceable.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2010 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;
+
+/**
+ * This interface can be implemented by any collection-type view which has a notion of
+ * progressing through its set of children. The interface exists to give AppWidgetHosts a way of
+ * taking responsibility for automatically advancing such collections.
+ *
+ */
+public interface Advanceable {
+
+ /**
+ * Advances this collection, eg. shows the next view.
+ */
+ public void advance();
+
+ /**
+ * Called by the AppWidgetHost once before it begins to call advance(), allowing the
+ * collection to do any required setup.
+ */
+ public void fyiWillBeAdvancedByHostKThx();
+}
diff --git a/android/widget/AlphabetIndexer.java b/android/widget/AlphabetIndexer.java
new file mode 100644
index 0000000..59b2c2a
--- /dev/null
+++ b/android/widget/AlphabetIndexer.java
@@ -0,0 +1,289 @@
+/*
+ * 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.database.Cursor;
+import android.database.DataSetObserver;
+import android.util.SparseIntArray;
+
+/**
+ * A helper class for adapters that implement the SectionIndexer interface.
+ * If the items in the adapter are sorted by simple alphabet-based sorting, then
+ * this class provides a way to do fast indexing of large lists using binary search.
+ * It caches the indices that have been determined through the binary search and also
+ * invalidates the cache if changes occur in the cursor.
+ * <p/>
+ * Your adapter is responsible for updating the cursor by calling {@link #setCursor} if the
+ * cursor changes. {@link #getPositionForSection} method does the binary search for the starting
+ * index of a given section (alphabet).
+ */
+public class AlphabetIndexer extends DataSetObserver implements SectionIndexer {
+
+ /**
+ * Cursor that is used by the adapter of the list view.
+ */
+ protected Cursor mDataCursor;
+
+ /**
+ * The index of the cursor column that this list is sorted on.
+ */
+ protected int mColumnIndex;
+
+ /**
+ * The string of characters that make up the indexing sections.
+ */
+ protected CharSequence mAlphabet;
+
+ /**
+ * Cached length of the alphabet array.
+ */
+ private int mAlphabetLength;
+
+ /**
+ * This contains a cache of the computed indices so far. It will get reset whenever
+ * the dataset changes or the cursor changes.
+ */
+ private SparseIntArray mAlphaMap;
+
+ /**
+ * Use a collator to compare strings in a localized manner.
+ */
+ private java.text.Collator mCollator;
+
+ /**
+ * The section array converted from the alphabet string.
+ */
+ private String[] mAlphabetArray;
+
+ /**
+ * Constructs the indexer.
+ * @param cursor the cursor containing the data set
+ * @param sortedColumnIndex the column number in the cursor that is sorted
+ * alphabetically
+ * @param alphabet string containing the alphabet, with space as the first character.
+ * For example, use the string " ABCDEFGHIJKLMNOPQRSTUVWXYZ" for English indexing.
+ * The characters must be uppercase and be sorted in ascii/unicode order. Basically
+ * characters in the alphabet will show up as preview letters.
+ */
+ public AlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet) {
+ mDataCursor = cursor;
+ mColumnIndex = sortedColumnIndex;
+ mAlphabet = alphabet;
+ mAlphabetLength = alphabet.length();
+ mAlphabetArray = new String[mAlphabetLength];
+ for (int i = 0; i < mAlphabetLength; i++) {
+ mAlphabetArray[i] = Character.toString(mAlphabet.charAt(i));
+ }
+ mAlphaMap = new SparseIntArray(mAlphabetLength);
+ if (cursor != null) {
+ cursor.registerDataSetObserver(this);
+ }
+ // Get a Collator for the current locale for string comparisons.
+ mCollator = java.text.Collator.getInstance();
+ mCollator.setStrength(java.text.Collator.PRIMARY);
+ }
+
+ /**
+ * Returns the section array constructed from the alphabet provided in the constructor.
+ * @return the section array
+ */
+ public Object[] getSections() {
+ return mAlphabetArray;
+ }
+
+ /**
+ * Sets a new cursor as the data set and resets the cache of indices.
+ * @param cursor the new cursor to use as the data set
+ */
+ public void setCursor(Cursor cursor) {
+ if (mDataCursor != null) {
+ mDataCursor.unregisterDataSetObserver(this);
+ }
+ mDataCursor = cursor;
+ if (cursor != null) {
+ mDataCursor.registerDataSetObserver(this);
+ }
+ mAlphaMap.clear();
+ }
+
+ /**
+ * Default implementation compares the first character of word with letter.
+ */
+ protected int compare(String word, String letter) {
+ final String firstLetter;
+ if (word.length() == 0) {
+ firstLetter = " ";
+ } else {
+ firstLetter = word.substring(0, 1);
+ }
+
+ return mCollator.compare(firstLetter, letter);
+ }
+
+ /**
+ * Performs a binary search or cache lookup to find the first row that
+ * matches a given section's starting letter.
+ * @param sectionIndex the section to search for
+ * @return the row index of the first occurrence, or the nearest next letter.
+ * For instance, if searching for "T" and no "T" is found, then the first
+ * row starting with "U" or any higher letter is returned. If there is no
+ * data following "T" at all, then the list size is returned.
+ */
+ public int getPositionForSection(int sectionIndex) {
+ final SparseIntArray alphaMap = mAlphaMap;
+ final Cursor cursor = mDataCursor;
+
+ if (cursor == null || mAlphabet == null) {
+ return 0;
+ }
+
+ // Check bounds
+ if (sectionIndex <= 0) {
+ return 0;
+ }
+ if (sectionIndex >= mAlphabetLength) {
+ sectionIndex = mAlphabetLength - 1;
+ }
+
+ int savedCursorPos = cursor.getPosition();
+
+ int count = cursor.getCount();
+ int start = 0;
+ int end = count;
+ int pos;
+
+ char letter = mAlphabet.charAt(sectionIndex);
+ String targetLetter = Character.toString(letter);
+ int key = letter;
+ // Check map
+ if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) {
+ // Is it approximate? Using negative value to indicate that it's
+ // an approximation and positive value when it is the accurate
+ // position.
+ if (pos < 0) {
+ pos = -pos;
+ end = pos;
+ } else {
+ // Not approximate, this is the confirmed start of section, return it
+ return pos;
+ }
+ }
+
+ // Do we have the position of the previous section?
+ if (sectionIndex > 0) {
+ int prevLetter =
+ mAlphabet.charAt(sectionIndex - 1);
+ int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE);
+ if (prevLetterPos != Integer.MIN_VALUE) {
+ start = Math.abs(prevLetterPos);
+ }
+ }
+
+ // Now that we have a possibly optimized start and end, let's binary search
+
+ pos = (end + start) / 2;
+
+ while (pos < end) {
+ // Get letter at pos
+ cursor.moveToPosition(pos);
+ String curName = cursor.getString(mColumnIndex);
+ if (curName == null) {
+ if (pos == 0) {
+ break;
+ } else {
+ pos--;
+ continue;
+ }
+ }
+ int diff = compare(curName, targetLetter);
+ if (diff != 0) {
+ // TODO: Commenting out approximation code because it doesn't work for certain
+ // lists with custom comparators
+ // Enter approximation in hash if a better solution doesn't exist
+ // String startingLetter = Character.toString(getFirstLetter(curName));
+ // int startingLetterKey = startingLetter.charAt(0);
+ // int curPos = alphaMap.get(startingLetterKey, Integer.MIN_VALUE);
+ // if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) {
+ // Negative pos indicates that it is an approximation
+ // alphaMap.put(startingLetterKey, -pos);
+ // }
+ // if (mCollator.compare(startingLetter, targetLetter) < 0) {
+ if (diff < 0) {
+ start = pos + 1;
+ if (start >= count) {
+ pos = count;
+ break;
+ }
+ } else {
+ end = pos;
+ }
+ } else {
+ // They're the same, but that doesn't mean it's the start
+ if (start == pos) {
+ // This is it
+ break;
+ } else {
+ // Need to go further lower to find the starting row
+ end = pos;
+ }
+ }
+ pos = (start + end) / 2;
+ }
+ alphaMap.put(key, pos);
+ cursor.moveToPosition(savedCursorPos);
+ return pos;
+ }
+
+ /**
+ * Returns the section index for a given position in the list by querying the item
+ * and comparing it with all items in the section array.
+ */
+ public int getSectionForPosition(int position) {
+ int savedCursorPos = mDataCursor.getPosition();
+ mDataCursor.moveToPosition(position);
+ String curName = mDataCursor.getString(mColumnIndex);
+ mDataCursor.moveToPosition(savedCursorPos);
+ // Linear search, as there are only a few items in the section index
+ // Could speed this up later if it actually gets used.
+ for (int i = 0; i < mAlphabetLength; i++) {
+ char letter = mAlphabet.charAt(i);
+ String targetLetter = Character.toString(letter);
+ if (compare(curName, targetLetter) == 0) {
+ return i;
+ }
+ }
+ return 0; // Don't recognize the letter - falls under zero'th section
+ }
+
+ /*
+ * @hide
+ */
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ mAlphaMap.clear();
+ }
+
+ /*
+ * @hide
+ */
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ mAlphaMap.clear();
+ }
+}
diff --git a/android/widget/AnalogClock.java b/android/widget/AnalogClock.java
new file mode 100644
index 0000000..bde5f7f
--- /dev/null
+++ b/android/widget/AnalogClock.java
@@ -0,0 +1,271 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RemoteViews.RemoteView;
+
+import java.util.TimeZone;
+
+/**
+ * This widget display an analogic clock with two hands for hours and
+ * minutes.
+ *
+ * @attr ref android.R.styleable#AnalogClock_dial
+ * @attr ref android.R.styleable#AnalogClock_hand_hour
+ * @attr ref android.R.styleable#AnalogClock_hand_minute
+ * @deprecated This widget is no longer supported.
+ */
+@RemoteView
+@Deprecated
+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 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final Resources r = context.getResources();
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes);
+
+ mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial);
+ if (mDial == null) {
+ mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial);
+ }
+
+ mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour);
+ if (mHourHand == null) {
+ mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour);
+ }
+
+ mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute);
+ if (mMinuteHand == null) {
+ mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute);
+ }
+
+ mCalendar = new Time();
+
+ mDialWidth = mDial.getIntrinsicWidth();
+ mDialHeight = mDial.getIntrinsicHeight();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ 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);
+
+ // OK, this is gross but needed. This class is supported by the
+ // remote views machanism and as a part of that the remote views
+ // can be inflated by a context for another user without the app
+ // having interact users permission - just for loading resources.
+ // For exmaple, when adding widgets from a user profile to the
+ // home screen. Therefore, we register the receiver as the current
+ // user not the one the context is for.
+ getContext().registerReceiverAsUser(mIntentReceiver,
+ android.os.Process.myUserHandle(), filter, null, getHandler());
+ }
+
+ // NOTE: It's safe to do these after registering the receiver since the receiver always runs
+ // in the main thread, therefore the receiver can't run before this method returns.
+
+ // The time zone may have changed while the receiver wasn't registered, so update the Time
+ mCalendar = new Time();
+
+ // Make sure we update to the current time
+ onTimeChanged();
+ }
+
+ @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(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0),
+ resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0));
+ }
+
+ @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() {
+ mCalendar.setToNow();
+
+ int hour = mCalendar.hour;
+ int minute = mCalendar.minute;
+ int second = mCalendar.second;
+
+ mMinutes = minute + second / 60.0f;
+ mHour = hour + mMinutes / 60.0f;
+ mChanged = true;
+
+ updateContentDescription(mCalendar);
+ }
+
+ 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());
+ }
+
+ onTimeChanged();
+
+ invalidate();
+ }
+ };
+
+ private void updateContentDescription(Time time) {
+ final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR;
+ String contentDescription = DateUtils.formatDateTime(mContext,
+ time.toMillis(false), flags);
+ setContentDescription(contentDescription);
+ }
+}
diff --git a/android/widget/AppSecurityPermissions.java b/android/widget/AppSecurityPermissions.java
new file mode 100644
index 0000000..6df76fa
--- /dev/null
+++ b/android/widget/AppSecurityPermissions.java
@@ -0,0 +1,639 @@
+/*
+**
+** 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 android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.UserHandle;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+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 {
+
+ public static final int WHICH_NEW = 1<<2;
+ public static final int WHICH_ALL = 0xffff;
+
+ private final static String TAG = "AppSecurityPermissions";
+ private final static boolean localLOGV = false;
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+ private final PackageManager mPm;
+ private final Map<String, MyPermissionGroupInfo> mPermGroups
+ = new HashMap<String, MyPermissionGroupInfo>();
+ private final List<MyPermissionGroupInfo> mPermGroupsList
+ = new ArrayList<MyPermissionGroupInfo>();
+ private final PermissionGroupInfoComparator mPermGroupComparator =
+ new PermissionGroupInfoComparator();
+ private final PermissionInfoComparator mPermComparator = new PermissionInfoComparator();
+ private final List<MyPermissionInfo> mPermsList = new ArrayList<MyPermissionInfo>();
+ private final CharSequence mNewPermPrefix;
+ private String mPackageName;
+
+ /** @hide */
+ static class MyPermissionGroupInfo extends PermissionGroupInfo {
+ CharSequence mLabel;
+
+ final ArrayList<MyPermissionInfo> mNewPermissions = new ArrayList<MyPermissionInfo>();
+ final ArrayList<MyPermissionInfo> mAllPermissions = new ArrayList<MyPermissionInfo>();
+
+ MyPermissionGroupInfo(PermissionInfo perm) {
+ name = perm.packageName;
+ packageName = perm.packageName;
+ }
+
+ MyPermissionGroupInfo(PermissionGroupInfo info) {
+ super(info);
+ }
+
+ public Drawable loadGroupIcon(Context context, PackageManager pm) {
+ if (icon != 0) {
+ return loadUnbadgedIcon(pm);
+ } else {
+ return context.getDrawable(R.drawable.ic_perm_device_info);
+ }
+ }
+ }
+
+ /** @hide */
+ private static class MyPermissionInfo extends PermissionInfo {
+ CharSequence mLabel;
+
+ /**
+ * PackageInfo.requestedPermissionsFlags for the new package being installed.
+ */
+ int mNewReqFlags;
+
+ /**
+ * PackageInfo.requestedPermissionsFlags for the currently installed
+ * package, if it is installed.
+ */
+ int mExistingReqFlags;
+
+ /**
+ * True if this should be considered a new permission.
+ */
+ boolean mNew;
+
+ MyPermissionInfo(PermissionInfo info) {
+ super(info);
+ }
+ }
+
+ /** @hide */
+ public static class PermissionItemView extends LinearLayout implements View.OnClickListener {
+ MyPermissionGroupInfo mGroup;
+ MyPermissionInfo mPerm;
+ AlertDialog mDialog;
+ private boolean mShowRevokeUI = false;
+ private String mPackageName;
+
+ public PermissionItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setClickable(true);
+ }
+
+ public void setPermission(MyPermissionGroupInfo grp, MyPermissionInfo perm,
+ boolean first, CharSequence newPermPrefix, String packageName,
+ boolean showRevokeUI) {
+ mGroup = grp;
+ mPerm = perm;
+ mShowRevokeUI = showRevokeUI;
+ mPackageName = packageName;
+
+ ImageView permGrpIcon = findViewById(R.id.perm_icon);
+ TextView permNameView = findViewById(R.id.perm_name);
+
+ PackageManager pm = getContext().getPackageManager();
+ Drawable icon = null;
+ if (first) {
+ icon = grp.loadGroupIcon(getContext(), pm);
+ }
+ CharSequence label = perm.mLabel;
+ if (perm.mNew && newPermPrefix != null) {
+ // If this is a new permission, format it appropriately.
+ SpannableStringBuilder builder = new SpannableStringBuilder();
+ Parcel parcel = Parcel.obtain();
+ TextUtils.writeToParcel(newPermPrefix, parcel, 0);
+ parcel.setDataPosition(0);
+ CharSequence newStr = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ builder.append(newStr);
+ builder.append(label);
+ label = builder;
+ }
+
+ permGrpIcon.setImageDrawable(icon);
+ permNameView.setText(label);
+ setOnClickListener(this);
+ if (localLOGV) Log.i(TAG, "Made perm item " + perm.name
+ + ": " + label + " in group " + grp.name);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mGroup != null && mPerm != null) {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ PackageManager pm = getContext().getPackageManager();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(mGroup.mLabel);
+ if (mPerm.descriptionRes != 0) {
+ builder.setMessage(mPerm.loadDescription(pm));
+ } else {
+ CharSequence appName;
+ try {
+ ApplicationInfo app = pm.getApplicationInfo(mPerm.packageName, 0);
+ appName = app.loadLabel(pm);
+ } catch (NameNotFoundException e) {
+ appName = mPerm.packageName;
+ }
+ StringBuilder sbuilder = new StringBuilder(128);
+ sbuilder.append(getContext().getString(
+ R.string.perms_description_app, appName));
+ sbuilder.append("\n\n");
+ sbuilder.append(mPerm.name);
+ builder.setMessage(sbuilder.toString());
+ }
+ builder.setCancelable(true);
+ builder.setIcon(mGroup.loadGroupIcon(getContext(), pm));
+ addRevokeUIIfNecessary(builder);
+ mDialog = builder.show();
+ mDialog.setCanceledOnTouchOutside(true);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ }
+
+ private void addRevokeUIIfNecessary(AlertDialog.Builder builder) {
+ if (!mShowRevokeUI) {
+ return;
+ }
+
+ final boolean isRequired =
+ ((mPerm.mExistingReqFlags & PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0);
+
+ if (isRequired) {
+ return;
+ }
+
+ DialogInterface.OnClickListener ocl = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ PackageManager pm = getContext().getPackageManager();
+ pm.revokeRuntimePermission(mPackageName, mPerm.name,
+ new UserHandle(mContext.getUserId()));
+ PermissionItemView.this.setVisibility(View.GONE);
+ }
+ };
+ builder.setNegativeButton(R.string.revoke, ocl);
+ builder.setPositiveButton(R.string.ok, null);
+ }
+ }
+
+ private AppSecurityPermissions(Context context) {
+ mContext = context;
+ mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPm = mContext.getPackageManager();
+ // Pick up from framework resources instead.
+ mNewPermPrefix = mContext.getText(R.string.perms_new_perm_prefix);
+ }
+
+ public AppSecurityPermissions(Context context, String packageName) {
+ this(context);
+ mPackageName = packageName;
+ Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>();
+ PackageInfo pkgInfo;
+ try {
+ pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Couldn't retrieve permissions for package:"+packageName);
+ return;
+ }
+ // Extract all user permissions
+ if((pkgInfo.applicationInfo != null) && (pkgInfo.applicationInfo.uid != -1)) {
+ getAllUsedPermissions(pkgInfo.applicationInfo.uid, permSet);
+ }
+ mPermsList.addAll(permSet);
+ setPermissions(mPermsList);
+ }
+
+ public AppSecurityPermissions(Context context, PackageInfo info) {
+ this(context);
+ Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>();
+ if(info == null) {
+ return;
+ }
+ mPackageName = info.packageName;
+
+ // Convert to a PackageInfo
+ PackageInfo installedPkgInfo = null;
+ // Get requested permissions
+ if (info.requestedPermissions != null) {
+ try {
+ installedPkgInfo = mPm.getPackageInfo(info.packageName,
+ PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException e) {
+ }
+ extractPerms(info, permSet, installedPkgInfo);
+ }
+ // Get permissions related to shared user if any
+ if (info.sharedUserId != null) {
+ int sharedUid;
+ try {
+ sharedUid = mPm.getUidForSharedUser(info.sharedUserId);
+ getAllUsedPermissions(sharedUid, permSet);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Couldn't retrieve shared user id for: " + info.packageName);
+ }
+ }
+ // Retrieve list of permissions
+ mPermsList.addAll(permSet);
+ setPermissions(mPermsList);
+ }
+
+ /**
+ * Utility to retrieve a view displaying a single permission. This provides
+ * the old UI layout for permissions; it is only here for the device admin
+ * settings to continue to use.
+ */
+ public static View getPermissionItemView(Context context,
+ CharSequence grpName, CharSequence description, boolean dangerous) {
+ LayoutInflater inflater = (LayoutInflater)context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ Drawable icon = context.getDrawable(dangerous
+ ? R.drawable.ic_bullet_key_permission : R.drawable.ic_text_dot);
+ return getPermissionItemViewOld(context, inflater, grpName,
+ description, dangerous, icon);
+ }
+
+ private void getAllUsedPermissions(int sharedUid, Set<MyPermissionInfo> permSet) {
+ String sharedPkgList[] = mPm.getPackagesForUid(sharedUid);
+ if(sharedPkgList == null || (sharedPkgList.length == 0)) {
+ return;
+ }
+ for(String sharedPkg : sharedPkgList) {
+ getPermissionsForPackage(sharedPkg, permSet);
+ }
+ }
+
+ private void getPermissionsForPackage(String packageName, Set<MyPermissionInfo> permSet) {
+ try {
+ PackageInfo pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+ extractPerms(pkgInfo, permSet, pkgInfo);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Couldn't retrieve permissions for package: " + packageName);
+ }
+ }
+
+ private void extractPerms(PackageInfo info, Set<MyPermissionInfo> permSet,
+ PackageInfo installedPkgInfo) {
+ String[] strList = info.requestedPermissions;
+ int[] flagsList = info.requestedPermissionsFlags;
+ if ((strList == null) || (strList.length == 0)) {
+ return;
+ }
+ for (int i=0; i<strList.length; i++) {
+ String permName = strList[i];
+ try {
+ PermissionInfo tmpPermInfo = mPm.getPermissionInfo(permName, 0);
+ if (tmpPermInfo == null) {
+ continue;
+ }
+ int existingIndex = -1;
+ if (installedPkgInfo != null
+ && installedPkgInfo.requestedPermissions != null) {
+ for (int j=0; j<installedPkgInfo.requestedPermissions.length; j++) {
+ if (permName.equals(installedPkgInfo.requestedPermissions[j])) {
+ existingIndex = j;
+ break;
+ }
+ }
+ }
+ final int existingFlags = existingIndex >= 0 ?
+ installedPkgInfo.requestedPermissionsFlags[existingIndex] : 0;
+ if (!isDisplayablePermission(tmpPermInfo, flagsList[i], existingFlags)) {
+ // This is not a permission that is interesting for the user
+ // to see, so skip it.
+ continue;
+ }
+ final String origGroupName = tmpPermInfo.group;
+ String groupName = origGroupName;
+ if (groupName == null) {
+ groupName = tmpPermInfo.packageName;
+ tmpPermInfo.group = groupName;
+ }
+ MyPermissionGroupInfo group = mPermGroups.get(groupName);
+ if (group == null) {
+ PermissionGroupInfo grp = null;
+ if (origGroupName != null) {
+ grp = mPm.getPermissionGroupInfo(origGroupName, 0);
+ }
+ if (grp != null) {
+ group = new MyPermissionGroupInfo(grp);
+ } else {
+ // We could be here either because the permission
+ // didn't originally specify a group or the group it
+ // gave couldn't be found. In either case, we consider
+ // its group to be the permission's package name.
+ tmpPermInfo.group = tmpPermInfo.packageName;
+ group = mPermGroups.get(tmpPermInfo.group);
+ if (group == null) {
+ group = new MyPermissionGroupInfo(tmpPermInfo);
+ }
+ group = new MyPermissionGroupInfo(tmpPermInfo);
+ }
+ mPermGroups.put(tmpPermInfo.group, group);
+ }
+ final boolean newPerm = installedPkgInfo != null
+ && (existingFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0;
+ MyPermissionInfo myPerm = new MyPermissionInfo(tmpPermInfo);
+ myPerm.mNewReqFlags = flagsList[i];
+ myPerm.mExistingReqFlags = existingFlags;
+ // This is a new permission if the app is already installed and
+ // doesn't currently hold this permission.
+ myPerm.mNew = newPerm;
+ permSet.add(myPerm);
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "Ignoring unknown permission:"+permName);
+ }
+ }
+ }
+
+ public int getPermissionCount() {
+ return getPermissionCount(WHICH_ALL);
+ }
+
+ private List<MyPermissionInfo> getPermissionList(MyPermissionGroupInfo grp, int which) {
+ if (which == WHICH_NEW) {
+ return grp.mNewPermissions;
+ } else {
+ return grp.mAllPermissions;
+ }
+ }
+
+ public int getPermissionCount(int which) {
+ int N = 0;
+ for (int i=0; i<mPermGroupsList.size(); i++) {
+ N += getPermissionList(mPermGroupsList.get(i), which).size();
+ }
+ return N;
+ }
+
+ public View getPermissionsView() {
+ return getPermissionsView(WHICH_ALL, false);
+ }
+
+ public View getPermissionsViewWithRevokeButtons() {
+ return getPermissionsView(WHICH_ALL, true);
+ }
+
+ public View getPermissionsView(int which) {
+ return getPermissionsView(which, false);
+ }
+
+ private View getPermissionsView(int which, boolean showRevokeUI) {
+ LinearLayout permsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null);
+ LinearLayout displayList = permsView.findViewById(R.id.perms_list);
+ View noPermsView = permsView.findViewById(R.id.no_permissions);
+
+ displayPermissions(mPermGroupsList, displayList, which, showRevokeUI);
+ if (displayList.getChildCount() <= 0) {
+ noPermsView.setVisibility(View.VISIBLE);
+ }
+
+ return permsView;
+ }
+
+ /**
+ * Utility method that displays permissions from a map containing group name and
+ * list of permission descriptions.
+ */
+ private void displayPermissions(List<MyPermissionGroupInfo> groups,
+ LinearLayout permListView, int which, boolean showRevokeUI) {
+ permListView.removeAllViews();
+
+ int spacing = (int)(8*mContext.getResources().getDisplayMetrics().density);
+
+ for (int i=0; i<groups.size(); i++) {
+ MyPermissionGroupInfo grp = groups.get(i);
+ final List<MyPermissionInfo> perms = getPermissionList(grp, which);
+ for (int j=0; j<perms.size(); j++) {
+ MyPermissionInfo perm = perms.get(j);
+ View view = getPermissionItemView(grp, perm, j == 0,
+ which != WHICH_NEW ? mNewPermPrefix : null, showRevokeUI);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ if (j == 0) {
+ lp.topMargin = spacing;
+ }
+ if (j == grp.mAllPermissions.size()-1) {
+ lp.bottomMargin = spacing;
+ }
+ if (permListView.getChildCount() == 0) {
+ lp.topMargin *= 2;
+ }
+ permListView.addView(view, lp);
+ }
+ }
+ }
+
+ private PermissionItemView getPermissionItemView(MyPermissionGroupInfo grp,
+ MyPermissionInfo perm, boolean first, CharSequence newPermPrefix, boolean showRevokeUI) {
+ return getPermissionItemView(mContext, mInflater, grp, perm, first, newPermPrefix,
+ mPackageName, showRevokeUI);
+ }
+
+ private static PermissionItemView getPermissionItemView(Context context, LayoutInflater inflater,
+ MyPermissionGroupInfo grp, MyPermissionInfo perm, boolean first,
+ CharSequence newPermPrefix, String packageName, boolean showRevokeUI) {
+ PermissionItemView permView = (PermissionItemView)inflater.inflate(
+ (perm.flags & PermissionInfo.FLAG_COSTS_MONEY) != 0
+ ? R.layout.app_permission_item_money : R.layout.app_permission_item,
+ null);
+ permView.setPermission(grp, perm, first, newPermPrefix, packageName, showRevokeUI);
+ return permView;
+ }
+
+ private static View getPermissionItemViewOld(Context context, LayoutInflater inflater,
+ CharSequence grpName, CharSequence permList, boolean dangerous, Drawable icon) {
+ View permView = inflater.inflate(R.layout.app_permission_item_old, null);
+
+ TextView permGrpView = permView.findViewById(R.id.permission_group);
+ TextView permDescView = permView.findViewById(R.id.permission_list);
+
+ 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 boolean isDisplayablePermission(PermissionInfo pInfo, int newReqFlags,
+ int existingReqFlags) {
+ final int base = pInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
+ final boolean isNormal = (base == PermissionInfo.PROTECTION_NORMAL);
+
+ // We do not show normal permissions in the UI.
+ if (isNormal) {
+ return false;
+ }
+
+ final boolean isDangerous = (base == PermissionInfo.PROTECTION_DANGEROUS)
+ || ((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_PRE23) != 0);
+ final boolean isRequired =
+ ((newReqFlags&PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0);
+ final boolean isDevelopment =
+ ((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0);
+ final boolean wasGranted =
+ ((existingReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
+ final boolean isGranted =
+ ((newReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
+
+ // Dangerous and normal permissions are always shown to the user if the permission
+ // is required, or it was previously granted
+ if (isDangerous && (isRequired || wasGranted || isGranted)) {
+ return true;
+ }
+
+ // Development permissions are only shown to the user if they are already
+ // granted to the app -- if we are installing an app and they are not
+ // already granted, they will not be granted as part of the install.
+ if (isDevelopment && wasGranted) {
+ if (localLOGV) Log.i(TAG, "Special perm " + pInfo.name
+ + ": protlevel=0x" + Integer.toHexString(pInfo.protectionLevel));
+ return true;
+ }
+ return false;
+ }
+
+ private static class PermissionGroupInfoComparator implements Comparator<MyPermissionGroupInfo> {
+ private final Collator sCollator = Collator.getInstance();
+ @Override
+ public final int compare(MyPermissionGroupInfo a, MyPermissionGroupInfo b) {
+ return sCollator.compare(a.mLabel, b.mLabel);
+ }
+ }
+
+ private static class PermissionInfoComparator implements Comparator<MyPermissionInfo> {
+ private final Collator sCollator = Collator.getInstance();
+ PermissionInfoComparator() {
+ }
+ public final int compare(MyPermissionInfo a, MyPermissionInfo b) {
+ return sCollator.compare(a.mLabel, b.mLabel);
+ }
+ }
+
+ private void addPermToList(List<MyPermissionInfo> permList,
+ MyPermissionInfo pInfo) {
+ if (pInfo.mLabel == null) {
+ pInfo.mLabel = pInfo.loadLabel(mPm);
+ }
+ int idx = Collections.binarySearch(permList, pInfo, mPermComparator);
+ if(localLOGV) Log.i(TAG, "idx="+idx+", list.size="+permList.size());
+ if (idx < 0) {
+ idx = -idx-1;
+ permList.add(idx, pInfo);
+ }
+ }
+
+ private void setPermissions(List<MyPermissionInfo> permList) {
+ if (permList != null) {
+ // First pass to group permissions
+ for (MyPermissionInfo pInfo : permList) {
+ if(localLOGV) Log.i(TAG, "Processing permission:"+pInfo.name);
+ if(!isDisplayablePermission(pInfo, pInfo.mNewReqFlags, pInfo.mExistingReqFlags)) {
+ if(localLOGV) Log.i(TAG, "Permission:"+pInfo.name+" is not displayable");
+ continue;
+ }
+ MyPermissionGroupInfo group = mPermGroups.get(pInfo.group);
+ if (group != null) {
+ pInfo.mLabel = pInfo.loadLabel(mPm);
+ addPermToList(group.mAllPermissions, pInfo);
+ if (pInfo.mNew) {
+ addPermToList(group.mNewPermissions, pInfo);
+ }
+ }
+ }
+ }
+
+ for (MyPermissionGroupInfo pgrp : mPermGroups.values()) {
+ if (pgrp.labelRes != 0 || pgrp.nonLocalizedLabel != null) {
+ pgrp.mLabel = pgrp.loadLabel(mPm);
+ } else {
+ ApplicationInfo app;
+ try {
+ app = mPm.getApplicationInfo(pgrp.packageName, 0);
+ pgrp.mLabel = app.loadLabel(mPm);
+ } catch (NameNotFoundException e) {
+ pgrp.mLabel = pgrp.loadLabel(mPm);
+ }
+ }
+ mPermGroupsList.add(pgrp);
+ }
+ Collections.sort(mPermGroupsList, mPermGroupComparator);
+ }
+}
diff --git a/android/widget/ArrayAdapter.java b/android/widget/ArrayAdapter.java
new file mode 100644
index 0000000..f18f217
--- /dev/null
+++ b/android/widget/ArrayAdapter.java
@@ -0,0 +1,623 @@
+/*
+ * 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.annotation.ArrayRes;
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * You can use this adapter to provide views for an {@link AdapterView},
+ * Returns a view for each object in a collection of data objects you
+ * provide, and can be used with list-based user interface widgets such as
+ * {@link ListView} or {@link Spinner}.
+ * <p>
+ * By default, the array adapter creates a view by calling {@link Object#toString()} on each
+ * data object in the collection you provide, and places the result in a TextView.
+ * You may also customize what type of view is used for the data object in the collection.
+ * To customize what type of view is used for the data object,
+ * override {@link #getView(int, View, ViewGroup)}
+ * and inflate a view resource.
+ * For a code example, see
+ * the <a href="https://developer.android.com/samples/CustomChoiceList/index.html">
+ * CustomChoiceList</a> sample.
+ * </p>
+ * <p>
+ * For an example of using an array adapter with a ListView, see the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews">
+ * Adapter Views</a> guide.
+ * </p>
+ * <p>
+ * For an example of using an array adapter with a Spinner, see the
+ * <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.
+ * </p>
+ * <p class="note"><strong>Note:</strong>
+ * If you are considering using array adapter with a ListView, consider using
+ * {@link android.support.v7.widget.RecyclerView} instead.
+ * RecyclerView offers similar features with better performance and more flexibility than
+ * ListView provides.
+ * See the
+ * <a href="https://developer.android.com/guide/topics/ui/layout/recyclerview.html">
+ * Recycler View</a> guide.</p>
+ */
+public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
+ /**
+ * 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();
+
+ private final LayoutInflater mInflater;
+
+ private final Context mContext;
+
+ /**
+ * The resource indicating what views to inflate to display the content of this
+ * array adapter.
+ */
+ private final 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;
+
+ /**
+ * 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;
+
+ /**
+ * Indicates whether the contents of {@link #mObjects} came from static resources.
+ */
+ private boolean mObjectsFromResources;
+
+ /**
+ * If the inflated resource is not a TextView, {@code 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;
+
+ // A copy of the original mObjects array, initialized from and then used instead as soon as
+ // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values.
+ private ArrayList<T> mOriginalValues;
+ private ArrayFilter mFilter;
+
+ /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
+ private LayoutInflater mDropDownInflater;
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param resource The resource ID for a layout file containing a TextView to use when
+ * instantiating views.
+ */
+ public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) {
+ this(context, resource, 0, new ArrayList<>());
+ }
+
+ /**
+ * 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(@NonNull Context context, @LayoutRes int resource,
+ @IdRes int textViewResourceId) {
+ this(context, resource, textViewResourceId, new ArrayList<>());
+ }
+
+ /**
+ * Constructor. This constructor will result in the underlying data collection being
+ * immutable, so methods such as {@link #clear()} will throw an exception.
+ *
+ * @param context The current context.
+ * @param resource 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(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
+ this(context, resource, 0, Arrays.asList(objects));
+ }
+
+ /**
+ * Constructor. This constructor will result in the underlying data collection being
+ * immutable, so methods such as {@link #clear()} will throw an exception.
+ *
+ * @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(@NonNull Context context, @LayoutRes int resource,
+ @IdRes int textViewResourceId, @NonNull T[] objects) {
+ this(context, resource, textViewResourceId, Arrays.asList(objects));
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param resource 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(@NonNull Context context, @LayoutRes int resource,
+ @NonNull List<T> objects) {
+ this(context, resource, 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(@NonNull Context context, @LayoutRes int resource,
+ @IdRes int textViewResourceId, @NonNull List<T> objects) {
+ this(context, resource, textViewResourceId, objects, false);
+ }
+
+ private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+ @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) {
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ mResource = mDropDownResource = resource;
+ mObjects = objects;
+ mObjectsFromResources = objsFromResources;
+ mFieldId = textViewResourceId;
+ }
+
+ /**
+ * Adds the specified object at the end of the array.
+ *
+ * @param object The object to add at the end of the array.
+ * @throws UnsupportedOperationException if the underlying data collection is immutable
+ */
+ public void add(@Nullable T object) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ mOriginalValues.add(object);
+ } else {
+ mObjects.add(object);
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Adds the specified Collection at the end of the array.
+ *
+ * @param collection The Collection to add at the end of the array.
+ * @throws UnsupportedOperationException if the <tt>addAll</tt> operation
+ * is not supported by this list
+ * @throws ClassCastException if the class of an element of the specified
+ * collection prevents it from being added to this list
+ * @throws NullPointerException if the specified collection contains one
+ * or more null elements and this list does not permit null
+ * elements, or if the specified collection is null
+ * @throws IllegalArgumentException if some property of an element of the
+ * specified collection prevents it from being added to this list
+ */
+ public void addAll(@NonNull Collection<? extends T> collection) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ mOriginalValues.addAll(collection);
+ } else {
+ mObjects.addAll(collection);
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Adds the specified items at the end of the array.
+ *
+ * @param items The items to add at the end of the array.
+ * @throws UnsupportedOperationException if the underlying data collection is immutable
+ */
+ public void addAll(T ... items) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ Collections.addAll(mOriginalValues, items);
+ } else {
+ Collections.addAll(mObjects, items);
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Inserts the specified 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.
+ * @throws UnsupportedOperationException if the underlying data collection is immutable
+ */
+ public void insert(@Nullable T object, int index) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ mOriginalValues.add(index, object);
+ } else {
+ mObjects.add(index, object);
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Removes the specified object from the array.
+ *
+ * @param object The object to remove.
+ * @throws UnsupportedOperationException if the underlying data collection is immutable
+ */
+ public void remove(@Nullable T object) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ mOriginalValues.remove(object);
+ } else {
+ mObjects.remove(object);
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Remove all elements from the list.
+ *
+ * @throws UnsupportedOperationException if the underlying data collection is immutable
+ */
+ public void clear() {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ mOriginalValues.clear();
+ } else {
+ mObjects.clear();
+ }
+ mObjectsFromResources = false;
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Sorts the content of this adapter using the specified comparator.
+ *
+ * @param comparator The comparator used to sort the objects contained
+ * in this adapter.
+ */
+ public void sort(@NonNull Comparator<? super T> comparator) {
+ synchronized (mLock) {
+ if (mOriginalValues != null) {
+ Collections.sort(mOriginalValues, comparator);
+ } else {
+ Collections.sort(mObjects, comparator);
+ }
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ super.notifyDataSetChanged();
+ mNotifyOnChange = true;
+ }
+
+ /**
+ * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)},
+ * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear},
+ * {@link #sort(Comparator)}) 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;
+ }
+
+ /**
+ * 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 @NonNull Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public int getCount() {
+ return mObjects.size();
+ }
+
+ @Override
+ public @Nullable 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(@Nullable T item) {
+ return mObjects.indexOf(item);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public @NonNull View getView(int position, @Nullable View convertView,
+ @NonNull ViewGroup parent) {
+ return createViewFromResource(mInflater, position, convertView, parent, mResource);
+ }
+
+ private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
+ @Nullable View convertView, @NonNull ViewGroup parent, int resource) {
+ final View view;
+ final TextView text;
+
+ if (convertView == null) {
+ view = inflater.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 = view.findViewById(mFieldId);
+
+ if (text == null) {
+ throw new RuntimeException("Failed to find view with ID "
+ + mContext.getResources().getResourceName(mFieldId)
+ + " in item layout");
+ }
+ }
+ } 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);
+ }
+
+ final T item = getItem(position);
+ if (item instanceof CharSequence) {
+ text.setText((CharSequence) item);
+ } else {
+ text.setText(item.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(@LayoutRes int resource) {
+ this.mDropDownResource = resource;
+ }
+
+ /**
+ * Sets the {@link Resources.Theme} against which drop-down views are
+ * inflated.
+ * <p>
+ * By default, drop-down views are inflated against the theme of the
+ * {@link Context} passed to the adapter's constructor.
+ *
+ * @param theme the theme against which to inflate drop-down views or
+ * {@code null} to use the theme from the adapter's context
+ * @see #getDropDownView(int, View, ViewGroup)
+ */
+ @Override
+ public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
+ if (theme == null) {
+ mDropDownInflater = null;
+ } else if (theme == mInflater.getContext().getTheme()) {
+ mDropDownInflater = mInflater;
+ } else {
+ final Context context = new ContextThemeWrapper(mContext, theme);
+ mDropDownInflater = LayoutInflater.from(context);
+ }
+ }
+
+ @Override
+ public @Nullable Resources.Theme getDropDownViewTheme() {
+ return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
+ }
+
+ @Override
+ public View getDropDownView(int position, @Nullable View convertView,
+ @NonNull ViewGroup parent) {
+ final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
+ return createViewFromResource(inflater, 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 @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context,
+ @ArrayRes int textArrayResId, @LayoutRes int textViewResId) {
+ final CharSequence[] strings = context.getResources().getTextArray(textArrayResId);
+ return new ArrayAdapter<>(context, textViewResId, 0, Arrays.asList(strings), true);
+ }
+
+ @Override
+ public @NonNull Filter getFilter() {
+ if (mFilter == null) {
+ mFilter = new ArrayFilter();
+ }
+ return mFilter;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return values from the string array used by {@link #createFromResource(Context, int, int)},
+ * or {@code null} if object was created otherwsie or if contents were dynamically changed after
+ * creation.
+ */
+ @Override
+ public CharSequence[] getAutofillOptions() {
+ // First check if app developer explicitly set them.
+ final CharSequence[] explicitOptions = super.getAutofillOptions();
+ if (explicitOptions != null) {
+ return explicitOptions;
+ }
+
+ // Otherwise, only return options that came from static resources.
+ if (!mObjectsFromResources || mObjects == null || mObjects.isEmpty()) {
+ return null;
+ }
+ final int size = mObjects.size();
+ final CharSequence[] options = new CharSequence[size];
+ mObjects.toArray(options);
+ return options;
+ }
+
+ /**
+ * <p>An array filter 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) {
+ final FilterResults results = new FilterResults();
+
+ if (mOriginalValues == null) {
+ synchronized (mLock) {
+ mOriginalValues = new ArrayList<>(mObjects);
+ }
+ }
+
+ if (prefix == null || prefix.length() == 0) {
+ final ArrayList<T> list;
+ synchronized (mLock) {
+ list = new ArrayList<>(mOriginalValues);
+ }
+ results.values = list;
+ results.count = list.size();
+ } else {
+ final String prefixString = prefix.toString().toLowerCase();
+
+ final ArrayList<T> values;
+ synchronized (mLock) {
+ values = new ArrayList<>(mOriginalValues);
+ }
+
+ final int count = values.size();
+ final ArrayList<T> newValues = new ArrayList<>();
+
+ 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(" ");
+ for (String word : words) {
+ if (word.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/android/widget/AutoCompleteTextView.java b/android/widget/AutoCompleteTextView.java
new file mode 100644
index 0000000..49741d4
--- /dev/null
+++ b/android/widget/AutoCompleteTextView.java
@@ -0,0 +1,1415 @@
+/*
+ * 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.DrawableRes;
+import android.content.Context;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * <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>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
+ * guide.</p>
+ *
+ * @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
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
+ static final boolean DEBUG = false;
+ static final String TAG = "AutoCompleteTextView";
+
+ static final int EXPAND_MAX = 3;
+
+ /** Context used to inflate the popup window or dialog. */
+ private final Context mPopupContext;
+
+ private final ListPopupWindow mPopup;
+ private final PassThroughClickListener mPassThroughClickListener;
+
+ private CharSequence mHintText;
+ private TextView mHintView;
+ private int mHintResource;
+
+ private ListAdapter mAdapter;
+ private Filter mFilter;
+ private int mThreshold;
+
+ private int mDropDownAnchorId;
+
+ private AdapterView.OnItemClickListener mItemClickListener;
+ private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+ private boolean mDropDownDismissedOnCompletion = true;
+
+ private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+ private boolean mOpenBefore;
+
+ private Validator mValidator = null;
+
+ // Set to true when text is set directly and no filtering shall be performed
+ private boolean mBlockCompletion;
+
+ // When set, an update in the underlying adapter will update the result list popup.
+ // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
+ private boolean mPopupCanBeUpdated = true;
+
+ private PopupDataSetObserver mObserver;
+
+ /**
+ * Constructs a new auto-complete text view with the given context's theme.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ */
+ public AutoCompleteTextView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructs a new auto-complete text view with the given context's theme
+ * and the supplied attribute set.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public AutoCompleteTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.autoCompleteTextViewStyle);
+ }
+
+ /**
+ * Constructs a new auto-complete text view with the given context's theme,
+ * the supplied attribute set, and default style attribute.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ */
+ public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Constructs a new auto-complete text view with the given context's theme,
+ * the supplied attribute set, and default styles.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme.
+ * Can be 0 to not look for defaults.
+ */
+ public AutoCompleteTextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ this(context, attrs, defStyleAttr, defStyleRes, null);
+ }
+
+ /**
+ * Constructs a new auto-complete text view with the given context, the
+ * supplied attribute set, default styles, and the theme against which the
+ * completion popup should be inflated.
+ *
+ * @param context The context against which the view is inflated, which
+ * provides access to the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme.
+ * Can be 0 to not look for defaults.
+ * @param popupTheme The theme against which the completion popup window
+ * should be inflated. May be {@code null} to use the
+ * view theme. If set, this will override any value
+ * specified by
+ * {@link android.R.styleable#AutoCompleteTextView_popupTheme}.
+ */
+ public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes, Theme popupTheme) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes);
+
+ if (popupTheme != null) {
+ mPopupContext = new ContextThemeWrapper(context, popupTheme);
+ } else {
+ final int popupThemeResId = a.getResourceId(
+ R.styleable.AutoCompleteTextView_popupTheme, 0);
+ if (popupThemeResId != 0) {
+ mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
+ } else {
+ mPopupContext = context;
+ }
+ }
+
+ // Load attributes used within the popup against the popup context.
+ final TypedArray pa;
+ if (mPopupContext != context) {
+ pa = mPopupContext.obtainStyledAttributes(
+ attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes);
+ } else {
+ pa = a;
+ }
+
+ final Drawable popupListSelector = pa.getDrawable(
+ R.styleable.AutoCompleteTextView_dropDownSelector);
+ final int popupWidth = pa.getLayoutDimension(
+ R.styleable.AutoCompleteTextView_dropDownWidth, LayoutParams.WRAP_CONTENT);
+ final int popupHeight = pa.getLayoutDimension(
+ R.styleable.AutoCompleteTextView_dropDownHeight, LayoutParams.WRAP_CONTENT);
+ final int popupHintLayoutResId = pa.getResourceId(
+ R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint);
+ final CharSequence popupHintText = pa.getText(
+ R.styleable.AutoCompleteTextView_completionHint);
+
+ if (pa != a) {
+ pa.recycle();
+ }
+
+ mPopup = new ListPopupWindow(mPopupContext, attrs, defStyleAttr, defStyleRes);
+ mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+ mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
+ mPopup.setListSelector(popupListSelector);
+ mPopup.setOnItemClickListener(new DropDownItemClickListener());
+
+ // For dropdown width, the developer can specify a specific width, or
+ // MATCH_PARENT (for full screen width), or WRAP_CONTENT (to match the
+ // width of the anchored view).
+ mPopup.setWidth(popupWidth);
+ mPopup.setHeight(popupHeight);
+
+ // Completion hint must be set after specifying hint layout.
+ mHintResource = popupHintLayoutResId;
+ setCompletionHint(popupHintText);
+
+ // Get the anchor's id now, but the view won't be ready, so wait to
+ // actually get the view and store it in mDropDownAnchorView lazily in
+ // getDropDownAnchorView later. Defaults to NO_ID, in which case the
+ // getDropDownAnchorView method will simply return this TextView, as a
+ // default anchoring point.
+ mDropDownAnchorId = a.getResourceId(
+ R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID);
+
+ mThreshold = a.getInt(R.styleable.AutoCompleteTextView_completionThreshold, 2);
+
+ a.recycle();
+
+ // Always turn on the auto complete input type flag, since it
+ // makes no sense to use this widget without it.
+ int inputType = getInputType();
+ if ((inputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ setRawInputType(inputType);
+ }
+
+ setFocusable(true);
+
+ addTextChangedListener(new MyWatcher());
+
+ mPassThroughClickListener = new PassThroughClickListener();
+ super.setOnClickListener(mPassThroughClickListener);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ mPassThroughClickListener.mWrapped = listener;
+ }
+
+ /**
+ * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
+ */
+ private void onClickImpl() {
+ // If the dropdown is showing, bring the keyboard to the front
+ // when the user touches the text field.
+ if (isPopupShowing()) {
+ ensureImeVisible(true);
+ }
+ }
+
+ /**
+ * <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
+ *
+ * @see #getCompletionHint()
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ */
+ public void setCompletionHint(CharSequence hint) {
+ mHintText = hint;
+ if (hint != null) {
+ if (mHintView == null) {
+ final TextView hintView = (TextView) LayoutInflater.from(mPopupContext).inflate(
+ mHintResource, null).findViewById(R.id.text1);
+ hintView.setText(mHintText);
+ mHintView = hintView;
+ mPopup.setPromptView(hintView);
+ } else {
+ mHintView.setText(hint);
+ }
+ } else {
+ mPopup.setPromptView(null);
+ mHintView = null;
+ }
+ }
+
+ /**
+ * Gets the optional hint text displayed at the bottom of the the matching list.
+ *
+ * @return The hint text, if any
+ *
+ * @see #setCompletionHint(CharSequence)
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ */
+ public CharSequence getCompletionHint() {
+ return mHintText;
+ }
+
+ /**
+ * <p>Returns the current width for the auto-complete drop down list. This can
+ * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
+ *
+ * @return the width for the drop down list
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ */
+ public int getDropDownWidth() {
+ return mPopup.getWidth();
+ }
+
+ /**
+ * <p>Sets the current width for the auto-complete drop down list. This can
+ * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
+ *
+ * @param width the width to use
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ */
+ public void setDropDownWidth(int width) {
+ mPopup.setWidth(width);
+ }
+
+ /**
+ * <p>Returns the current height for the auto-complete drop down list. This can
+ * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+ * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+ * of the drop down's content.</p>
+ *
+ * @return the height for the drop down list
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ */
+ public int getDropDownHeight() {
+ return mPopup.getHeight();
+ }
+
+ /**
+ * <p>Sets the current height for the auto-complete drop down list. This can
+ * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+ * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+ * of the drop down's content.</p>
+ *
+ * @param height the height to use
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ */
+ public void setDropDownHeight(int height) {
+ mPopup.setHeight(height);
+ }
+
+ /**
+ * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
+ *
+ * @return the view's id, or {@link View#NO_ID} if none specified
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ */
+ public int getDropDownAnchor() {
+ return mDropDownAnchorId;
+ }
+
+ /**
+ * <p>Sets the view to which the auto-complete drop down list should anchor. The view
+ * corresponding to this id will not be loaded until the next time it is needed to avoid
+ * loading a view which is not yet instantiated.</p>
+ *
+ * @param id the id to anchor the drop down list view to
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ */
+ public void setDropDownAnchor(int id) {
+ mDropDownAnchorId = id;
+ mPopup.setAnchorView(null);
+ }
+
+ /**
+ * <p>Gets the background of the auto-complete drop-down list.</p>
+ *
+ * @return the background drawable
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public Drawable getDropDownBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * <p>Sets the background of the auto-complete drop-down list.</p>
+ *
+ * @param d the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public void setDropDownBackgroundDrawable(Drawable d) {
+ mPopup.setBackgroundDrawable(d);
+ }
+
+ /**
+ * <p>Sets the background of the auto-complete drop-down list.</p>
+ *
+ * @param id the id of the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public void setDropDownBackgroundResource(@DrawableRes int id) {
+ mPopup.setBackgroundDrawable(getContext().getDrawable(id));
+ }
+
+ /**
+ * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
+ *
+ * @param offset the vertical offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public void setDropDownVerticalOffset(int offset) {
+ mPopup.setVerticalOffset(offset);
+ }
+
+ /**
+ * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
+ *
+ * @return the vertical offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public int getDropDownVerticalOffset() {
+ return mPopup.getVerticalOffset();
+ }
+
+ /**
+ * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
+ *
+ * @param offset the horizontal offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public void setDropDownHorizontalOffset(int offset) {
+ mPopup.setHorizontalOffset(offset);
+ }
+
+ /**
+ * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
+ *
+ * @return the horizontal offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public int getDropDownHorizontalOffset() {
+ return mPopup.getHorizontalOffset();
+ }
+
+ /**
+ * <p>Sets the animation style of the auto-complete drop-down list.</p>
+ *
+ * <p>If the drop-down is showing, calling this method will take effect only
+ * the next time the drop-down is shown.</p>
+ *
+ * @param animationStyle animation style to use when the drop-down appears
+ * and disappears. Set to -1 for the default animation, 0 for no
+ * animation, or a resource identifier for an explicit animation.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownAnimationStyle(int animationStyle) {
+ mPopup.setAnimationStyle(animationStyle);
+ }
+
+ /**
+ * <p>Returns the animation style that is used when the drop-down list appears and disappears
+ * </p>
+ *
+ * @return the animation style that is used when the drop-down list appears and disappears
+ *
+ * @hide Pending API council approval
+ */
+ public int getDropDownAnimationStyle() {
+ return mPopup.getAnimationStyle();
+ }
+
+ /**
+ * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
+ *
+ * @hide Pending API council approval
+ */
+ public boolean isDropDownAlwaysVisible() {
+ return mPopup.isDropDownAlwaysVisible();
+ }
+
+ /**
+ * Sets whether the drop-down should remain visible as long as there is there is
+ * {@link #enoughToFilter()}. This is useful if an unknown number of results are expected
+ * to show up in the adapter sometime in the future.
+ *
+ * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
+ * of the size or content of the list. {@link #getDropDownBackground()} will fill any space
+ * that is not used by the list.
+ *
+ * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+ mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible);
+ }
+
+ /**
+ * Checks whether the drop-down is dismissed when a suggestion is clicked.
+ *
+ * @hide Pending API council approval
+ */
+ public boolean isDropDownDismissedOnCompletion() {
+ return mDropDownDismissedOnCompletion;
+ }
+
+ /**
+ * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
+ * true by default.
+ *
+ * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
+ mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
+ }
+
+ /**
+ * <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)
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ */
+ 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
+ *
+ * @deprecated Use {@link #getOnItemClickListener()} intead
+ */
+ @Deprecated
+ 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
+ *
+ * @deprecated Use {@link #getOnItemSelectedListener()} intead
+ */
+ @Deprecated
+ public AdapterView.OnItemSelectedListener getItemSelectedListener() {
+ return mItemSelectedListener;
+ }
+
+ /**
+ * <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 getOnItemClickListener() {
+ 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 getOnItemSelectedListener() {
+ return mItemSelectedListener;
+ }
+
+ /**
+ * Set a listener that will be invoked whenever the AutoCompleteTextView's
+ * list of completions is dismissed.
+ * @param dismissListener Listener to invoke when completions are dismissed
+ */
+ public void setOnDismissListener(final OnDismissListener dismissListener) {
+ PopupWindow.OnDismissListener wrappedListener = null;
+ if (dismissListener != null) {
+ wrappedListener = new PopupWindow.OnDismissListener() {
+ @Override public void onDismiss() {
+ dismissListener.onDismiss();
+ }
+ };
+ }
+ mPopup.setOnDismissListener(wrappedListener);
+ }
+
+ /**
+ * <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>
+ *
+ * <p>The caller is still responsible for managing any resources used by the adapter.
+ * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
+ * A common case is the use of {@link android.widget.CursorAdapter}, which
+ * contains a {@link android.database.Cursor} that must be closed. This can be done
+ * automatically (see
+ * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
+ * startManagingCursor()}),
+ * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</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) {
+ if (mObserver == null) {
+ mObserver = new PopupDataSetObserver(this);
+ } else if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ mAdapter = adapter;
+ if (mAdapter != null) {
+ //noinspection unchecked
+ mFilter = ((Filterable) mAdapter).getFilter();
+ adapter.registerDataSetObserver(mObserver);
+ } else {
+ mFilter = null;
+ }
+
+ mPopup.setAdapter(mAdapter);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
+ && !mPopup.isDropDownAlwaysVisible()) {
+ // 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 (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.handleUpEvent(event);
+ }
+ if (event.isTracking() && !event.isCanceled()) {
+ dismissDropDown();
+ return true;
+ }
+ }
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ boolean consumed = mPopup.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:
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ performCompletion();
+ }
+ return true;
+ }
+ }
+
+ if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+ performCompletion();
+ return true;
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mPopup.onKeyDown(keyCode, event)) {
+ return true;
+ }
+
+ if (!isPopupShowing()) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (event.hasNoModifiers()) {
+ performValidation();
+ }
+ }
+ }
+
+ if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+ return true;
+ }
+
+ mLastKeyCode = keyCode;
+ boolean handled = super.onKeyDown(keyCode, event);
+ mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+
+ if (handled && isPopupShowing()) {
+ clearListSelection();
+ }
+
+ 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() {
+ if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
+ + " threshold=" + mThreshold);
+ return getText().length() >= mThreshold;
+ }
+
+ /**
+ * This is used to watch for edits to the text view. Note that we call
+ * to methods on the auto complete text view class so that we can access
+ * private vars without going through thunks.
+ */
+ private class MyWatcher implements TextWatcher {
+ public void afterTextChanged(Editable s) {
+ doAfterTextChanged();
+ }
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ doBeforeTextChanged();
+ }
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ }
+
+ void doBeforeTextChanged() {
+ if (mBlockCompletion) return;
+
+ // when text is changed, inserted or deleted, we attempt to show
+ // the drop down
+ mOpenBefore = isPopupShowing();
+ if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
+ }
+
+ void doAfterTextChanged() {
+ if (mBlockCompletion) return;
+
+ // 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 (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
+ + " open=" + isPopupShowing());
+ if (mOpenBefore && !isPopupShowing()) {
+ return;
+ }
+
+ // the drop down is shown only when a minimum number of characters
+ // was typed in the text view
+ if (enoughToFilter()) {
+ if (mFilter != null) {
+ mPopupCanBeUpdated = true;
+ performFiltering(getText(), mLastKeyCode);
+ }
+ } else {
+ // drop down is automatically dismissed when enough characters
+ // are deleted from the text view
+ if (!mPopup.isDropDownAlwaysVisible()) {
+ dismissDropDown();
+ }
+ if (mFilter != null) {
+ mFilter.filter(null);
+ }
+ }
+ }
+
+ /**
+ * <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>Clear the list selection. This may only be temporary, as user input will often bring
+ * it back.
+ */
+ public void clearListSelection() {
+ mPopup.clearListSelection();
+ }
+
+ /**
+ * Set the position of the dropdown view selection.
+ *
+ * @param position The position to move the selector to.
+ */
+ public void setListSelection(int position) {
+ mPopup.setSelection(position);
+ }
+
+ /**
+ * Get the position of the dropdown view selection, if there is one. Returns
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
+ * there is no selection.
+ *
+ * @return the position of the current selection, if there is one, or
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getListSelection() {
+ return mPopup.getSelectedItemPosition();
+ }
+
+ /**
+ * <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; beware that
+ * this will be null when text is being added through a soft input method.
+ */
+ @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);
+ }
+
+ @Override
+ public void onCommitCompletion(CompletionInfo completion) {
+ if (isPopupShowing()) {
+ mPopup.performItemClick(completion.getPosition());
+ }
+ }
+
+ private void performCompletion(View selectedView, int position, long id) {
+ if (isPopupShowing()) {
+ Object selectedItem;
+ if (position < 0) {
+ selectedItem = mPopup.getSelectedItem();
+ } else {
+ selectedItem = mAdapter.getItem(position);
+ }
+ if (selectedItem == null) {
+ Log.w(TAG, "performCompletion: no selected item");
+ return;
+ }
+
+ mBlockCompletion = true;
+ replaceText(convertSelectionToString(selectedItem));
+ mBlockCompletion = false;
+
+ if (mItemClickListener != null) {
+ final ListPopupWindow list = mPopup;
+
+ if (selectedView == null || position < 0) {
+ selectedView = list.getSelectedView();
+ position = list.getSelectedItemPosition();
+ id = list.getSelectedItemId();
+ }
+ mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
+ }
+ }
+
+ if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
+ dismissDropDown();
+ }
+ }
+
+ /**
+ * Identifies whether the view is currently performing a text completion, so subclasses
+ * can decide whether to respond to text changed events.
+ */
+ public boolean isPerformingCompletion() {
+ return mBlockCompletion;
+ }
+
+ /**
+ * Like {@link #setText(CharSequence)}, except that it can disable filtering.
+ *
+ * @param filter If <code>false</code>, no filtering will be performed
+ * as a result of this call.
+ */
+ public void setText(CharSequence text, boolean filter) {
+ if (filter) {
+ setText(text);
+ } else {
+ mBlockCompletion = true;
+ setText(text);
+ mBlockCompletion = false;
+ }
+ }
+
+ /**
+ * <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) {
+ clearComposingText();
+
+ setText(text);
+ // make sure we keep the caret at the end of the text view
+ Editable spannable = getText();
+ Selection.setSelection(spannable, spannable.length());
+ }
+
+ /** {@inheritDoc} */
+ public void onFilterComplete(int count) {
+ updateDropDownForFilter(count);
+ }
+
+ private void updateDropDownForFilter(int count) {
+ // Not attached to window, don't update drop-down
+ if (getWindowVisibility() == View.GONE) return;
+
+ /*
+ * 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.
+ */
+
+ final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
+ final boolean enoughToFilter = enoughToFilter();
+ if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) {
+ if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
+ showDropDown();
+ }
+ } else if (!dropDownAlwaysVisible && isPopupShowing()) {
+ dismissDropDown();
+ // When the filter text is changed, the first update from the adapter may show an empty
+ // count (when the query is being performed on the network). Future updates when some
+ // content has been retrieved should still be able to update the list.
+ mPopupCanBeUpdated = true;
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
+ dismissDropDown();
+ }
+ }
+
+ @Override
+ protected void onDisplayHint(int hint) {
+ super.onDisplayHint(hint);
+ switch (hint) {
+ case INVISIBLE:
+ if (!mPopup.isDropDownAlwaysVisible()) {
+ dismissDropDown();
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+ if (isTemporarilyDetached()) {
+ // If we are temporarily in the detach state, then do nothing.
+ return;
+ }
+
+ // Perform validation if the view is losing focus.
+ if (!focused) {
+ performValidation();
+ }
+ if (!focused && !mPopup.isDropDownAlwaysVisible()) {
+ dismissDropDown();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ dismissDropDown();
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * <p>Closes the drop down if present on screen.</p>
+ */
+ public void dismissDropDown() {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.displayCompletions(this, null);
+ }
+ mPopup.dismiss();
+ mPopupCanBeUpdated = false;
+ }
+
+ @Override
+ protected boolean setFrame(final int l, int t, final int r, int b) {
+ boolean result = super.setFrame(l, t, r, b);
+
+ if (isPopupShowing()) {
+ showDropDown();
+ }
+
+ return result;
+ }
+
+ /**
+ * Issues a runnable to show the dropdown as soon as possible.
+ *
+ * @hide internal used only by SearchDialog
+ */
+ public void showDropDownAfterLayout() {
+ mPopup.postShow();
+ }
+
+ /**
+ * Ensures that the drop down is not obscuring the IME.
+ * @param visible whether the ime should be in front. If false, the ime is pushed to
+ * the background.
+ * @hide internal used only here and SearchDialog
+ */
+ public void ensureImeVisible(boolean visible) {
+ mPopup.setInputMethodMode(visible
+ ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+ if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) {
+ showDropDown();
+ }
+ }
+
+ /**
+ * @hide internal used only here and SearchDialog
+ */
+ public boolean isInputMethodNotNeeded() {
+ return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
+ }
+
+ /**
+ * <p>Displays the drop down on screen.</p>
+ */
+ public void showDropDown() {
+ buildImeCompletions();
+
+ if (mPopup.getAnchorView() == null) {
+ if (mDropDownAnchorId != View.NO_ID) {
+ mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
+ } else {
+ mPopup.setAnchorView(this);
+ }
+ }
+ if (!isPopupShowing()) {
+ // Make sure the list does not obscure the IME when shown for the first time.
+ mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
+ mPopup.setListItemExpandMax(EXPAND_MAX);
+ }
+ mPopup.show();
+ mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
+ }
+
+ /**
+ * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+ * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+ * ignore outside touch even when the drop down is not set to always visible.
+ *
+ * @hide used only by SearchDialog
+ */
+ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+ mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
+ }
+
+ private void buildImeCompletions() {
+ final ListAdapter adapter = mAdapter;
+ if (adapter != null) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ final int count = Math.min(adapter.getCount(), 20);
+ CompletionInfo[] completions = new CompletionInfo[count];
+ int realCount = 0;
+
+ for (int i = 0; i < count; i++) {
+ if (adapter.isEnabled(i)) {
+ Object item = adapter.getItem(i);
+ long id = adapter.getItemId(i);
+ completions[realCount] = new CompletionInfo(id, realCount,
+ convertSelectionToString(item));
+ realCount++;
+ }
+ }
+
+ if (realCount != count) {
+ CompletionInfo[] tmp = new CompletionInfo[realCount];
+ System.arraycopy(completions, 0, tmp, 0, realCount);
+ completions = tmp;
+ }
+
+ imm.displayCompletions(this, completions);
+ }
+ }
+ }
+
+ /**
+ * Sets the validator used to perform text validation.
+ *
+ * @param validator The validator used to validate the text entered in this widget.
+ *
+ * @see #getValidator()
+ * @see #performValidation()
+ */
+ public void setValidator(Validator validator) {
+ mValidator = validator;
+ }
+
+ /**
+ * Returns the Validator set with {@link #setValidator},
+ * or <code>null</code> if it was not set.
+ *
+ * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
+ * @see #performValidation()
+ */
+ 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.
+ *
+ * @see #getValidator()
+ * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
+ */
+ 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;
+ }
+
+ private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
+ performCompletion(v, position, id);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public interface Validator {
+ /**
+ * Validates the specified text.
+ *
+ * @return true If the text currently in the text editor is valid.
+ *
+ * @see #fixText(CharSequence)
+ */
+ boolean isValid(CharSequence text);
+
+ /**
+ * Corrects the specified text to make it valid.
+ *
+ * @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.
+ *
+ * @see #isValid(CharSequence)
+ */
+ CharSequence fixText(CharSequence invalidText);
+ }
+
+ /**
+ * Listener to respond to the AutoCompleteTextView's completion list being dismissed.
+ * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener)
+ */
+ public interface OnDismissListener {
+ /**
+ * This method will be invoked whenever the AutoCompleteTextView's list
+ * of completion options has been dismissed and is no longer available
+ * for user interaction.
+ */
+ void onDismiss();
+ }
+
+ /**
+ * Allows us a private hook into the on click event without preventing users from setting
+ * their own click listener.
+ */
+ private class PassThroughClickListener implements OnClickListener {
+
+ private View.OnClickListener mWrapped;
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ onClickImpl();
+
+ if (mWrapped != null) mWrapped.onClick(v);
+ }
+ }
+
+ /**
+ * Static inner listener that keeps a WeakReference to the actual AutoCompleteTextView.
+ * <p>
+ * This way, if adapter has a longer life span than the View, we won't leak the View, instead
+ * we will just leak a small Observer with 1 field.
+ */
+ private static class PopupDataSetObserver extends DataSetObserver {
+ private final WeakReference<AutoCompleteTextView> mViewReference;
+
+ private PopupDataSetObserver(AutoCompleteTextView view) {
+ mViewReference = new WeakReference<AutoCompleteTextView>(view);
+ }
+
+ @Override
+ public void onChanged() {
+ final AutoCompleteTextView textView = mViewReference.get();
+ if (textView != null && textView.mAdapter != null) {
+ // If the popup is not showing already, showing it will cause
+ // the list of data set observers attached to the adapter to
+ // change. We can't do it from here, because we are in the middle
+ // of iterating through the list of observers.
+ textView.post(updateRunnable);
+ }
+ }
+
+ private final Runnable updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final AutoCompleteTextView textView = mViewReference.get();
+ if (textView == null) {
+ return;
+ }
+ final ListAdapter adapter = textView.mAdapter;
+ if (adapter == null) {
+ return;
+ }
+ textView.updateDropDownForFilter(adapter.getCount());
+ }
+ };
+ }
+}
diff --git a/android/widget/BaseAdapter.java b/android/widget/BaseAdapter.java
new file mode 100644
index 0000000..5838cc1
--- /dev/null
+++ b/android/widget/BaseAdapter.java
@@ -0,0 +1,99 @@
+/*
+ * 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.Nullable;
+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();
+ private CharSequence[] mAutofillOptions;
+
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * Notifies the attached observers that the underlying data has been changed
+ * and any View reflecting the data set should refresh itself.
+ */
+ public void notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged();
+ }
+
+ /**
+ * Notifies the attached observers that the underlying data is no longer valid
+ * or available. Once invoked this adapter is no longer valid and should
+ * not report further data set changes.
+ */
+ 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;
+ }
+
+ @Override
+ public CharSequence[] getAutofillOptions() {
+ return mAutofillOptions;
+ }
+
+ /**
+ * Sets the value returned by {@link #getAutofillOptions()}
+ */
+ public void setAutofillOptions(@Nullable CharSequence... options) {
+ mAutofillOptions = options;
+ }
+}
diff --git a/android/widget/BaseExpandableListAdapter.java b/android/widget/BaseExpandableListAdapter.java
new file mode 100644
index 0000000..b4d6ad7
--- /dev/null
+++ b/android/widget/BaseExpandableListAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+/**
+ * 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,
+ HeterogeneousExpandableList {
+ 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 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 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 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 getCombinedGroupId(long groupId) {
+ return (groupId & 0x7FFFFFFF) << 32;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isEmpty() {
+ return getGroupCount() == 0;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ * @return 0 for any group or child position, since only one child type count is declared.
+ */
+ public int getChildType(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @return 1 as a default value in BaseExpandableListAdapter.
+ */
+ public int getChildTypeCount() {
+ return 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @return 0 for any groupPosition, since only one group type count is declared.
+ */
+ public int getGroupType(int groupPosition) {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @return 1 as a default value in BaseExpandableListAdapter.
+ */
+ public int getGroupTypeCount() {
+ return 1;
+ }
+}
diff --git a/android/widget/Button.java b/android/widget/Button.java
new file mode 100644
index 0000000..634cbe3
--- /dev/null
+++ b/android/widget/Button.java
@@ -0,0 +1,181 @@
+/*
+ * 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.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * A user interface element the user can tap or click to perform an action.
+ *
+ * <p>To display a button in an activity, add a button to the activity's layout XML file:</p>
+ *
+ * <pre>
+ * <Button
+ * android:id="@+id/button_id"
+ * android:layout_height="wrap_content"
+ * android:layout_width="wrap_content"
+ * android:text="@string/self_destruct" /></pre>
+ *
+ * <p>To specify an action when the button is pressed, set a click
+ * listener on the button object in the corresponding activity code:</p>
+ *
+ * <pre>
+ * public class MyActivity extends Activity {
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * setContentView(R.layout.content_layout_id);
+ *
+ * final Button button = findViewById(R.id.button_id);
+ * button.setOnClickListener(new View.OnClickListener() {
+ * public void onClick(View v) {
+ * // Code here executes on main thread after user presses button
+ * }
+ * });
+ * }
+ * }</pre>
+ *
+ * <p>The above snippet creates an instance of {@link android.view.View.OnClickListener} and wires
+ * the listener to the button using
+ * {@link #setOnClickListener setOnClickListener(View.OnClickListener)}.
+ * As a result, the system executes the code you write in {@code onClick(View)} after the
+ * user presses the button.</p>
+ *
+ * <p class="note">The system executes the code in {@code onClick} on the
+ * <a href="{@docRoot}guide/components/processes-and-threads.html#Threads">main thread</a>.
+ * This means your onClick code must execute quickly to avoid delaying your app's response
+ * to further user actions. See
+ * <a href="{@docRoot}training/articles/perf-anr.html">Keeping Your App Responsive</a>
+ * for more details.</p>
+ *
+ * <p>Every button is styled using the system's default button background, which is often
+ * different from one version of the platform to another. If you are not satisfied with the
+ * default button style, you can customize it. For more details and code samples, see the
+ * <a href="{@docRoot}guide/topics/ui/controls/button.html#Style">Styling Your Button</a>
+ * guide.</p>
+ *
+ * <p>For all XML style attributes available on Button see
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}. See the
+ * <a href="{@docRoot}guide/topics/ui/themes.html#ApplyingStyles">Styles and Themes</a>
+ * guide to learn how to implement and organize overrides to style-related attributes.</p>
+ */
+@RemoteView
+public class Button extends TextView {
+
+ /**
+ * Simple constructor to use when creating a button from code.
+ *
+ * @param context The Context the Button is running in, through which it can
+ * access the current theme, resources, etc.
+ *
+ * @see #Button(Context, AttributeSet)
+ */
+ public Button(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * {@link LayoutInflater} calls this constructor when inflating a Button from XML.
+ * The attributes defined by the current theme's
+ * {@link android.R.attr#buttonStyle android:buttonStyle}
+ * override base view attributes.
+ *
+ * You typically do not call this constructor to create your own button instance in code.
+ * However, you must override this constructor when
+ * <a href="{@docRoot}training/custom-views/index.html">creating custom views</a>.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML Button tag being used to inflate the view.
+ *
+ * @see #Button(Context, AttributeSet, int)
+ * @see android.view.View#View(Context, AttributeSet)
+ */
+ public Button(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.buttonStyle);
+ }
+
+ /**
+ * This constructor allows a Button subclass to use its own class-specific base style from a
+ * theme attribute when inflating. The attributes defined by the current theme's
+ * {@code defStyleAttr} override base view attributes.
+ *
+ * <p>For Button's base view attributes see
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}.
+ *
+ * @param context The Context the Button is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML Button tag that is inflating the view.
+ * @param defStyleAttr The resource identifier of an attribute in the current theme
+ * whose value is the the resource id of a style. The specified style’s
+ * attribute values serve as default values for the button. Set this parameter
+ * to 0 to avoid use of default values.
+ * @see #Button(Context, AttributeSet, int, int)
+ * @see android.view.View#View(Context, AttributeSet, int)
+ */
+ public Button(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * This constructor allows a Button subclass to use its own class-specific base style from
+ * either a theme attribute or style resource when inflating. To see how the final value of a
+ * particular attribute is resolved based on your inputs to this constructor, see
+ * {@link android.view.View#View(Context, AttributeSet, int, int)}.
+ *
+ * @param context The Context the Button is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML Button tag that is inflating the view.
+ * @param defStyleAttr The resource identifier of an attribute in the current theme
+ * whose value is the the resource id of a style. The specified style’s
+ * attribute values serve as default values for the button. Set this parameter
+ * to 0 to avoid use of default values.
+ * @param defStyleRes The identifier of a style resource that
+ * supplies default values for the button, used only if
+ * defStyleAttr is 0 or cannot be found in the theme.
+ * Set this parameter to 0 to avoid use of default values.
+ *
+ * @see #Button(Context, AttributeSet, int)
+ * @see android.view.View#View(Context, AttributeSet, int, int)
+ */
+ public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Button.class.getName();
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+}
diff --git a/android/widget/CalendarView.java b/android/widget/CalendarView.java
new file mode 100644
index 0000000..db50e34
--- /dev/null
+++ b/android/widget/CalendarView.java
@@ -0,0 +1,787 @@
+/*
+ * Copyright (C) 2010 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.AttrRes;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.icu.util.Calendar;
+import android.icu.util.TimeZone;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * This class is a calendar widget for displaying and selecting dates. The
+ * range of dates supported by this calendar is configurable.
+ * <p>
+ * The exact appearance and interaction model of this widget may vary between
+ * OS versions and themes (e.g. Holo versus Material), but in general a user
+ * can select a date by tapping on it and can scroll or fling the calendar to a
+ * desired date.
+ *
+ * @attr ref android.R.styleable#CalendarView_showWeekNumber
+ * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+ * @attr ref android.R.styleable#CalendarView_minDate
+ * @attr ref android.R.styleable#CalendarView_maxDate
+ * @attr ref android.R.styleable#CalendarView_shownWeekCount
+ * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+ * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+ * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+ * @attr ref android.R.styleable#CalendarView_weekNumberColor
+ * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+ * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+ * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+ * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+ */
+@Widget
+public class CalendarView extends FrameLayout {
+ private static final String LOG_TAG = "CalendarView";
+
+ private static final int MODE_HOLO = 0;
+ private static final int MODE_MATERIAL = 1;
+
+ private final CalendarViewDelegate mDelegate;
+
+ /**
+ * The callback used to indicate the user changes the date.
+ */
+ public interface OnDateChangeListener {
+
+ /**
+ * Called upon change of the selected day.
+ *
+ * @param view The view associated with this listener.
+ * @param year The year that was set.
+ * @param month The month that was set [0-11].
+ * @param dayOfMonth The day of the month that was set.
+ */
+ void onSelectedDayChange(@NonNull CalendarView view, int year, int month, int dayOfMonth);
+ }
+
+ public CalendarView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, R.attr.calendarViewStyle);
+ }
+
+ public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.CalendarView, defStyleAttr, defStyleRes);
+ final int mode = a.getInt(R.styleable.CalendarView_calendarViewMode, MODE_HOLO);
+ a.recycle();
+
+ switch (mode) {
+ case MODE_HOLO:
+ mDelegate = new CalendarViewLegacyDelegate(
+ this, context, attrs, defStyleAttr, defStyleRes);
+ break;
+ case MODE_MATERIAL:
+ mDelegate = new CalendarViewMaterialDelegate(
+ this, context, attrs, defStyleAttr, defStyleRes);
+ break;
+ default:
+ throw new IllegalArgumentException("invalid calendarViewMode attribute");
+ }
+ }
+
+ /**
+ * Sets the number of weeks to be shown.
+ *
+ * @param count The shown week count.
+ *
+ * @attr ref android.R.styleable#CalendarView_shownWeekCount
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setShownWeekCount(int count) {
+ mDelegate.setShownWeekCount(count);
+ }
+
+ /**
+ * Gets the number of weeks to be shown.
+ *
+ * @return The shown week count.
+ *
+ * @attr ref android.R.styleable#CalendarView_shownWeekCount
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public int getShownWeekCount() {
+ return mDelegate.getShownWeekCount();
+ }
+
+ /**
+ * Sets the background color for the selected week.
+ *
+ * @param color The week background color.
+ *
+ * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setSelectedWeekBackgroundColor(@ColorInt int color) {
+ mDelegate.setSelectedWeekBackgroundColor(color);
+ }
+
+ /**
+ * Gets the background color for the selected week.
+ *
+ * @return The week background color.
+ *
+ * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @ColorInt
+ @Deprecated
+ public int getSelectedWeekBackgroundColor() {
+ return mDelegate.getSelectedWeekBackgroundColor();
+ }
+
+ /**
+ * Sets the color for the dates of the focused month.
+ *
+ * @param color The focused month date color.
+ *
+ * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setFocusedMonthDateColor(@ColorInt int color) {
+ mDelegate.setFocusedMonthDateColor(color);
+ }
+
+ /**
+ * Gets the color for the dates in the focused month.
+ *
+ * @return The focused month date color.
+ *
+ * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @ColorInt
+ @Deprecated
+ public int getFocusedMonthDateColor() {
+ return mDelegate.getFocusedMonthDateColor();
+ }
+
+ /**
+ * Sets the color for the dates of a not focused month.
+ *
+ * @param color A not focused month date color.
+ *
+ * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setUnfocusedMonthDateColor(@ColorInt int color) {
+ mDelegate.setUnfocusedMonthDateColor(color);
+ }
+
+ /**
+ * Gets the color for the dates in a not focused month.
+ *
+ * @return A not focused month date color.
+ *
+ * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @ColorInt
+ @Deprecated
+ public int getUnfocusedMonthDateColor() {
+ return mDelegate.getUnfocusedMonthDateColor();
+ }
+
+ /**
+ * Sets the color for the week numbers.
+ *
+ * @param color The week number color.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekNumberColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setWeekNumberColor(@ColorInt int color) {
+ mDelegate.setWeekNumberColor(color);
+ }
+
+ /**
+ * Gets the color for the week numbers.
+ *
+ * @return The week number color.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekNumberColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @ColorInt
+ @Deprecated
+ public int getWeekNumberColor() {
+ return mDelegate.getWeekNumberColor();
+ }
+
+ /**
+ * Sets the color for the separator line between weeks.
+ *
+ * @param color The week separator color.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setWeekSeparatorLineColor(@ColorInt int color) {
+ mDelegate.setWeekSeparatorLineColor(color);
+ }
+
+ /**
+ * Gets the color for the separator line between weeks.
+ *
+ * @return The week separator color.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @ColorInt
+ @Deprecated
+ public int getWeekSeparatorLineColor() {
+ return mDelegate.getWeekSeparatorLineColor();
+ }
+
+ /**
+ * Sets the drawable for the vertical bar shown at the beginning and at
+ * the end of the selected date.
+ *
+ * @param resourceId The vertical bar drawable resource id.
+ *
+ * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setSelectedDateVerticalBar(@DrawableRes int resourceId) {
+ mDelegate.setSelectedDateVerticalBar(resourceId);
+ }
+
+ /**
+ * Sets the drawable for the vertical bar shown at the beginning and at
+ * the end of the selected date.
+ *
+ * @param drawable The vertical bar drawable.
+ *
+ * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public void setSelectedDateVerticalBar(Drawable drawable) {
+ mDelegate.setSelectedDateVerticalBar(drawable);
+ }
+
+ /**
+ * Gets the drawable for the vertical bar shown at the beginning and at
+ * the end of the selected date.
+ *
+ * @return The vertical bar drawable.
+ * @deprecated No longer used by Material-style CalendarView.
+ */
+ @Deprecated
+ public Drawable getSelectedDateVerticalBar() {
+ return mDelegate.getSelectedDateVerticalBar();
+ }
+
+ /**
+ * Sets the text appearance for the week day abbreviation of the calendar header.
+ *
+ * @param resourceId The text appearance resource id.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+ */
+ public void setWeekDayTextAppearance(@StyleRes int resourceId) {
+ mDelegate.setWeekDayTextAppearance(resourceId);
+ }
+
+ /**
+ * Gets the text appearance for the week day abbreviation of the calendar header.
+ *
+ * @return The text appearance resource id.
+ *
+ * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+ */
+ public @StyleRes int getWeekDayTextAppearance() {
+ return mDelegate.getWeekDayTextAppearance();
+ }
+
+ /**
+ * Sets the text appearance for the calendar dates.
+ *
+ * @param resourceId The text appearance resource id.
+ *
+ * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+ */
+ public void setDateTextAppearance(@StyleRes int resourceId) {
+ mDelegate.setDateTextAppearance(resourceId);
+ }
+
+ /**
+ * Gets the text appearance for the calendar dates.
+ *
+ * @return The text appearance resource id.
+ *
+ * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+ */
+ public @StyleRes int getDateTextAppearance() {
+ return mDelegate.getDateTextAppearance();
+ }
+
+ /**
+ * Gets the minimal date supported by this {@link CalendarView} in milliseconds
+ * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+ * zone.
+ * <p>
+ * Note: The default minimal date is 01/01/1900.
+ * <p>
+ *
+ * @return The minimal supported date.
+ *
+ * @attr ref android.R.styleable#CalendarView_minDate
+ */
+ public long getMinDate() {
+ return mDelegate.getMinDate();
+ }
+
+ /**
+ * Sets the minimal date supported by this {@link CalendarView} in milliseconds
+ * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+ * zone.
+ *
+ * @param minDate The minimal supported date.
+ *
+ * @attr ref android.R.styleable#CalendarView_minDate
+ */
+ public void setMinDate(long minDate) {
+ mDelegate.setMinDate(minDate);
+ }
+
+ /**
+ * Gets the maximal date supported by this {@link CalendarView} in milliseconds
+ * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+ * zone.
+ * <p>
+ * Note: The default maximal date is 01/01/2100.
+ * <p>
+ *
+ * @return The maximal supported date.
+ *
+ * @attr ref android.R.styleable#CalendarView_maxDate
+ */
+ public long getMaxDate() {
+ return mDelegate.getMaxDate();
+ }
+
+ /**
+ * Sets the maximal date supported by this {@link CalendarView} in milliseconds
+ * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+ * zone.
+ *
+ * @param maxDate The maximal supported date.
+ *
+ * @attr ref android.R.styleable#CalendarView_maxDate
+ */
+ public void setMaxDate(long maxDate) {
+ mDelegate.setMaxDate(maxDate);
+ }
+
+ /**
+ * Sets whether to show the week number.
+ *
+ * @param showWeekNumber True to show the week number.
+ * @deprecated No longer used by Material-style CalendarView.
+ *
+ * @attr ref android.R.styleable#CalendarView_showWeekNumber
+ */
+ @Deprecated
+ public void setShowWeekNumber(boolean showWeekNumber) {
+ mDelegate.setShowWeekNumber(showWeekNumber);
+ }
+
+ /**
+ * Gets whether to show the week number.
+ *
+ * @return True if showing the week number.
+ * @deprecated No longer used by Material-style CalendarView.
+ *
+ * @attr ref android.R.styleable#CalendarView_showWeekNumber
+ */
+ @Deprecated
+ public boolean getShowWeekNumber() {
+ return mDelegate.getShowWeekNumber();
+ }
+
+ /**
+ * Gets the first day of week.
+ *
+ * @return The first day of the week conforming to the {@link CalendarView}
+ * APIs.
+ * @see Calendar#MONDAY
+ * @see Calendar#TUESDAY
+ * @see Calendar#WEDNESDAY
+ * @see Calendar#THURSDAY
+ * @see Calendar#FRIDAY
+ * @see Calendar#SATURDAY
+ * @see Calendar#SUNDAY
+ *
+ * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+ */
+ public int getFirstDayOfWeek() {
+ return mDelegate.getFirstDayOfWeek();
+ }
+
+ /**
+ * Sets the first day of week.
+ *
+ * @param firstDayOfWeek The first day of the week conforming to the
+ * {@link CalendarView} APIs.
+ * @see Calendar#MONDAY
+ * @see Calendar#TUESDAY
+ * @see Calendar#WEDNESDAY
+ * @see Calendar#THURSDAY
+ * @see Calendar#FRIDAY
+ * @see Calendar#SATURDAY
+ * @see Calendar#SUNDAY
+ *
+ * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+ */
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ mDelegate.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ /**
+ * Sets the listener to be notified upon selected date change.
+ *
+ * @param listener The listener to be notified.
+ */
+ public void setOnDateChangeListener(OnDateChangeListener listener) {
+ mDelegate.setOnDateChangeListener(listener);
+ }
+
+ /**
+ * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @return The selected date.
+ */
+ public long getDate() {
+ return mDelegate.getDate();
+ }
+
+ /**
+ * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @param date The selected date.
+ *
+ * @throws IllegalArgumentException of the provided date is before the
+ * minimal or after the maximal date.
+ *
+ * @see #setDate(long, boolean, boolean)
+ * @see #setMinDate(long)
+ * @see #setMaxDate(long)
+ */
+ public void setDate(long date) {
+ mDelegate.setDate(date);
+ }
+
+ /**
+ * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @param date The date.
+ * @param animate Whether to animate the scroll to the current date.
+ * @param center Whether to center the current date even if it is already visible.
+ *
+ * @throws IllegalArgumentException of the provided date is before the
+ * minimal or after the maximal date.
+ *
+ * @see #setMinDate(long)
+ * @see #setMaxDate(long)
+ */
+ public void setDate(long date, boolean animate, boolean center) {
+ mDelegate.setDate(date, animate, center);
+ }
+
+ /**
+ * Retrieves the screen bounds for the specific date in the coordinate system of this
+ * view. If the passed date is being currently displayed, this method returns true and
+ * the caller can query the fields of the passed {@link Rect} object. Otherwise the
+ * method returns false and does not touch the passed {@link Rect} object.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean getBoundsForDate(long date, Rect outBounds) {
+ return mDelegate.getBoundsForDate(date, outBounds);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDelegate.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return CalendarView.class.getName();
+ }
+
+ /**
+ * A delegate interface that defined the public API of the CalendarView. Allows different
+ * CalendarView implementations. This would need to be implemented by the CalendarView delegates
+ * for the real behavior.
+ */
+ private interface CalendarViewDelegate {
+ void setShownWeekCount(int count);
+ int getShownWeekCount();
+
+ void setSelectedWeekBackgroundColor(@ColorInt int color);
+ @ColorInt int getSelectedWeekBackgroundColor();
+
+ void setFocusedMonthDateColor(@ColorInt int color);
+ @ColorInt int getFocusedMonthDateColor();
+
+ void setUnfocusedMonthDateColor(@ColorInt int color);
+ @ColorInt int getUnfocusedMonthDateColor();
+
+ void setWeekNumberColor(@ColorInt int color);
+ @ColorInt int getWeekNumberColor();
+
+ void setWeekSeparatorLineColor(@ColorInt int color);
+ @ColorInt int getWeekSeparatorLineColor();
+
+ void setSelectedDateVerticalBar(@DrawableRes int resourceId);
+ void setSelectedDateVerticalBar(Drawable drawable);
+ Drawable getSelectedDateVerticalBar();
+
+ void setWeekDayTextAppearance(@StyleRes int resourceId);
+ @StyleRes int getWeekDayTextAppearance();
+
+ void setDateTextAppearance(@StyleRes int resourceId);
+ @StyleRes int getDateTextAppearance();
+
+ void setMinDate(long minDate);
+ long getMinDate();
+
+ void setMaxDate(long maxDate);
+ long getMaxDate();
+
+ void setShowWeekNumber(boolean showWeekNumber);
+ boolean getShowWeekNumber();
+
+ void setFirstDayOfWeek(int firstDayOfWeek);
+ int getFirstDayOfWeek();
+
+ void setDate(long date);
+ void setDate(long date, boolean animate, boolean center);
+ long getDate();
+
+ boolean getBoundsForDate(long date, Rect outBounds);
+
+ void setOnDateChangeListener(OnDateChangeListener listener);
+
+ void onConfigurationChanged(Configuration newConfig);
+ }
+
+ /**
+ * An abstract class which can be used as a start for CalendarView implementations
+ */
+ abstract static class AbstractCalendarViewDelegate implements CalendarViewDelegate {
+ /** The default minimal date. */
+ protected static final String DEFAULT_MIN_DATE = "01/01/1900";
+
+ /** The default maximal date. */
+ protected static final String DEFAULT_MAX_DATE = "01/01/2100";
+
+ protected CalendarView mDelegator;
+ protected Context mContext;
+ protected Locale mCurrentLocale;
+
+ AbstractCalendarViewDelegate(CalendarView delegator, Context context) {
+ mDelegator = delegator;
+ mContext = context;
+
+ // Initialization based on locale
+ setCurrentLocale(Locale.getDefault());
+ }
+
+ protected void setCurrentLocale(Locale locale) {
+ if (locale.equals(mCurrentLocale)) {
+ return;
+ }
+ mCurrentLocale = locale;
+ }
+
+ @Override
+ public void setShownWeekCount(int count) {
+ // Deprecated.
+ }
+
+ @Override
+ public int getShownWeekCount() {
+ // Deprecated.
+ return 0;
+ }
+
+ @Override
+ public void setSelectedWeekBackgroundColor(@ColorInt int color) {
+ // Deprecated.
+ }
+
+ @ColorInt
+ @Override
+ public int getSelectedWeekBackgroundColor() {
+ return 0;
+ }
+
+ @Override
+ public void setFocusedMonthDateColor(@ColorInt int color) {
+ // Deprecated.
+ }
+
+ @ColorInt
+ @Override
+ public int getFocusedMonthDateColor() {
+ return 0;
+ }
+
+ @Override
+ public void setUnfocusedMonthDateColor(@ColorInt int color) {
+ // Deprecated.
+ }
+
+ @ColorInt
+ @Override
+ public int getUnfocusedMonthDateColor() {
+ return 0;
+ }
+
+ @Override
+ public void setWeekNumberColor(@ColorInt int color) {
+ // Deprecated.
+ }
+
+ @ColorInt
+ @Override
+ public int getWeekNumberColor() {
+ // Deprecated.
+ return 0;
+ }
+
+ @Override
+ public void setWeekSeparatorLineColor(@ColorInt int color) {
+ // Deprecated.
+ }
+
+ @ColorInt
+ @Override
+ public int getWeekSeparatorLineColor() {
+ // Deprecated.
+ return 0;
+ }
+
+ @Override
+ public void setSelectedDateVerticalBar(@DrawableRes int resId) {
+ // Deprecated.
+ }
+
+ @Override
+ public void setSelectedDateVerticalBar(Drawable drawable) {
+ // Deprecated.
+ }
+
+ @Override
+ public Drawable getSelectedDateVerticalBar() {
+ // Deprecated.
+ return null;
+ }
+
+ @Override
+ public void setShowWeekNumber(boolean showWeekNumber) {
+ // Deprecated.
+ }
+
+ @Override
+ public boolean getShowWeekNumber() {
+ // Deprecated.
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Nothing to do here, configuration changes are already propagated
+ // by ViewGroup.
+ }
+ }
+
+ /** String for parsing dates. */
+ private static final String DATE_FORMAT = "MM/dd/yyyy";
+
+ /** Date format for parsing dates. */
+ private static final DateFormat DATE_FORMATTER = new SimpleDateFormat(DATE_FORMAT);
+
+ /**
+ * Utility method for the date format used by CalendarView's min/max date.
+ *
+ * @hide Use only as directed. For internal use only.
+ */
+ public static boolean parseDate(String date, Calendar outDate) {
+ if (date == null || date.isEmpty()) {
+ return false;
+ }
+
+ try {
+ final Date parsedDate = DATE_FORMATTER.parse(date);
+ outDate.setTime(parsedDate);
+ return true;
+ } catch (ParseException e) {
+ Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
+ return false;
+ }
+ }
+}
diff --git a/android/widget/CalendarViewLegacyDelegate.java b/android/widget/CalendarViewLegacyDelegate.java
new file mode 100644
index 0000000..1b899db
--- /dev/null
+++ b/android/widget/CalendarViewLegacyDelegate.java
@@ -0,0 +1,1578 @@
+/*
+ * Copyright (C) 2014 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.Service;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.icu.util.Calendar;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Locale;
+
+/**
+ * A delegate implementing the legacy CalendarView
+ */
+class CalendarViewLegacyDelegate extends CalendarView.AbstractCalendarViewDelegate {
+ /**
+ * Default value whether to show week number.
+ */
+ private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;
+
+ /**
+ * The number of milliseconds in a day.e
+ */
+ private static final long MILLIS_IN_DAY = 86400000L;
+
+ /**
+ * The number of day in a week.
+ */
+ private static final int DAYS_PER_WEEK = 7;
+
+ /**
+ * The number of milliseconds in a week.
+ */
+ private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;
+
+ /**
+ * Affects when the month selection will change while scrolling upe
+ */
+ private static final int SCROLL_HYST_WEEKS = 2;
+
+ /**
+ * How long the GoTo fling animation should last.
+ */
+ private static final int GOTO_SCROLL_DURATION = 1000;
+
+ /**
+ * The duration of the adjustment upon a user scroll in milliseconds.
+ */
+ private static final int ADJUSTMENT_SCROLL_DURATION = 500;
+
+ /**
+ * How long to wait after receiving an onScrollStateChanged notification
+ * before acting on it.
+ */
+ private static final int SCROLL_CHANGE_DELAY = 40;
+
+ private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;
+
+ private static final int DEFAULT_DATE_TEXT_SIZE = 14;
+
+ private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;
+
+ private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;
+
+ private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;
+
+ private static final int UNSCALED_BOTTOM_BUFFER = 20;
+
+ private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;
+
+ private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;
+
+ private final int mWeekSeparatorLineWidth;
+
+ private int mDateTextSize;
+
+ private Drawable mSelectedDateVerticalBar;
+
+ private final int mSelectedDateVerticalBarWidth;
+
+ private int mSelectedWeekBackgroundColor;
+
+ private int mFocusedMonthDateColor;
+
+ private int mUnfocusedMonthDateColor;
+
+ private int mWeekSeparatorLineColor;
+
+ private int mWeekNumberColor;
+
+ private int mWeekDayTextAppearanceResId;
+
+ private int mDateTextAppearanceResId;
+
+ /**
+ * The top offset of the weeks list.
+ */
+ private int mListScrollTopOffset = 2;
+
+ /**
+ * The visible height of a week view.
+ */
+ private int mWeekMinVisibleHeight = 12;
+
+ /**
+ * The visible height of a week view.
+ */
+ private int mBottomBuffer = 20;
+
+ /**
+ * The number of shown weeks.
+ */
+ private int mShownWeekCount;
+
+ /**
+ * Flag whether to show the week number.
+ */
+ private boolean mShowWeekNumber;
+
+ /**
+ * The number of day per week to be shown.
+ */
+ private int mDaysPerWeek = 7;
+
+ /**
+ * The friction of the week list while flinging.
+ */
+ private float mFriction = .05f;
+
+ /**
+ * Scale for adjusting velocity of the week list while flinging.
+ */
+ private float mVelocityScale = 0.333f;
+
+ /**
+ * The adapter for the weeks list.
+ */
+ private WeeksAdapter mAdapter;
+
+ /**
+ * The weeks list.
+ */
+ private ListView mListView;
+
+ /**
+ * The name of the month to display.
+ */
+ private TextView mMonthName;
+
+ /**
+ * The header with week day names.
+ */
+ private ViewGroup mDayNamesHeader;
+
+ /**
+ * Cached abbreviations for day of week names.
+ */
+ private String[] mDayNamesShort;
+
+ /**
+ * Cached full-length day of week names.
+ */
+ private String[] mDayNamesLong;
+
+ /**
+ * The first day of the week.
+ */
+ private int mFirstDayOfWeek;
+
+ /**
+ * Which month should be displayed/highlighted [0-11].
+ */
+ private int mCurrentMonthDisplayed = -1;
+
+ /**
+ * Used for tracking during a scroll.
+ */
+ private long mPreviousScrollPosition;
+
+ /**
+ * Used for tracking which direction the view is scrolling.
+ */
+ private boolean mIsScrollingUp = false;
+
+ /**
+ * The previous scroll state of the weeks ListView.
+ */
+ private int mPreviousScrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
+
+ /**
+ * The current scroll state of the weeks ListView.
+ */
+ private int mCurrentScrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
+
+ /**
+ * Listener for changes in the selected day.
+ */
+ private CalendarView.OnDateChangeListener mOnDateChangeListener;
+
+ /**
+ * Command for adjusting the position after a scroll/fling.
+ */
+ private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
+
+ /**
+ * Temporary instance to avoid multiple instantiations.
+ */
+ private Calendar mTempDate;
+
+ /**
+ * The first day of the focused month.
+ */
+ private Calendar mFirstDayOfMonth;
+
+ /**
+ * The start date of the range supported by this picker.
+ */
+ private Calendar mMinDate;
+
+ /**
+ * The end date of the range supported by this picker.
+ */
+ private Calendar mMaxDate;
+
+ CalendarViewLegacyDelegate(CalendarView delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CalendarView, defStyleAttr, defStyleRes);
+ mShowWeekNumber = a.getBoolean(R.styleable.CalendarView_showWeekNumber,
+ DEFAULT_SHOW_WEEK_NUMBER);
+ mFirstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
+ LocaleData.get(Locale.getDefault()).firstDayOfWeek);
+ final String minDate = a.getString(R.styleable.CalendarView_minDate);
+ if (!CalendarView.parseDate(minDate, mMinDate)) {
+ CalendarView.parseDate(DEFAULT_MIN_DATE, mMinDate);
+ }
+ final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
+ if (!CalendarView.parseDate(maxDate, mMaxDate)) {
+ CalendarView.parseDate(DEFAULT_MAX_DATE, mMaxDate);
+ }
+ if (mMaxDate.before(mMinDate)) {
+ throw new IllegalArgumentException("Max date cannot be before min date.");
+ }
+ mShownWeekCount = a.getInt(R.styleable.CalendarView_shownWeekCount,
+ DEFAULT_SHOWN_WEEK_COUNT);
+ mSelectedWeekBackgroundColor = a.getColor(
+ R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
+ mFocusedMonthDateColor = a.getColor(
+ R.styleable.CalendarView_focusedMonthDateColor, 0);
+ mUnfocusedMonthDateColor = a.getColor(
+ R.styleable.CalendarView_unfocusedMonthDateColor, 0);
+ mWeekSeparatorLineColor = a.getColor(
+ R.styleable.CalendarView_weekSeparatorLineColor, 0);
+ mWeekNumberColor = a.getColor(R.styleable.CalendarView_weekNumberColor, 0);
+ mSelectedDateVerticalBar = a.getDrawable(
+ R.styleable.CalendarView_selectedDateVerticalBar);
+
+ mDateTextAppearanceResId = a.getResourceId(
+ R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
+ updateDateTextSize();
+
+ mWeekDayTextAppearanceResId = a.getResourceId(
+ R.styleable.CalendarView_weekDayTextAppearance,
+ DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
+ a.recycle();
+
+ DisplayMetrics displayMetrics = mDelegator.getResources().getDisplayMetrics();
+ mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
+ mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
+ mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ UNSCALED_BOTTOM_BUFFER, displayMetrics);
+ mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
+ mWeekSeparatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);
+
+ LayoutInflater layoutInflater = (LayoutInflater) mContext
+ .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
+ View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
+ mDelegator.addView(content);
+
+ mListView = mDelegator.findViewById(R.id.list);
+ mDayNamesHeader = content.findViewById(R.id.day_names);
+ mMonthName = content.findViewById(R.id.month_name);
+
+ setUpHeader();
+ setUpListView();
+ setUpAdapter();
+
+ // go to today or whichever is close to today min or max date
+ mTempDate.setTimeInMillis(System.currentTimeMillis());
+ if (mTempDate.before(mMinDate)) {
+ goTo(mMinDate, false, true, true);
+ } else if (mMaxDate.before(mTempDate)) {
+ goTo(mMaxDate, false, true, true);
+ } else {
+ goTo(mTempDate, false, true, true);
+ }
+
+ mDelegator.invalidate();
+ }
+
+ @Override
+ public void setShownWeekCount(int count) {
+ if (mShownWeekCount != count) {
+ mShownWeekCount = count;
+ mDelegator.invalidate();
+ }
+ }
+
+ @Override
+ public int getShownWeekCount() {
+ return mShownWeekCount;
+ }
+
+ @Override
+ public void setSelectedWeekBackgroundColor(int color) {
+ if (mSelectedWeekBackgroundColor != color) {
+ mSelectedWeekBackgroundColor = color;
+ final int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ WeekView weekView = (WeekView) mListView.getChildAt(i);
+ if (weekView.mHasSelectedDay) {
+ weekView.invalidate();
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getSelectedWeekBackgroundColor() {
+ return mSelectedWeekBackgroundColor;
+ }
+
+ @Override
+ public void setFocusedMonthDateColor(int color) {
+ if (mFocusedMonthDateColor != color) {
+ mFocusedMonthDateColor = color;
+ final int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ WeekView weekView = (WeekView) mListView.getChildAt(i);
+ if (weekView.mHasFocusedDay) {
+ weekView.invalidate();
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getFocusedMonthDateColor() {
+ return mFocusedMonthDateColor;
+ }
+
+ @Override
+ public void setUnfocusedMonthDateColor(int color) {
+ if (mUnfocusedMonthDateColor != color) {
+ mUnfocusedMonthDateColor = color;
+ final int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ WeekView weekView = (WeekView) mListView.getChildAt(i);
+ if (weekView.mHasUnfocusedDay) {
+ weekView.invalidate();
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getUnfocusedMonthDateColor() {
+ return mUnfocusedMonthDateColor;
+ }
+
+ @Override
+ public void setWeekNumberColor(int color) {
+ if (mWeekNumberColor != color) {
+ mWeekNumberColor = color;
+ if (mShowWeekNumber) {
+ invalidateAllWeekViews();
+ }
+ }
+ }
+
+ @Override
+ public int getWeekNumberColor() {
+ return mWeekNumberColor;
+ }
+
+ @Override
+ public void setWeekSeparatorLineColor(int color) {
+ if (mWeekSeparatorLineColor != color) {
+ mWeekSeparatorLineColor = color;
+ invalidateAllWeekViews();
+ }
+ }
+
+ @Override
+ public int getWeekSeparatorLineColor() {
+ return mWeekSeparatorLineColor;
+ }
+
+ @Override
+ public void setSelectedDateVerticalBar(int resourceId) {
+ Drawable drawable = mDelegator.getContext().getDrawable(resourceId);
+ setSelectedDateVerticalBar(drawable);
+ }
+
+ @Override
+ public void setSelectedDateVerticalBar(Drawable drawable) {
+ if (mSelectedDateVerticalBar != drawable) {
+ mSelectedDateVerticalBar = drawable;
+ final int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ WeekView weekView = (WeekView) mListView.getChildAt(i);
+ if (weekView.mHasSelectedDay) {
+ weekView.invalidate();
+ }
+ }
+ }
+ }
+
+ @Override
+ public Drawable getSelectedDateVerticalBar() {
+ return mSelectedDateVerticalBar;
+ }
+
+ @Override
+ public void setWeekDayTextAppearance(int resourceId) {
+ if (mWeekDayTextAppearanceResId != resourceId) {
+ mWeekDayTextAppearanceResId = resourceId;
+ setUpHeader();
+ }
+ }
+
+ @Override
+ public int getWeekDayTextAppearance() {
+ return mWeekDayTextAppearanceResId;
+ }
+
+ @Override
+ public void setDateTextAppearance(int resourceId) {
+ if (mDateTextAppearanceResId != resourceId) {
+ mDateTextAppearanceResId = resourceId;
+ updateDateTextSize();
+ invalidateAllWeekViews();
+ }
+ }
+
+ @Override
+ public int getDateTextAppearance() {
+ return mDateTextAppearanceResId;
+ }
+
+ @Override
+ public void setMinDate(long minDate) {
+ mTempDate.setTimeInMillis(minDate);
+ if (isSameDate(mTempDate, mMinDate)) {
+ return;
+ }
+ mMinDate.setTimeInMillis(minDate);
+ // make sure the current date is not earlier than
+ // the new min date since the latter is used for
+ // calculating the indices in the adapter thus
+ // avoiding out of bounds error
+ Calendar date = mAdapter.mSelectedDate;
+ if (date.before(mMinDate)) {
+ mAdapter.setSelectedDay(mMinDate);
+ }
+ // reinitialize the adapter since its range depends on min date
+ mAdapter.init();
+ if (date.before(mMinDate)) {
+ setDate(mTempDate.getTimeInMillis());
+ } else {
+ // we go to the current date to force the ListView to query its
+ // adapter for the shown views since we have changed the adapter
+ // range and the base from which the later calculates item indices
+ // note that calling setDate will not work since the date is the same
+ goTo(date, false, true, false);
+ }
+ }
+
+ @Override
+ public long getMinDate() {
+ return mMinDate.getTimeInMillis();
+ }
+
+ @Override
+ public void setMaxDate(long maxDate) {
+ mTempDate.setTimeInMillis(maxDate);
+ if (isSameDate(mTempDate, mMaxDate)) {
+ return;
+ }
+ mMaxDate.setTimeInMillis(maxDate);
+ // reinitialize the adapter since its range depends on max date
+ mAdapter.init();
+ Calendar date = mAdapter.mSelectedDate;
+ if (date.after(mMaxDate)) {
+ setDate(mMaxDate.getTimeInMillis());
+ } else {
+ // we go to the current date to force the ListView to query its
+ // adapter for the shown views since we have changed the adapter
+ // range and the base from which the later calculates item indices
+ // note that calling setDate will not work since the date is the same
+ goTo(date, false, true, false);
+ }
+ }
+
+ @Override
+ public long getMaxDate() {
+ return mMaxDate.getTimeInMillis();
+ }
+
+ @Override
+ public void setShowWeekNumber(boolean showWeekNumber) {
+ if (mShowWeekNumber == showWeekNumber) {
+ return;
+ }
+ mShowWeekNumber = showWeekNumber;
+ mAdapter.notifyDataSetChanged();
+ setUpHeader();
+ }
+
+ @Override
+ public boolean getShowWeekNumber() {
+ return mShowWeekNumber;
+ }
+
+ @Override
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ if (mFirstDayOfWeek == firstDayOfWeek) {
+ return;
+ }
+ mFirstDayOfWeek = firstDayOfWeek;
+ mAdapter.init();
+ mAdapter.notifyDataSetChanged();
+ setUpHeader();
+ }
+
+ @Override
+ public int getFirstDayOfWeek() {
+ return mFirstDayOfWeek;
+ }
+
+ @Override
+ public void setDate(long date) {
+ setDate(date, false, false);
+ }
+
+ @Override
+ public void setDate(long date, boolean animate, boolean center) {
+ mTempDate.setTimeInMillis(date);
+ if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
+ return;
+ }
+ goTo(mTempDate, animate, true, center);
+ }
+
+ @Override
+ public long getDate() {
+ return mAdapter.mSelectedDate.getTimeInMillis();
+ }
+
+ @Override
+ public void setOnDateChangeListener(CalendarView.OnDateChangeListener listener) {
+ mOnDateChangeListener = listener;
+ }
+
+ @Override
+ public boolean getBoundsForDate(long date, Rect outBounds) {
+ Calendar calendarDate = Calendar.getInstance();
+ calendarDate.setTimeInMillis(date);
+ int listViewEntryCount = mListView.getCount();
+ for (int i = 0; i < listViewEntryCount; i++) {
+ WeekView currWeekView = (WeekView) mListView.getChildAt(i);
+ if (currWeekView.getBoundsForDate(calendarDate, outBounds)) {
+ // Found the date in this week. Now need to offset vertically to return correct
+ // bounds in the coordinate system of the entire layout
+ final int[] weekViewPositionOnScreen = new int[2];
+ final int[] delegatorPositionOnScreen = new int[2];
+ currWeekView.getLocationOnScreen(weekViewPositionOnScreen);
+ mDelegator.getLocationOnScreen(delegatorPositionOnScreen);
+ final int extraVerticalOffset =
+ weekViewPositionOnScreen[1] - delegatorPositionOnScreen[1];
+ outBounds.top += extraVerticalOffset;
+ outBounds.bottom += extraVerticalOffset;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ setCurrentLocale(newConfig.locale);
+ }
+
+ /**
+ * Sets the current locale.
+ *
+ * @param locale The current locale.
+ */
+ @Override
+ protected void setCurrentLocale(Locale locale) {
+ super.setCurrentLocale(locale);
+
+ mTempDate = getCalendarForLocale(mTempDate, locale);
+ mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
+ mMinDate = getCalendarForLocale(mMinDate, locale);
+ mMaxDate = getCalendarForLocale(mMaxDate, locale);
+ }
+ private void updateDateTextSize() {
+ TypedArray dateTextAppearance = mDelegator.getContext().obtainStyledAttributes(
+ mDateTextAppearanceResId, R.styleable.TextAppearance);
+ mDateTextSize = dateTextAppearance.getDimensionPixelSize(
+ R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
+ dateTextAppearance.recycle();
+ }
+
+ /**
+ * Invalidates all week views.
+ */
+ private void invalidateAllWeekViews() {
+ final int childCount = mListView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View view = mListView.getChildAt(i);
+ view.invalidate();
+ }
+ }
+
+ /**
+ * Gets a calendar for locale bootstrapped with the value of a given calendar.
+ *
+ * @param oldCalendar The old calendar.
+ * @param locale The locale.
+ */
+ private static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+ if (oldCalendar == null) {
+ return Calendar.getInstance(locale);
+ } else {
+ final long currentTimeMillis = oldCalendar.getTimeInMillis();
+ Calendar newCalendar = Calendar.getInstance(locale);
+ newCalendar.setTimeInMillis(currentTimeMillis);
+ return newCalendar;
+ }
+ }
+
+ /**
+ * @return True if the <code>firstDate</code> is the same as the <code>
+ * secondDate</code>.
+ */
+ private static boolean isSameDate(Calendar firstDate, Calendar secondDate) {
+ return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
+ && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
+ }
+
+ /**
+ * Creates a new adapter if necessary and sets up its parameters.
+ */
+ private void setUpAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new WeeksAdapter(mContext);
+ mAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (mOnDateChangeListener != null) {
+ Calendar selectedDay = mAdapter.getSelectedDay();
+ mOnDateChangeListener.onSelectedDayChange(mDelegator,
+ selectedDay.get(Calendar.YEAR),
+ selectedDay.get(Calendar.MONTH),
+ selectedDay.get(Calendar.DAY_OF_MONTH));
+ }
+ }
+ });
+ mListView.setAdapter(mAdapter);
+ }
+
+ // refresh the view with the new parameters
+ mAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Sets up the strings to be used by the header.
+ */
+ private void setUpHeader() {
+ mDayNamesShort = new String[mDaysPerWeek];
+ mDayNamesLong = new String[mDaysPerWeek];
+ for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
+ int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
+ mDayNamesShort[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
+ DateUtils.LENGTH_SHORTEST);
+ mDayNamesLong[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
+ DateUtils.LENGTH_LONG);
+ }
+
+ TextView label = (TextView) mDayNamesHeader.getChildAt(0);
+ if (mShowWeekNumber) {
+ label.setVisibility(View.VISIBLE);
+ } else {
+ label.setVisibility(View.GONE);
+ }
+ for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
+ label = (TextView) mDayNamesHeader.getChildAt(i);
+ if (mWeekDayTextAppearanceResId > -1) {
+ label.setTextAppearance(mWeekDayTextAppearanceResId);
+ }
+ if (i < mDaysPerWeek + 1) {
+ label.setText(mDayNamesShort[i - 1]);
+ label.setContentDescription(mDayNamesLong[i - 1]);
+ label.setVisibility(View.VISIBLE);
+ } else {
+ label.setVisibility(View.GONE);
+ }
+ }
+ mDayNamesHeader.invalidate();
+ }
+
+ /**
+ * Sets all the required fields for the list view.
+ */
+ private void setUpListView() {
+ // Configure the listview
+ mListView.setDivider(null);
+ mListView.setItemsCanFocus(true);
+ mListView.setVerticalScrollBarEnabled(false);
+ mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ CalendarViewLegacyDelegate.this.onScrollStateChanged(view, scrollState);
+ }
+
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ CalendarViewLegacyDelegate.this.onScroll(view, firstVisibleItem,
+ visibleItemCount, totalItemCount);
+ }
+ });
+ // Make the scrolling behavior nicer
+ mListView.setFriction(mFriction);
+ mListView.setVelocityScale(mVelocityScale);
+ }
+
+ /**
+ * This moves to the specified time in the view. If the time is not already
+ * in range it will move the list so that the first of the month containing
+ * the time is at the top of the view. If the new time is already in view
+ * the list will not be scrolled unless forceScroll is true. This time may
+ * optionally be highlighted as selected as well.
+ *
+ * @param date The time to move to.
+ * @param animate Whether to scroll to the given time or just redraw at the
+ * new location.
+ * @param setSelected Whether to set the given time as selected.
+ * @param forceScroll Whether to recenter even if the time is already
+ * visible.
+ *
+ * @throws IllegalArgumentException if the provided date is before the
+ * range start or after the range end.
+ */
+ private void goTo(Calendar date, boolean animate, boolean setSelected,
+ boolean forceScroll) {
+ if (date.before(mMinDate) || date.after(mMaxDate)) {
+ throw new IllegalArgumentException("timeInMillis must be between the values of "
+ + "getMinDate() and getMaxDate()");
+ }
+ // Find the first and last entirely visible weeks
+ int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
+ View firstChild = mListView.getChildAt(0);
+ if (firstChild != null && firstChild.getTop() < 0) {
+ firstFullyVisiblePosition++;
+ }
+ int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
+ if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
+ lastFullyVisiblePosition--;
+ }
+ if (setSelected) {
+ mAdapter.setSelectedDay(date);
+ }
+ // Get the week we're going to
+ int position = getWeeksSinceMinDate(date);
+
+ // Check if the selected day is now outside of our visible range
+ // and if so scroll to the month that contains it
+ if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
+ || forceScroll) {
+ mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
+ mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
+
+ setMonthDisplayed(mFirstDayOfMonth);
+
+ // the earliest time we can scroll to is the min date
+ if (mFirstDayOfMonth.before(mMinDate)) {
+ position = 0;
+ } else {
+ position = getWeeksSinceMinDate(mFirstDayOfMonth);
+ }
+
+ mPreviousScrollState = AbsListView.OnScrollListener.SCROLL_STATE_FLING;
+ if (animate) {
+ mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
+ GOTO_SCROLL_DURATION);
+ } else {
+ mListView.setSelectionFromTop(position, mListScrollTopOffset);
+ // Perform any after scroll operations that are needed
+ onScrollStateChanged(mListView, AbsListView.OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ } else if (setSelected) {
+ // Otherwise just set the selection
+ setMonthDisplayed(date);
+ }
+ }
+
+ /**
+ * Called when a <code>view</code> transitions to a new <code>scrollState
+ * </code>.
+ */
+ private void onScrollStateChanged(AbsListView view, int scrollState) {
+ mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
+ }
+
+ /**
+ * Updates the title and selected month if the <code>view</code> has moved to a new
+ * month.
+ */
+ private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ WeekView child = (WeekView) view.getChildAt(0);
+ if (child == null) {
+ return;
+ }
+
+ // Figure out where we are
+ long currScroll =
+ view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
+
+ // If we have moved since our last call update the direction
+ if (currScroll < mPreviousScrollPosition) {
+ mIsScrollingUp = true;
+ } else if (currScroll > mPreviousScrollPosition) {
+ mIsScrollingUp = false;
+ } else {
+ return;
+ }
+
+ // Use some hysteresis for checking which month to highlight. This
+ // causes the month to transition when two full weeks of a month are
+ // visible when scrolling up, and when the first day in a month reaches
+ // the top of the screen when scrolling down.
+ int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
+ if (mIsScrollingUp) {
+ child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
+ } else if (offset != 0) {
+ child = (WeekView) view.getChildAt(offset);
+ }
+
+ if (child != null) {
+ // Find out which month we're moving into
+ int month;
+ if (mIsScrollingUp) {
+ month = child.getMonthOfFirstWeekDay();
+ } else {
+ month = child.getMonthOfLastWeekDay();
+ }
+
+ // And how it relates to our current highlighted month
+ int monthDiff;
+ if (mCurrentMonthDisplayed == 11 && month == 0) {
+ monthDiff = 1;
+ } else if (mCurrentMonthDisplayed == 0 && month == 11) {
+ monthDiff = -1;
+ } else {
+ monthDiff = month - mCurrentMonthDisplayed;
+ }
+
+ // Only switch months if we're scrolling away from the currently
+ // selected month
+ if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
+ Calendar firstDay = child.getFirstDay();
+ if (mIsScrollingUp) {
+ firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
+ } else {
+ firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
+ }
+ setMonthDisplayed(firstDay);
+ }
+ }
+ mPreviousScrollPosition = currScroll;
+ mPreviousScrollState = mCurrentScrollState;
+ }
+
+ /**
+ * Sets the month displayed at the top of this view based on time. Override
+ * to add custom events when the title is changed.
+ *
+ * @param calendar A day in the new focus month.
+ */
+ private void setMonthDisplayed(Calendar calendar) {
+ mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
+ mAdapter.setFocusMonth(mCurrentMonthDisplayed);
+ final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
+ | DateUtils.FORMAT_SHOW_YEAR;
+ final long millis = calendar.getTimeInMillis();
+ String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
+ mMonthName.setText(newMonthName);
+ mMonthName.invalidate();
+ }
+
+ /**
+ * @return Returns the number of weeks between the current <code>date</code>
+ * and the <code>mMinDate</code>.
+ */
+ private int getWeeksSinceMinDate(Calendar date) {
+ if (date.before(mMinDate)) {
+ throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
+ + " does not precede toDate: " + date.getTime());
+ }
+ long endTimeMillis = date.getTimeInMillis()
+ + date.getTimeZone().getOffset(date.getTimeInMillis());
+ long startTimeMillis = mMinDate.getTimeInMillis()
+ + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
+ long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
+ * MILLIS_IN_DAY;
+ return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
+ }
+
+ /**
+ * Command responsible for acting upon scroll state changes.
+ */
+ private class ScrollStateRunnable implements Runnable {
+ private AbsListView mView;
+
+ private int mNewState;
+
+ /**
+ * Sets up the runnable with a short delay in case the scroll state
+ * immediately changes again.
+ *
+ * @param view The list view that changed state
+ * @param scrollState The new state it changed to
+ */
+ public void doScrollStateChange(AbsListView view, int scrollState) {
+ mView = view;
+ mNewState = scrollState;
+ mDelegator.removeCallbacks(this);
+ mDelegator.postDelayed(this, SCROLL_CHANGE_DELAY);
+ }
+
+ public void run() {
+ mCurrentScrollState = mNewState;
+ // Fix the position after a scroll or a fling ends
+ if (mNewState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE
+ && mPreviousScrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
+ View child = mView.getChildAt(0);
+ if (child == null) {
+ // The view is no longer visible, just return
+ return;
+ }
+ int dist = child.getBottom() - mListScrollTopOffset;
+ if (dist > mListScrollTopOffset) {
+ if (mIsScrollingUp) {
+ mView.smoothScrollBy(dist - child.getHeight(),
+ ADJUSTMENT_SCROLL_DURATION);
+ } else {
+ mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
+ }
+ }
+ }
+ mPreviousScrollState = mNewState;
+ }
+ }
+
+ /**
+ * <p>
+ * This is a specialized adapter for creating a list of weeks with
+ * selectable days. It can be configured to display the week number, start
+ * the week on a given day, show a reduced number of days, or display an
+ * arbitrary number of weeks at a time.
+ * </p>
+ */
+ private class WeeksAdapter extends BaseAdapter implements View.OnTouchListener {
+
+ private int mSelectedWeek;
+
+ private GestureDetector mGestureDetector;
+
+ private int mFocusedMonth;
+
+ private final Calendar mSelectedDate = Calendar.getInstance();
+
+ private int mTotalWeekCount;
+
+ public WeeksAdapter(Context context) {
+ mContext = context;
+ mGestureDetector = new GestureDetector(mContext, new WeeksAdapter.CalendarGestureListener());
+ init();
+ }
+
+ /**
+ * Set up the gesture detector and selected time
+ */
+ private void init() {
+ mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
+ mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
+ if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
+ || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
+ mTotalWeekCount++;
+ }
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Updates the selected day and related parameters.
+ *
+ * @param selectedDay The time to highlight
+ */
+ public void setSelectedDay(Calendar selectedDay) {
+ if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
+ && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
+ return;
+ }
+ mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
+ mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
+ mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * @return The selected day of month.
+ */
+ public Calendar getSelectedDay() {
+ return mSelectedDate;
+ }
+
+ @Override
+ public int getCount() {
+ return mTotalWeekCount;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ WeekView weekView = null;
+ if (convertView != null) {
+ weekView = (WeekView) convertView;
+ } else {
+ weekView = new WeekView(mContext);
+ AbsListView.LayoutParams params =
+ new AbsListView.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT);
+ weekView.setLayoutParams(params);
+ weekView.setClickable(true);
+ weekView.setOnTouchListener(this);
+ }
+
+ int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
+ Calendar.DAY_OF_WEEK) : -1;
+ weekView.init(position, selectedWeekDay, mFocusedMonth);
+
+ return weekView;
+ }
+
+ /**
+ * Changes which month is in focus and updates the view.
+ *
+ * @param month The month to show as in focus [0-11]
+ */
+ public void setFocusMonth(int month) {
+ if (mFocusedMonth == month) {
+ return;
+ }
+ mFocusedMonth = month;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
+ WeekView weekView = (WeekView) v;
+ // if we cannot find a day for the given location we are done
+ if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
+ return true;
+ }
+ // it is possible that the touched day is outside the valid range
+ // we draw whole weeks but range end can fall not on the week end
+ if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
+ return true;
+ }
+ onDateTapped(mTempDate);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Maintains the same hour/min/sec but moves the day to the tapped day.
+ *
+ * @param day The day that was tapped
+ */
+ private void onDateTapped(Calendar day) {
+ setSelectedDay(day);
+ setMonthDisplayed(day);
+ }
+
+ /**
+ * This is here so we can identify single tap events and set the
+ * selected day correctly
+ */
+ class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * This is a dynamic view for drawing a single week. It can be configured to
+ * display the week number, start the week on a given day, or show a reduced
+ * number of days. It is intended for use as a single view within a
+ * ListView. See {@link WeeksAdapter} for usage.
+ * </p>
+ */
+ private class WeekView extends View {
+
+ private final Rect mTempRect = new Rect();
+
+ private final Paint mDrawPaint = new Paint();
+
+ private final Paint mMonthNumDrawPaint = new Paint();
+
+ // Cache the number strings so we don't have to recompute them each time
+ private String[] mDayNumbers;
+
+ // Quick lookup for checking which days are in the focus month
+ private boolean[] mFocusDay;
+
+ // Whether this view has a focused day.
+ private boolean mHasFocusedDay;
+
+ // Whether this view has only focused days.
+ private boolean mHasUnfocusedDay;
+
+ // The first day displayed by this item
+ private Calendar mFirstDay;
+
+ // The month of the first day in this week
+ private int mMonthOfFirstWeekDay = -1;
+
+ // The month of the last day in this week
+ private int mLastWeekDayMonth = -1;
+
+ // The position of this week, equivalent to weeks since the week of Jan
+ // 1st, 1900
+ private int mWeek = -1;
+
+ // Quick reference to the width of this view, matches parent
+ private int mWidth;
+
+ // The height this view should draw at in pixels, set by height param
+ private int mHeight;
+
+ // If this view contains the selected day
+ private boolean mHasSelectedDay = false;
+
+ // Which day is selected [0-6] or -1 if no day is selected
+ private int mSelectedDay = -1;
+
+ // The number of days + a spot for week number if it is displayed
+ private int mNumCells;
+
+ // The left edge of the selected day
+ private int mSelectedLeft = -1;
+
+ // The right edge of the selected day
+ private int mSelectedRight = -1;
+
+ public WeekView(Context context) {
+ super(context);
+
+ // Sets up any standard paints that will be used
+ initializePaints();
+ }
+
+ /**
+ * Initializes this week view.
+ *
+ * @param weekNumber The number of the week this view represents. The
+ * week number is a zero based index of the weeks since
+ * {@link android.widget.CalendarView#getMinDate()}.
+ * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
+ * selected day.
+ * @param focusedMonth The month that is currently in focus i.e.
+ * highlighted.
+ */
+ public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
+ mSelectedDay = selectedWeekDay;
+ mHasSelectedDay = mSelectedDay != -1;
+ mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
+ mWeek = weekNumber;
+ mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
+
+ mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
+ mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);
+
+ // Allocate space for caching the day numbers and focus values
+ mDayNumbers = new String[mNumCells];
+ mFocusDay = new boolean[mNumCells];
+
+ // If we're showing the week number calculate it based on Monday
+ int i = 0;
+ if (mShowWeekNumber) {
+ mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
+ mTempDate.get(Calendar.WEEK_OF_YEAR));
+ i++;
+ }
+
+ // Now adjust our starting day based on the start day of the week
+ int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
+ mTempDate.add(Calendar.DAY_OF_MONTH, diff);
+
+ mFirstDay = (Calendar) mTempDate.clone();
+ mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);
+
+ mHasUnfocusedDay = true;
+ for (; i < mNumCells; i++) {
+ final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
+ mFocusDay[i] = isFocusedDay;
+ mHasFocusedDay |= isFocusedDay;
+ mHasUnfocusedDay &= !isFocusedDay;
+ // do not draw dates outside the valid range to avoid user confusion
+ if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
+ mDayNumbers[i] = "";
+ } else {
+ mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
+ mTempDate.get(Calendar.DAY_OF_MONTH));
+ }
+ mTempDate.add(Calendar.DAY_OF_MONTH, 1);
+ }
+ // We do one extra add at the end of the loop, if that pushed us to
+ // new month undo it
+ if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
+ mTempDate.add(Calendar.DAY_OF_MONTH, -1);
+ }
+ mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);
+
+ updateSelectionPositions();
+ }
+
+ /**
+ * Initialize the paint instances.
+ */
+ private void initializePaints() {
+ mDrawPaint.setFakeBoldText(false);
+ mDrawPaint.setAntiAlias(true);
+ mDrawPaint.setStyle(Paint.Style.FILL);
+
+ mMonthNumDrawPaint.setFakeBoldText(true);
+ mMonthNumDrawPaint.setAntiAlias(true);
+ mMonthNumDrawPaint.setStyle(Paint.Style.FILL);
+ mMonthNumDrawPaint.setTextAlign(Paint.Align.CENTER);
+ mMonthNumDrawPaint.setTextSize(mDateTextSize);
+ }
+
+ /**
+ * Returns the month of the first day in this week.
+ *
+ * @return The month the first day of this view is in.
+ */
+ public int getMonthOfFirstWeekDay() {
+ return mMonthOfFirstWeekDay;
+ }
+
+ /**
+ * Returns the month of the last day in this week
+ *
+ * @return The month the last day of this view is in
+ */
+ public int getMonthOfLastWeekDay() {
+ return mLastWeekDayMonth;
+ }
+
+ /**
+ * Returns the first day in this view.
+ *
+ * @return The first day in the view.
+ */
+ public Calendar getFirstDay() {
+ return mFirstDay;
+ }
+
+ /**
+ * Calculates the day that the given x position is in, accounting for
+ * week number.
+ *
+ * @param x The x position of the touch event.
+ * @return True if a day was found for the given location.
+ */
+ public boolean getDayFromLocation(float x, Calendar outCalendar) {
+ final boolean isLayoutRtl = isLayoutRtl();
+
+ int start;
+ int end;
+
+ if (isLayoutRtl) {
+ start = 0;
+ end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+ } else {
+ start = mShowWeekNumber ? mWidth / mNumCells : 0;
+ end = mWidth;
+ }
+
+ if (x < start || x > end) {
+ outCalendar.clear();
+ return false;
+ }
+
+ // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels
+ int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));
+
+ if (isLayoutRtl) {
+ dayPosition = mDaysPerWeek - 1 - dayPosition;
+ }
+
+ outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
+ outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);
+
+ return true;
+ }
+
+ public boolean getBoundsForDate(Calendar date, Rect outBounds) {
+ Calendar currDay = Calendar.getInstance();
+ currDay.setTime(mFirstDay.getTime());
+ for (int i = 0; i < mDaysPerWeek; i++) {
+ if ((date.get(Calendar.YEAR) == currDay.get(Calendar.YEAR))
+ && (date.get(Calendar.MONTH) == currDay.get(Calendar.MONTH))
+ && (date.get(Calendar.DAY_OF_MONTH) == currDay.get(Calendar.DAY_OF_MONTH))) {
+ // We found the matching date. Follow the logic in the draw pass that divides
+ // the available horizontal space equally between all the entries in this week.
+ // Note that if we're showing week number, the start entry will be that number.
+ int cellSize = mWidth / mNumCells;
+ if (isLayoutRtl()) {
+ outBounds.left = cellSize *
+ (mShowWeekNumber ? (mNumCells - i - 2) : (mNumCells - i - 1));
+ } else {
+ outBounds.left = cellSize * (mShowWeekNumber ? i + 1 : i);
+ }
+ outBounds.top = 0;
+ outBounds.right = outBounds.left + cellSize;
+ outBounds.bottom = getHeight();
+ return true;
+ }
+ // Add one day
+ currDay.add(Calendar.DAY_OF_MONTH, 1);
+ }
+ return false;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ drawBackground(canvas);
+ drawWeekNumbersAndDates(canvas);
+ drawWeekSeparators(canvas);
+ drawSelectedDateVerticalBars(canvas);
+ }
+
+ /**
+ * This draws the selection highlight if a day is selected in this week.
+ *
+ * @param canvas The canvas to draw on
+ */
+ private void drawBackground(Canvas canvas) {
+ if (!mHasSelectedDay) {
+ return;
+ }
+ mDrawPaint.setColor(mSelectedWeekBackgroundColor);
+
+ mTempRect.top = mWeekSeparatorLineWidth;
+ mTempRect.bottom = mHeight;
+
+ final boolean isLayoutRtl = isLayoutRtl();
+
+ if (isLayoutRtl) {
+ mTempRect.left = 0;
+ mTempRect.right = mSelectedLeft - 2;
+ } else {
+ mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
+ mTempRect.right = mSelectedLeft - 2;
+ }
+ canvas.drawRect(mTempRect, mDrawPaint);
+
+ if (isLayoutRtl) {
+ mTempRect.left = mSelectedRight + 3;
+ mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+ } else {
+ mTempRect.left = mSelectedRight + 3;
+ mTempRect.right = mWidth;
+ }
+ canvas.drawRect(mTempRect, mDrawPaint);
+ }
+
+ /**
+ * Draws the week and month day numbers for this week.
+ *
+ * @param canvas The canvas to draw on
+ */
+ private void drawWeekNumbersAndDates(Canvas canvas) {
+ final float textHeight = mDrawPaint.getTextSize();
+ final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeparatorLineWidth;
+ final int nDays = mNumCells;
+ final int divisor = 2 * nDays;
+
+ mDrawPaint.setTextAlign(Paint.Align.CENTER);
+ mDrawPaint.setTextSize(mDateTextSize);
+
+ int i = 0;
+
+ if (isLayoutRtl()) {
+ for (; i < nDays - 1; i++) {
+ mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
+ : mUnfocusedMonthDateColor);
+ int x = (2 * i + 1) * mWidth / divisor;
+ canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint);
+ }
+ if (mShowWeekNumber) {
+ mDrawPaint.setColor(mWeekNumberColor);
+ int x = mWidth - mWidth / divisor;
+ canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
+ }
+ } else {
+ if (mShowWeekNumber) {
+ mDrawPaint.setColor(mWeekNumberColor);
+ int x = mWidth / divisor;
+ canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
+ i++;
+ }
+ for (; i < nDays; i++) {
+ mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
+ : mUnfocusedMonthDateColor);
+ int x = (2 * i + 1) * mWidth / divisor;
+ canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
+ }
+ }
+ }
+
+ /**
+ * Draws a horizontal line for separating the weeks.
+ *
+ * @param canvas The canvas to draw on.
+ */
+ private void drawWeekSeparators(Canvas canvas) {
+ // If it is the topmost fully visible child do not draw separator line
+ int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
+ if (mListView.getChildAt(0).getTop() < 0) {
+ firstFullyVisiblePosition++;
+ }
+ if (firstFullyVisiblePosition == mWeek) {
+ return;
+ }
+ mDrawPaint.setColor(mWeekSeparatorLineColor);
+ mDrawPaint.setStrokeWidth(mWeekSeparatorLineWidth);
+ float startX;
+ float stopX;
+ if (isLayoutRtl()) {
+ startX = 0;
+ stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+ } else {
+ startX = mShowWeekNumber ? mWidth / mNumCells : 0;
+ stopX = mWidth;
+ }
+ canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
+ }
+
+ /**
+ * Draws the selected date bars if this week has a selected day.
+ *
+ * @param canvas The canvas to draw on
+ */
+ private void drawSelectedDateVerticalBars(Canvas canvas) {
+ if (!mHasSelectedDay) {
+ return;
+ }
+ mSelectedDateVerticalBar.setBounds(
+ mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
+ mWeekSeparatorLineWidth,
+ mSelectedLeft + mSelectedDateVerticalBarWidth / 2,
+ mHeight);
+ mSelectedDateVerticalBar.draw(canvas);
+ mSelectedDateVerticalBar.setBounds(
+ mSelectedRight - mSelectedDateVerticalBarWidth / 2,
+ mWeekSeparatorLineWidth,
+ mSelectedRight + mSelectedDateVerticalBarWidth / 2,
+ mHeight);
+ mSelectedDateVerticalBar.draw(canvas);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ updateSelectionPositions();
+ }
+
+ /**
+ * This calculates the positions for the selected day lines.
+ */
+ private void updateSelectionPositions() {
+ if (mHasSelectedDay) {
+ final boolean isLayoutRtl = isLayoutRtl();
+ int selectedPosition = mSelectedDay - mFirstDayOfWeek;
+ if (selectedPosition < 0) {
+ selectedPosition += 7;
+ }
+ if (mShowWeekNumber && !isLayoutRtl) {
+ selectedPosition++;
+ }
+ if (isLayoutRtl) {
+ mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells;
+
+ } else {
+ mSelectedLeft = selectedPosition * mWidth / mNumCells;
+ }
+ mSelectedRight = mSelectedLeft + mWidth / mNumCells;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
+ .getPaddingBottom()) / mShownWeekCount;
+ setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
+ }
+ }
+
+}
diff --git a/android/widget/CalendarViewMaterialDelegate.java b/android/widget/CalendarViewMaterialDelegate.java
new file mode 100644
index 0000000..b752eb6
--- /dev/null
+++ b/android/widget/CalendarViewMaterialDelegate.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2014 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.StyleRes;
+import android.content.Context;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.widget.DayPickerView.OnDaySelectedListener;
+
+class CalendarViewMaterialDelegate extends CalendarView.AbstractCalendarViewDelegate {
+ private final DayPickerView mDayPickerView;
+
+ private CalendarView.OnDateChangeListener mOnDateChangeListener;
+
+ public CalendarViewMaterialDelegate(CalendarView delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ mDayPickerView = new DayPickerView(context, attrs, defStyleAttr, defStyleRes);
+ mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
+
+ delegator.addView(mDayPickerView);
+ }
+
+ @Override
+ public void setWeekDayTextAppearance(@StyleRes int resId) {
+ mDayPickerView.setDayOfWeekTextAppearance(resId);
+ }
+
+ @StyleRes
+ @Override
+ public int getWeekDayTextAppearance() {
+ return mDayPickerView.getDayOfWeekTextAppearance();
+ }
+
+ @Override
+ public void setDateTextAppearance(@StyleRes int resId) {
+ mDayPickerView.setDayTextAppearance(resId);
+ }
+
+ @StyleRes
+ @Override
+ public int getDateTextAppearance() {
+ return mDayPickerView.getDayTextAppearance();
+ }
+
+ @Override
+ public void setMinDate(long minDate) {
+ mDayPickerView.setMinDate(minDate);
+ }
+
+ @Override
+ public long getMinDate() {
+ return mDayPickerView.getMinDate();
+ }
+
+ @Override
+ public void setMaxDate(long maxDate) {
+ mDayPickerView.setMaxDate(maxDate);
+ }
+
+ @Override
+ public long getMaxDate() {
+ return mDayPickerView.getMaxDate();
+ }
+
+ @Override
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ @Override
+ public int getFirstDayOfWeek() {
+ return mDayPickerView.getFirstDayOfWeek();
+ }
+
+ @Override
+ public void setDate(long date) {
+ mDayPickerView.setDate(date, true);
+ }
+
+ @Override
+ public void setDate(long date, boolean animate, boolean center) {
+ mDayPickerView.setDate(date, animate);
+ }
+
+ @Override
+ public long getDate() {
+ return mDayPickerView.getDate();
+ }
+
+ @Override
+ public void setOnDateChangeListener(CalendarView.OnDateChangeListener listener) {
+ mOnDateChangeListener = listener;
+ }
+
+ @Override
+ public boolean getBoundsForDate(long date, Rect outBounds) {
+ boolean result = mDayPickerView.getBoundsForDate(date, outBounds);
+ if (result) {
+ // Found the date in the current picker. Now need to offset vertically to return correct
+ // bounds in the coordinate system of the entire layout
+ final int[] dayPickerPositionOnScreen = new int[2];
+ final int[] delegatorPositionOnScreen = new int[2];
+ mDayPickerView.getLocationOnScreen(dayPickerPositionOnScreen);
+ mDelegator.getLocationOnScreen(delegatorPositionOnScreen);
+ final int extraVerticalOffset =
+ dayPickerPositionOnScreen[1] - delegatorPositionOnScreen[1];
+ outBounds.top += extraVerticalOffset;
+ outBounds.bottom += extraVerticalOffset;
+ return true;
+ }
+ return false;
+ }
+
+ private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
+ @Override
+ public void onDaySelected(DayPickerView view, Calendar day) {
+ if (mOnDateChangeListener != null) {
+ final int year = day.get(Calendar.YEAR);
+ final int month = day.get(Calendar.MONTH);
+ final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+ mOnDateChangeListener.onSelectedDayChange(mDelegator, year, month, dayOfMonth);
+ }
+ }
+ };
+}
diff --git a/android/widget/CheckBox.java b/android/widget/CheckBox.java
new file mode 100644
index 0000000..046f75f
--- /dev/null
+++ b/android/widget/CheckBox.java
@@ -0,0 +1,76 @@
+/*
+ * 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>See the <a href="{@docRoot}guide/topics/ui/controls/checkbox.html">Checkboxes</a>
+ * guide.</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 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return CheckBox.class.getName();
+ }
+}
diff --git a/android/widget/Checkable.java b/android/widget/Checkable.java
new file mode 100644
index 0000000..eb97b4a
--- /dev/null
+++ b/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/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java
new file mode 100644
index 0000000..92bfd56
--- /dev/null
+++ b/android/widget/CheckedTextView.java
@@ -0,0 +1,538 @@
+/*
+ * 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.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * An extension to {@link TextView} that supports the {@link Checkable}
+ * interface and displays.
+ * <p>
+ * This is useful when used in a {@link android.widget.ListView ListView} where
+ * the {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has
+ * been set to something other than
+ * {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}.
+ *
+ * @attr ref android.R.styleable#CheckedTextView_checked
+ * @attr ref android.R.styleable#CheckedTextView_checkMark
+ */
+public class CheckedTextView extends TextView implements Checkable {
+ private boolean mChecked;
+
+ private int mCheckMarkResource;
+ private Drawable mCheckMarkDrawable;
+ private ColorStateList mCheckMarkTintList = null;
+ private PorterDuff.Mode mCheckMarkTintMode = null;
+ private boolean mHasCheckMarkTint = false;
+ private boolean mHasCheckMarkTintMode = false;
+
+ private int mBasePadding;
+ private int mCheckMarkWidth;
+ private int mCheckMarkGravity = Gravity.END;
+
+ private boolean mNeedRequestlayout;
+
+ 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, R.attr.checkedTextViewStyle);
+ }
+
+ public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes);
+
+ final Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark);
+ if (d != null) {
+ setCheckMarkDrawable(d);
+ }
+
+ if (a.hasValue(R.styleable.CheckedTextView_checkMarkTintMode)) {
+ mCheckMarkTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.CheckedTextView_checkMarkTintMode, -1), mCheckMarkTintMode);
+ mHasCheckMarkTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.CheckedTextView_checkMarkTint)) {
+ mCheckMarkTintList = a.getColorStateList(R.styleable.CheckedTextView_checkMarkTint);
+ mHasCheckMarkTint = true;
+ }
+
+ mCheckMarkGravity = a.getInt(R.styleable.CheckedTextView_checkMarkGravity, Gravity.END);
+
+ final boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false);
+ setChecked(checked);
+
+ a.recycle();
+
+ applyCheckMarkTint();
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @ViewDebug.ExportedProperty
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * Sets the checked state of this view.
+ *
+ * @param checked {@code true} set the state to checked, {@code false} to
+ * uncheck
+ */
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ refreshDrawableState();
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+
+ /**
+ * Sets the check mark to the drawable with the specified resource ID.
+ * <p>
+ * When this view is checked, the drawable's state set will include
+ * {@link android.R.attr#state_checked}.
+ *
+ * @param resId the resource identifier of drawable to use as the check
+ * mark
+ * @attr ref android.R.styleable#CheckedTextView_checkMark
+ * @see #setCheckMarkDrawable(Drawable)
+ * @see #getCheckMarkDrawable()
+ */
+ public void setCheckMarkDrawable(@DrawableRes int resId) {
+ if (resId != 0 && resId == mCheckMarkResource) {
+ return;
+ }
+
+ final Drawable d = resId != 0 ? getContext().getDrawable(resId) : null;
+ setCheckMarkDrawableInternal(d, resId);
+ }
+
+ /**
+ * Set the check mark to the specified drawable.
+ * <p>
+ * When this view is checked, the drawable's state set will include
+ * {@link android.R.attr#state_checked}.
+ *
+ * @param d the drawable to use for the check mark
+ * @attr ref android.R.styleable#CheckedTextView_checkMark
+ * @see #setCheckMarkDrawable(int)
+ * @see #getCheckMarkDrawable()
+ */
+ public void setCheckMarkDrawable(@Nullable Drawable d) {
+ setCheckMarkDrawableInternal(d, 0);
+ }
+
+ private void setCheckMarkDrawableInternal(@Nullable Drawable d, @DrawableRes int resId) {
+ if (mCheckMarkDrawable != null) {
+ mCheckMarkDrawable.setCallback(null);
+ unscheduleDrawable(mCheckMarkDrawable);
+ }
+
+ mNeedRequestlayout = (d != mCheckMarkDrawable);
+
+ if (d != null) {
+ d.setCallback(this);
+ d.setVisible(getVisibility() == VISIBLE, false);
+ d.setState(CHECKED_STATE_SET);
+
+ // Record the intrinsic dimensions when in "checked" state.
+ setMinHeight(d.getIntrinsicHeight());
+ mCheckMarkWidth = d.getIntrinsicWidth();
+
+ d.setState(getDrawableState());
+ } else {
+ mCheckMarkWidth = 0;
+ }
+
+ mCheckMarkDrawable = d;
+ mCheckMarkResource = resId;
+
+ applyCheckMarkTint();
+
+ // Do padding resolution. This will call internalSetPadding() and do a
+ // requestLayout() if needed.
+ resolvePadding();
+ }
+
+ /**
+ * Applies a tint to the check mark drawable. Does not modify the
+ * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and
+ * tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
+ * @see #getCheckMarkTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setCheckMarkTintList(@Nullable ColorStateList tint) {
+ mCheckMarkTintList = tint;
+ mHasCheckMarkTint = true;
+
+ applyCheckMarkTint();
+ }
+
+ /**
+ * Returns the tint applied to the check mark drawable, if specified.
+ *
+ * @return the tint applied to the check mark drawable
+ * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
+ * @see #setCheckMarkTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getCheckMarkTintList() {
+ return mCheckMarkTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setCheckMarkTintList(ColorStateList)} to the check mark
+ * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
+ * @see #setCheckMarkTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mCheckMarkTintMode = tintMode;
+ mHasCheckMarkTintMode = true;
+
+ applyCheckMarkTint();
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the check mark
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the check mark
+ * drawable
+ * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
+ * @see #setCheckMarkTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getCheckMarkTintMode() {
+ return mCheckMarkTintMode;
+ }
+
+ private void applyCheckMarkTint() {
+ if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) {
+ mCheckMarkDrawable = mCheckMarkDrawable.mutate();
+
+ if (mHasCheckMarkTint) {
+ mCheckMarkDrawable.setTintList(mCheckMarkTintList);
+ }
+
+ if (mHasCheckMarkTintMode) {
+ mCheckMarkDrawable.setTintMode(mCheckMarkTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mCheckMarkDrawable.isStateful()) {
+ mCheckMarkDrawable.setState(getDrawableState());
+ }
+ }
+ }
+
+ @RemotableViewMethod
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (mCheckMarkDrawable != null) {
+ mCheckMarkDrawable.setVisible(visibility == VISIBLE, false);
+ }
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+
+ if (mCheckMarkDrawable != null) {
+ mCheckMarkDrawable.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return who == mCheckMarkDrawable || super.verifyDrawable(who);
+ }
+
+ /**
+ * Gets the checkmark drawable
+ *
+ * @return The drawable use to represent the checkmark, if any.
+ *
+ * @see #setCheckMarkDrawable(Drawable)
+ * @see #setCheckMarkDrawable(int)
+ *
+ * @attr ref android.R.styleable#CheckedTextView_checkMark
+ */
+ public Drawable getCheckMarkDrawable() {
+ return mCheckMarkDrawable;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void internalSetPadding(int left, int top, int right, int bottom) {
+ super.internalSetPadding(left, top, right, bottom);
+ setBasePadding(isCheckMarkAtStart());
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ updatePadding();
+ }
+
+ private void updatePadding() {
+ resetPaddingToInitialValues();
+ int newPadding = (mCheckMarkDrawable != null) ?
+ mCheckMarkWidth + mBasePadding : mBasePadding;
+ if (isCheckMarkAtStart()) {
+ mNeedRequestlayout |= (mPaddingLeft != newPadding);
+ mPaddingLeft = newPadding;
+ } else {
+ mNeedRequestlayout |= (mPaddingRight != newPadding);
+ mPaddingRight = newPadding;
+ }
+ if (mNeedRequestlayout) {
+ requestLayout();
+ mNeedRequestlayout = false;
+ }
+ }
+
+ private void setBasePadding(boolean checkmarkAtStart) {
+ if (checkmarkAtStart) {
+ mBasePadding = mPaddingLeft;
+ } else {
+ mBasePadding = mPaddingRight;
+ }
+ }
+
+ private boolean isCheckMarkAtStart() {
+ final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection());
+ final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ return hgrav == Gravity.LEFT;
+ }
+
+ @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;
+ }
+
+ final boolean checkMarkAtStart = isCheckMarkAtStart();
+ final int width = getWidth();
+ final int top = y;
+ final int bottom = top + height;
+ final int left;
+ final int right;
+ if (checkMarkAtStart) {
+ left = mBasePadding;
+ right = left + mCheckMarkWidth;
+ } else {
+ right = width - mBasePadding;
+ left = right - mCheckMarkWidth;
+ }
+ checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom);
+ checkMarkDrawable.draw(canvas);
+
+ final Drawable background = getBackground();
+ if (background != null) {
+ background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom);
+ }
+ }
+ }
+
+ @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();
+
+ final Drawable checkMarkDrawable = mCheckMarkDrawable;
+ if (checkMarkDrawable != null && checkMarkDrawable.isStateful()
+ && checkMarkDrawable.setState(getDrawableState())) {
+ invalidateDrawable(checkMarkDrawable);
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mCheckMarkDrawable != null) {
+ mCheckMarkDrawable.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return CheckedTextView.class.getName();
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean checked;
+
+ /**
+ * Constructor called from {@link CheckedTextView#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 "CheckedTextView.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() {
+ 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();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setChecked(mChecked);
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setCheckable(true);
+ info.setChecked(mChecked);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+ stream.addProperty("text:checked", isChecked());
+ }
+}
diff --git a/android/widget/Chronometer.java b/android/widget/Chronometer.java
new file mode 100644
index 0000000..d11c03a
--- /dev/null
+++ b/android/widget/Chronometer.java
@@ -0,0 +1,394 @@
+/*
+ * 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.Intent;
+import android.content.res.TypedArray;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+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}.
+ *
+ * <p>The timer can also count downward towards the base time by
+ * setting {@link #setCountDown(boolean)} to true.
+ *
+ * <p>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
+ * @attr ref android.R.styleable#Chronometer_countDown
+ */
+@RemoteView
+public class Chronometer extends TextView {
+ private static final String TAG = "Chronometer";
+
+ /**
+ * A callback that notifies when the chronometer has incremented on its own.
+ */
+ public interface OnChronometerTickListener {
+
+ /**
+ * Notification that the chronometer has changed.
+ */
+ void onChronometerTick(Chronometer chronometer);
+
+ }
+
+ private long mBase;
+ private long mNow; // the currently displayed time
+ 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;
+ private OnChronometerTickListener mOnChronometerTickListener;
+ private StringBuilder mRecycle = new StringBuilder(8);
+ private boolean mCountDown;
+
+ /**
+ * 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
+ setFormat(a.getString(R.styleable.Chronometer_format));
+ setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false));
+ a.recycle();
+
+ init();
+ }
+
+ private void init() {
+ mBase = SystemClock.elapsedRealtime();
+ updateText(mBase);
+ }
+
+ /**
+ * Set this view to count down to the base instead of counting up from it.
+ *
+ * @param countDown whether this view should count down
+ *
+ * @see #setBase(long)
+ */
+ @android.view.RemotableViewMethod
+ public void setCountDown(boolean countDown) {
+ mCountDown = countDown;
+ updateText(SystemClock.elapsedRealtime());
+ }
+
+ /**
+ * @return whether this view counts down
+ *
+ * @see #setCountDown(boolean)
+ */
+ public boolean isCountDown() {
+ return mCountDown;
+ }
+
+ /**
+ * @return whether this is the final countdown
+ */
+ public boolean isTheFinalCountDown() {
+ try {
+ getContext().startActivity(
+ new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Set the time that the count-up timer is in reference to.
+ *
+ * @param base Use the {@link SystemClock#elapsedRealtime} time base.
+ */
+ @android.view.RemotableViewMethod
+ public void setBase(long base) {
+ mBase = base;
+ dispatchChronometerTick();
+ 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.
+ */
+ @android.view.RemotableViewMethod
+ 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;
+ }
+
+ /**
+ * Sets the listener to be called when the chronometer changes.
+ *
+ * @param listener The listener.
+ */
+ public void setOnChronometerTickListener(OnChronometerTickListener listener) {
+ mOnChronometerTickListener = listener;
+ }
+
+ /**
+ * @return The listener (may be null) that is listening for chronometer change
+ * events.
+ */
+ public OnChronometerTickListener getOnChronometerTickListener() {
+ return mOnChronometerTickListener;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * The same as calling {@link #start} or {@link #stop}.
+ * @hide pending API council approval
+ */
+ @android.view.RemotableViewMethod
+ public void setStarted(boolean started) {
+ mStarted = started;
+ updateRunning();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mVisible = false;
+ updateRunning();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mVisible = visibility == VISIBLE;
+ updateRunning();
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ updateRunning();
+ }
+
+ private synchronized void updateText(long now) {
+ mNow = now;
+ long seconds = mCountDown ? mBase - now : now - mBase;
+ seconds /= 1000;
+ boolean negative = false;
+ if (seconds < 0) {
+ seconds = -seconds;
+ negative = true;
+ }
+ String text = DateUtils.formatElapsedTime(mRecycle, seconds);
+ if (negative) {
+ text = getResources().getString(R.string.negative_duration, text);
+ }
+
+ 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 && isShown();
+ if (running != mRunning) {
+ if (running) {
+ updateText(SystemClock.elapsedRealtime());
+ dispatchChronometerTick();
+ postDelayed(mTickRunnable, 1000);
+ } else {
+ removeCallbacks(mTickRunnable);
+ }
+ mRunning = running;
+ }
+ }
+
+ private final Runnable mTickRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mRunning) {
+ updateText(SystemClock.elapsedRealtime());
+ dispatchChronometerTick();
+ postDelayed(mTickRunnable, 1000);
+ }
+ }
+ };
+
+ void dispatchChronometerTick() {
+ if (mOnChronometerTickListener != null) {
+ mOnChronometerTickListener.onChronometerTick(this);
+ }
+ }
+
+ private static final int MIN_IN_SEC = 60;
+ private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
+ private static String formatDuration(long ms) {
+ int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
+ if (duration < 0) {
+ duration = -duration;
+ }
+
+ int h = 0;
+ int m = 0;
+
+ if (duration >= HOUR_IN_SEC) {
+ h = duration / HOUR_IN_SEC;
+ duration -= h * HOUR_IN_SEC;
+ }
+ if (duration >= MIN_IN_SEC) {
+ m = duration / MIN_IN_SEC;
+ duration -= m * MIN_IN_SEC;
+ }
+ final int s = duration;
+
+ final ArrayList<Measure> measures = new ArrayList<Measure>();
+ if (h > 0) {
+ measures.add(new Measure(h, MeasureUnit.HOUR));
+ }
+ if (m > 0) {
+ measures.add(new Measure(m, MeasureUnit.MINUTE));
+ }
+ measures.add(new Measure(s, MeasureUnit.SECOND));
+
+ return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE)
+ .formatMeasures(measures.toArray(new Measure[measures.size()]));
+ }
+
+ @Override
+ public CharSequence getContentDescription() {
+ return formatDuration(mNow - mBase);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Chronometer.class.getName();
+ }
+}
diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java
new file mode 100644
index 0000000..0762b15
--- /dev/null
+++ b/android/widget/CompoundButton.java
@@ -0,0 +1,605 @@
+/*
+ * 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.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SoundEffectConstants;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+/**
+ * <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 static final String LOG_TAG = CompoundButton.class.getSimpleName();
+
+ private boolean mChecked;
+ private boolean mBroadcasting;
+
+ private Drawable mButtonDrawable;
+ private ColorStateList mButtonTintList = null;
+ private PorterDuff.Mode mButtonTintMode = null;
+ private boolean mHasButtonTint = false;
+ private boolean mHasButtonTintMode = false;
+
+ private OnCheckedChangeListener mOnCheckedChangeListener;
+ private OnCheckedChangeListener mOnCheckedChangeWidgetListener;
+
+ // Indicates whether the toggle state was set from resources or dynamically, so it can be used
+ // to sanitize autofill requests.
+ private boolean mCheckedFromResource = false;
+
+ 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes);
+
+ final Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);
+ if (d != null) {
+ setButtonDrawable(d);
+ }
+
+ if (a.hasValue(R.styleable.CompoundButton_buttonTintMode)) {
+ mButtonTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.CompoundButton_buttonTintMode, -1), mButtonTintMode);
+ mHasButtonTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.CompoundButton_buttonTint)) {
+ mButtonTintList = a.getColorStateList(R.styleable.CompoundButton_buttonTint);
+ mHasButtonTint = true;
+ }
+
+ final boolean checked = a.getBoolean(
+ com.android.internal.R.styleable.CompoundButton_checked, false);
+ setChecked(checked);
+ mCheckedFromResource = true;
+
+ a.recycle();
+
+ applyButtonTint();
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean performClick() {
+ toggle();
+
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+
+ return handled;
+ }
+
+ @ViewDebug.ExportedProperty
+ @Override
+ 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
+ */
+ @Override
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mCheckedFromResource = false;
+ mChecked = checked;
+ refreshDrawableState();
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+ // 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);
+ }
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.notifyValueChanged(this);
+ }
+
+ 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(@Nullable 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);
+ }
+
+ /**
+ * Sets a drawable as the compound button image given its resource
+ * identifier.
+ *
+ * @param resId the resource identifier of the drawable
+ * @attr ref android.R.styleable#CompoundButton_button
+ */
+ public void setButtonDrawable(@DrawableRes int resId) {
+ final Drawable d;
+ if (resId != 0) {
+ d = getContext().getDrawable(resId);
+ } else {
+ d = null;
+ }
+ setButtonDrawable(d);
+ }
+
+ /**
+ * Sets a drawable as the compound button image.
+ *
+ * @param drawable the drawable to set
+ * @attr ref android.R.styleable#CompoundButton_button
+ */
+ public void setButtonDrawable(@Nullable Drawable drawable) {
+ if (mButtonDrawable != drawable) {
+ if (mButtonDrawable != null) {
+ mButtonDrawable.setCallback(null);
+ unscheduleDrawable(mButtonDrawable);
+ }
+
+ mButtonDrawable = drawable;
+
+ if (drawable != null) {
+ drawable.setCallback(this);
+ drawable.setLayoutDirection(getLayoutDirection());
+ if (drawable.isStateful()) {
+ drawable.setState(getDrawableState());
+ }
+ drawable.setVisible(getVisibility() == VISIBLE, false);
+ setMinHeight(drawable.getIntrinsicHeight());
+ applyButtonTint();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
+ super.onResolveDrawables(layoutDirection);
+ if (mButtonDrawable != null) {
+ mButtonDrawable.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ /**
+ * @return the drawable used as the compound button image
+ * @see #setButtonDrawable(Drawable)
+ * @see #setButtonDrawable(int)
+ */
+ @Nullable
+ public Drawable getButtonDrawable() {
+ return mButtonDrawable;
+ }
+
+ /**
+ * Applies a tint to the button drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setButtonDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and tint
+ * mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#CompoundButton_buttonTint
+ * @see #setButtonTintList(ColorStateList)
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setButtonTintList(@Nullable ColorStateList tint) {
+ mButtonTintList = tint;
+ mHasButtonTint = true;
+
+ applyButtonTint();
+ }
+
+ /**
+ * @return the tint applied to the button drawable
+ * @attr ref android.R.styleable#CompoundButton_buttonTint
+ * @see #setButtonTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getButtonTintList() {
+ return mButtonTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setButtonTintList(ColorStateList)}} to the button drawable. The
+ * default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#CompoundButton_buttonTintMode
+ * @see #getButtonTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mButtonTintMode = tintMode;
+ mHasButtonTintMode = true;
+
+ applyButtonTint();
+ }
+
+ /**
+ * @return the blending mode used to apply the tint to the button drawable
+ * @attr ref android.R.styleable#CompoundButton_buttonTintMode
+ * @see #setButtonTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getButtonTintMode() {
+ return mButtonTintMode;
+ }
+
+ private void applyButtonTint() {
+ if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) {
+ mButtonDrawable = mButtonDrawable.mutate();
+
+ if (mHasButtonTint) {
+ mButtonDrawable.setTintList(mButtonTintList);
+ }
+
+ if (mHasButtonTintMode) {
+ mButtonDrawable.setTintMode(mButtonTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mButtonDrawable.isStateful()) {
+ mButtonDrawable.setState(getDrawableState());
+ }
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return CompoundButton.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setChecked(mChecked);
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setCheckable(true);
+ info.setChecked(mChecked);
+ }
+
+ @Override
+ public int getCompoundPaddingLeft() {
+ int padding = super.getCompoundPaddingLeft();
+ if (!isLayoutRtl()) {
+ final Drawable buttonDrawable = mButtonDrawable;
+ if (buttonDrawable != null) {
+ padding += buttonDrawable.getIntrinsicWidth();
+ }
+ }
+ return padding;
+ }
+
+ @Override
+ public int getCompoundPaddingRight() {
+ int padding = super.getCompoundPaddingRight();
+ if (isLayoutRtl()) {
+ final Drawable buttonDrawable = mButtonDrawable;
+ if (buttonDrawable != null) {
+ padding += buttonDrawable.getIntrinsicWidth();
+ }
+ }
+ return padding;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int getHorizontalOffsetForDrawables() {
+ final Drawable buttonDrawable = mButtonDrawable;
+ return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final Drawable buttonDrawable = mButtonDrawable;
+ if (buttonDrawable != null) {
+ final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ final int drawableHeight = buttonDrawable.getIntrinsicHeight();
+ final int drawableWidth = buttonDrawable.getIntrinsicWidth();
+
+ final int top;
+ switch (verticalGravity) {
+ case Gravity.BOTTOM:
+ top = getHeight() - drawableHeight;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ top = (getHeight() - drawableHeight) / 2;
+ break;
+ default:
+ top = 0;
+ }
+ final int bottom = top + drawableHeight;
+ final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
+ final int right = isLayoutRtl() ? getWidth() : drawableWidth;
+
+ buttonDrawable.setBounds(left, top, right, bottom);
+
+ final Drawable background = getBackground();
+ if (background != null) {
+ background.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ super.onDraw(canvas);
+
+ if (buttonDrawable != null) {
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ if (scrollX == 0 && scrollY == 0) {
+ buttonDrawable.draw(canvas);
+ } else {
+ canvas.translate(scrollX, scrollY);
+ buttonDrawable.draw(canvas);
+ canvas.translate(-scrollX, -scrollY);
+ }
+ }
+ }
+
+ @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();
+
+ final Drawable buttonDrawable = mButtonDrawable;
+ if (buttonDrawable != null && buttonDrawable.isStateful()
+ && buttonDrawable.setState(getDrawableState())) {
+ invalidateDrawable(buttonDrawable);
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mButtonDrawable != null) {
+ mButtonDrawable.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || who == mButtonDrawable;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
+ }
+
+ 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 + "}";
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ 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();
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+ stream.addProperty("checked", isChecked());
+ }
+
+ @Override
+ public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+ super.onProvideAutofillStructure(structure, flags);
+
+ structure.setDataIsSensitive(!mCheckedFromResource);
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!isEnabled()) return;
+
+ if (!value.isToggle()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ setChecked(value.getToggleValue());
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isEnabled() ? AUTOFILL_TYPE_TOGGLE : AUTOFILL_TYPE_NONE;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ return isEnabled() ? AutofillValue.forToggle(isChecked()) : null;
+ }
+}
diff --git a/android/widget/CursorAdapter.java b/android/widget/CursorAdapter.java
new file mode 100644
index 0000000..9fb98db
--- /dev/null
+++ b/android/widget/CursorAdapter.java
@@ -0,0 +1,518 @@
+/*
+ * 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.annotation.WorkerThread;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+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.
+ * <p>
+ * The Cursor must include a column named "_id" or this class will not work.
+ * Additionally, using {@link android.database.MergeCursor} with this class will
+ * not work if the merged Cursors have overlapping values in their "_id"
+ * columns.
+ */
+public abstract class CursorAdapter extends BaseAdapter implements Filterable,
+ CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {
+ /**
+ * 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;
+ /**
+ * Context used for {@link #getDropDownView(int, View, ViewGroup)}.
+ * {@hide}
+ */
+ protected Context mDropDownContext;
+ /**
+ * 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;
+ /**
+ * 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;
+
+ /**
+ * If set the adapter will call requery() on the cursor whenever a content change
+ * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ */
+ @Deprecated
+ public static final int FLAG_AUTO_REQUERY = 0x01;
+
+ /**
+ * If set the adapter will register a content observer on the cursor and will call
+ * {@link #onContentChanged()} when a notification comes in. Be careful when
+ * using this flag: you will need to unset the current Cursor from the adapter
+ * to avoid leaks due to its registered observers. This flag is not needed
+ * when using a CursorAdapter with a
+ * {@link android.content.CursorLoader}.
+ */
+ public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
+
+ /**
+ * Constructor that always enables auto-requery.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ *
+ * @param c The cursor from which to get the data.
+ * @param context The context
+ */
+ @Deprecated
+ public CursorAdapter(Context context, Cursor c) {
+ init(context, c, FLAG_AUTO_REQUERY);
+ }
+
+ /**
+ * Constructor that allows control over auto-requery. It is recommended
+ * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
+ * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+ * will always be set.
+ *
+ * @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. Using true here is discouraged.
+ */
+ public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
+ init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+ }
+
+ /**
+ * Recommended constructor.
+ *
+ * @param c The cursor from which to get the data.
+ * @param context The context
+ * @param flags Flags used to determine the behavior of the adapter; may
+ * be any combination of {@link #FLAG_AUTO_REQUERY} and
+ * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
+ */
+ public CursorAdapter(Context context, Cursor c, int flags) {
+ init(context, c, flags);
+ }
+
+ /**
+ * @deprecated Don't use this, use the normal constructor. This will
+ * be removed in the future.
+ */
+ @Deprecated
+ protected void init(Context context, Cursor c, boolean autoRequery) {
+ init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+ }
+
+ void init(Context context, Cursor c, int flags) {
+ if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
+ flags |= FLAG_REGISTER_CONTENT_OBSERVER;
+ mAutoRequery = true;
+ } else {
+ mAutoRequery = false;
+ }
+ boolean cursorPresent = c != null;
+ mCursor = c;
+ mDataValid = cursorPresent;
+ mContext = context;
+ mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
+ if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
+ mChangeObserver = new ChangeObserver();
+ mDataSetObserver = new MyDataSetObserver();
+ } else {
+ mChangeObserver = null;
+ mDataSetObserver = null;
+ }
+
+ if (cursorPresent) {
+ if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
+ if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
+ }
+ }
+
+ /**
+ * Sets the {@link Resources.Theme} against which drop-down views are
+ * inflated.
+ * <p>
+ * By default, drop-down views are inflated against the theme of the
+ * {@link Context} passed to the adapter's constructor.
+ *
+ * @param theme the theme against which to inflate drop-down views or
+ * {@code null} to use the theme from the adapter's context
+ * @see #newDropDownView(Context, Cursor, ViewGroup)
+ */
+ @Override
+ public void setDropDownViewTheme(Resources.Theme theme) {
+ if (theme == null) {
+ mDropDownContext = null;
+ } else if (theme == mContext.getTheme()) {
+ mDropDownContext = mContext;
+ } else {
+ mDropDownContext = new ContextThemeWrapper(mContext, theme);
+ }
+ }
+
+ @Override
+ public Resources.Theme getDropDownViewTheme() {
+ return mDropDownContext == null ? null : mDropDownContext.getTheme();
+ }
+
+ /**
+ * Returns the cursor.
+ * @return the cursor.
+ */
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getCount()
+ */
+ public int getCount() {
+ if (mDataValid && mCursor != null) {
+ return mCursor.getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getItem(int)
+ */
+ public Object getItem(int position) {
+ if (mDataValid && mCursor != null) {
+ mCursor.moveToPosition(position);
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getItemId(int)
+ */
+ public 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) {
+ final Context context = mDropDownContext == null ? mContext : mDropDownContext;
+ mCursor.moveToPosition(position);
+ final View v;
+ if (convertView == null) {
+ v = newDropDownView(context, mCursor, parent);
+ } else {
+ v = convertView;
+ }
+ bindView(v, context, 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) {
+ Cursor old = swapCursor(cursor);
+ if (old != null) {
+ old.close();
+ }
+ }
+
+ /**
+ * Swap in a new Cursor, returning the old Cursor. Unlike
+ * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
+ * closed.
+ *
+ * @param newCursor The new cursor to be used.
+ * @return Returns the previously set Cursor, or null if there wasa not one.
+ * If the given new Cursor is the same instance is the previously set
+ * Cursor, null is also returned.
+ */
+ public Cursor swapCursor(Cursor newCursor) {
+ if (newCursor == mCursor) {
+ return null;
+ }
+ Cursor oldCursor = mCursor;
+ if (oldCursor != null) {
+ if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
+ if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
+ }
+ mCursor = newCursor;
+ if (newCursor != null) {
+ if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
+ if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
+ mRowIDColumn = newCursor.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();
+ }
+ return oldCursor;
+ }
+
+ /**
+ * <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)
+ */
+ @WorkerThread
+ 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;
+ }
+
+ /**
+ * Called when the {@link ContentObserver} on the cursor receives a change notification.
+ * The default implementation provides the auto-requery logic, but may be overridden by
+ * sub classes.
+ *
+ * @see ContentObserver#onChange(boolean)
+ */
+ protected void onContentChanged() {
+ if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
+ if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
+ mDataValid = mCursor.requery();
+ }
+ }
+
+ private class ChangeObserver extends ContentObserver {
+ public ChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onContentChanged();
+ }
+ }
+
+ private class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ }
+
+}
diff --git a/android/widget/CursorFilter.java b/android/widget/CursorFilter.java
new file mode 100644
index 0000000..dbded69
--- /dev/null
+++ b/android/widget/CursorFilter.java
@@ -0,0 +1,70 @@
+/*
+ * 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 != null && results.values != oldCursor) {
+ mClient.changeCursor((Cursor) results.values);
+ }
+ }
+}
diff --git a/android/widget/CursorTreeAdapter.java b/android/widget/CursorTreeAdapter.java
new file mode 100644
index 0000000..405e45a
--- /dev/null
+++ b/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.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. If there is an existing cursor
+ * it will be closed.
+ */
+ public void setGroupCursor(Cursor cursor) {
+ mGroupCursorHelper.changeCursor(cursor, false);
+ }
+
+ /**
+ * Sets the children Cursor for a particular group. If there is an existing cursor
+ * it will be closed.
+ * <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 (cursor == mCursor) return;
+
+ deactivate();
+ 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.close();
+ 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 && !mCursor.isClosed()) {
+ if (false) 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/android/widget/DatePicker.java b/android/widget/DatePicker.java
new file mode 100644
index 0000000..dfb3642
--- /dev/null
+++ b/android/widget/DatePicker.java
@@ -0,0 +1,821 @@
+/*
+ * 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.IntDef;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.icu.util.Calendar;
+import android.icu.util.TimeZone;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * Provides a widget for selecting a date.
+ * <p>
+ * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
+ * set to {@code spinner}, the date can be selected using year, month, and day
+ * spinners or a {@link CalendarView}. The set of spinners and the calendar
+ * view are automatically synchronized. The client can customize whether only
+ * the spinners, or only the calendar view, or both to be displayed.
+ * </p>
+ * <p>
+ * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
+ * set to {@code calendar}, the month and day can be selected using a
+ * calendar-style view while the year can be selected separately using a list.
+ * </p>
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide.
+ * </p>
+ * <p>
+ * For a dialog using this view, see {@link android.app.DatePickerDialog}.
+ * </p>
+ *
+ * @attr ref android.R.styleable#DatePicker_startYear
+ * @attr ref android.R.styleable#DatePicker_endYear
+ * @attr ref android.R.styleable#DatePicker_maxDate
+ * @attr ref android.R.styleable#DatePicker_minDate
+ * @attr ref android.R.styleable#DatePicker_spinnersShown
+ * @attr ref android.R.styleable#DatePicker_calendarViewShown
+ * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground
+ * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerBackground
+ * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance
+ * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance
+ * @attr ref android.R.styleable#DatePicker_yearListSelectorColor
+ * @attr ref android.R.styleable#DatePicker_calendarTextColor
+ * @attr ref android.R.styleable#DatePicker_datePickerMode
+ */
+@Widget
+public class DatePicker extends FrameLayout {
+ private static final String LOG_TAG = DatePicker.class.getSimpleName();
+
+ /**
+ * Presentation mode for the Holo-style date picker that uses a set of
+ * {@link android.widget.NumberPicker}s.
+ *
+ * @see #getMode()
+ * @hide Visible for testing only.
+ */
+ @TestApi
+ public static final int MODE_SPINNER = 1;
+
+ /**
+ * Presentation mode for the Material-style date picker that uses a
+ * calendar.
+ *
+ * @see #getMode()
+ * @hide Visible for testing only.
+ */
+ @TestApi
+ public static final int MODE_CALENDAR = 2;
+
+ /** @hide */
+ @IntDef({MODE_SPINNER, MODE_CALENDAR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DatePickerMode {}
+
+ private final DatePickerDelegate mDelegate;
+
+ @DatePickerMode
+ private final int mMode;
+
+ /**
+ * The callback used to indicate the user changed the date.
+ */
+ public interface OnDateChangedListener {
+
+ /**
+ * Called upon a date change.
+ *
+ * @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, R.attr.datePickerStyle);
+ }
+
+ public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // DatePicker is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
+ defStyleAttr, defStyleRes);
+ final boolean isDialogMode = a.getBoolean(R.styleable.DatePicker_dialogMode, false);
+ final int requestedMode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER);
+ final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0);
+ a.recycle();
+
+ if (requestedMode == MODE_CALENDAR && isDialogMode) {
+ // You want MODE_CALENDAR? YOU CAN'T HANDLE MODE_CALENDAR! Well,
+ // maybe you can depending on your screen size. Let's check...
+ mMode = context.getResources().getInteger(R.integer.date_picker_mode);
+ } else {
+ mMode = requestedMode;
+ }
+
+ switch (mMode) {
+ case MODE_CALENDAR:
+ mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
+ break;
+ case MODE_SPINNER:
+ default:
+ mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
+ break;
+ }
+
+ if (firstDayOfWeek != 0) {
+ setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ mDelegate.setAutoFillChangeListener((v, y, m, d) -> {
+ final AutofillManager afm = context.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.notifyValueChanged(this);
+ }
+ });
+ }
+
+ private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr,
+ defStyleRes);
+ }
+
+ /**
+ * @return the picker's presentation mode, one of {@link #MODE_CALENDAR} or
+ * {@link #MODE_SPINNER}
+ * @attr ref android.R.styleable#DatePicker_datePickerMode
+ * @hide Visible for testing only.
+ */
+ @DatePickerMode
+ @TestApi
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * Initialize the state. If the provided values designate an inconsistent
+ * date the values are normalized before updating the spinners.
+ *
+ * @param year The initial year.
+ * @param monthOfYear The initial month <strong>starting from zero</strong>.
+ * @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) {
+ mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener);
+ }
+
+ /**
+ * Set the callback that indicates the date has been adjusted by the user.
+ *
+ * @param onDateChangedListener How user is notified date is changed by
+ * user, can be null.
+ */
+ public void setOnDateChangedListener(OnDateChangedListener onDateChangedListener) {
+ mDelegate.setOnDateChangedListener(onDateChangedListener);
+ }
+
+ /**
+ * Update the current date.
+ *
+ * @param year The year.
+ * @param month The month which is <strong>starting from zero</strong>.
+ * @param dayOfMonth The day of the month.
+ */
+ public void updateDate(int year, int month, int dayOfMonth) {
+ mDelegate.updateDate(year, month, dayOfMonth);
+ }
+
+ /**
+ * @return The selected year.
+ */
+ public int getYear() {
+ return mDelegate.getYear();
+ }
+
+ /**
+ * @return The selected month.
+ */
+ public int getMonth() {
+ return mDelegate.getMonth();
+ }
+
+ /**
+ * @return The selected day of month.
+ */
+ public int getDayOfMonth() {
+ return mDelegate.getDayOfMonth();
+ }
+
+ /**
+ * Gets the minimal date supported by this {@link DatePicker} in
+ * milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ * <p>
+ * Note: The default minimal date is 01/01/1900.
+ * <p>
+ *
+ * @return The minimal supported date.
+ */
+ public long getMinDate() {
+ return mDelegate.getMinDate().getTimeInMillis();
+ }
+
+ /**
+ * Sets the minimal date supported by this {@link NumberPicker} in
+ * milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @param minDate The minimal supported date.
+ */
+ public void setMinDate(long minDate) {
+ mDelegate.setMinDate(minDate);
+ }
+
+ /**
+ * Gets the maximal date supported by this {@link DatePicker} in
+ * milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ * <p>
+ * Note: The default maximal date is 12/31/2100.
+ * <p>
+ *
+ * @return The maximal supported date.
+ */
+ public long getMaxDate() {
+ return mDelegate.getMaxDate().getTimeInMillis();
+ }
+
+ /**
+ * Sets the maximal date supported by this {@link DatePicker} in
+ * milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @param maxDate The maximal supported date.
+ */
+ public void setMaxDate(long maxDate) {
+ mDelegate.setMaxDate(maxDate);
+ }
+
+ /**
+ * Sets the callback that indicates the current date is valid.
+ *
+ * @param callback the callback, may be null
+ * @hide
+ */
+ public void setValidationCallback(@Nullable ValidationCallback callback) {
+ mDelegate.setValidationCallback(callback);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (mDelegate.isEnabled() == enabled) {
+ return;
+ }
+ super.setEnabled(enabled);
+ mDelegate.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mDelegate.isEnabled();
+ }
+
+ /** @hide */
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ return mDelegate.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ /** @hide */
+ @Override
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEventInternal(event);
+ mDelegate.onPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return DatePicker.class.getName();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDelegate.onConfigurationChanged(newConfig);
+ }
+
+ /**
+ * Sets the first day of week.
+ *
+ * @param firstDayOfWeek The first day of the week conforming to the
+ * {@link CalendarView} APIs.
+ * @see Calendar#SUNDAY
+ * @see Calendar#MONDAY
+ * @see Calendar#TUESDAY
+ * @see Calendar#WEDNESDAY
+ * @see Calendar#THURSDAY
+ * @see Calendar#FRIDAY
+ * @see Calendar#SATURDAY
+ *
+ * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
+ */
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) {
+ throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7");
+ }
+ mDelegate.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ /**
+ * Gets the first day of week.
+ *
+ * @return The first day of the week conforming to the {@link CalendarView}
+ * APIs.
+ * @see Calendar#SUNDAY
+ * @see Calendar#MONDAY
+ * @see Calendar#TUESDAY
+ * @see Calendar#WEDNESDAY
+ * @see Calendar#THURSDAY
+ * @see Calendar#FRIDAY
+ * @see Calendar#SATURDAY
+ *
+ * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
+ */
+ public int getFirstDayOfWeek() {
+ return mDelegate.getFirstDayOfWeek();
+ }
+
+ /**
+ * Returns whether the {@link CalendarView} is shown.
+ * <p>
+ * <strong>Note:</strong> This method returns {@code false} when the
+ * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+ * to {@code calendar}.
+ *
+ * @return {@code true} if the calendar view is shown
+ * @see #getCalendarView()
+ * @deprecated Not supported by Material-style {@code calendar} mode
+ */
+ @Deprecated
+ public boolean getCalendarViewShown() {
+ return mDelegate.getCalendarViewShown();
+ }
+
+ /**
+ * Returns the {@link CalendarView} used by this picker.
+ * <p>
+ * <strong>Note:</strong> This method throws an
+ * {@link UnsupportedOperationException} when the
+ * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+ * to {@code calendar}.
+ *
+ * @return the calendar view
+ * @see #getCalendarViewShown()
+ * @deprecated Not supported by Material-style {@code calendar} mode
+ * @throws UnsupportedOperationException if called when the picker is
+ * displayed in {@code calendar} mode
+ */
+ @Deprecated
+ public CalendarView getCalendarView() {
+ return mDelegate.getCalendarView();
+ }
+
+ /**
+ * Sets whether the {@link CalendarView} is shown.
+ * <p>
+ * <strong>Note:</strong> Calling this method has no effect when the
+ * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+ * to {@code calendar}.
+ *
+ * @param shown {@code true} to show the calendar view, {@code false} to
+ * hide it
+ * @deprecated Not supported by Material-style {@code calendar} mode
+ */
+ @Deprecated
+ public void setCalendarViewShown(boolean shown) {
+ mDelegate.setCalendarViewShown(shown);
+ }
+
+ /**
+ * Returns whether the spinners are shown.
+ * <p>
+ * <strong>Note:</strong> his method returns {@code false} when the
+ * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+ * to {@code calendar}.
+ *
+ * @return {@code true} if the spinners are shown
+ * @deprecated Not supported by Material-style {@code calendar} mode
+ */
+ @Deprecated
+ public boolean getSpinnersShown() {
+ return mDelegate.getSpinnersShown();
+ }
+
+ /**
+ * Sets whether the spinners are shown.
+ * <p>
+ * Calling this method has no effect when the
+ * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+ * to {@code calendar}.
+ *
+ * @param shown {@code true} to show the spinners, {@code false} to hide
+ * them
+ * @deprecated Not supported by Material-style {@code calendar} mode
+ */
+ @Deprecated
+ public void setSpinnersShown(boolean shown) {
+ mDelegate.setSpinnersShown(shown);
+ }
+
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ dispatchThawSelfOnly(container);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return mDelegate.onSaveInstanceState(superState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ BaseSavedState ss = (BaseSavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDelegate.onRestoreInstanceState(ss);
+ }
+
+ /**
+ * A delegate interface that defined the public API of the DatePicker. Allows different
+ * DatePicker implementations. This would need to be implemented by the DatePicker delegates
+ * for the real behavior.
+ *
+ * @hide
+ */
+ interface DatePickerDelegate {
+ void init(int year, int monthOfYear, int dayOfMonth,
+ OnDateChangedListener onDateChangedListener);
+
+ void setOnDateChangedListener(OnDateChangedListener onDateChangedListener);
+ void setAutoFillChangeListener(OnDateChangedListener onDateChangedListener);
+
+ void updateDate(int year, int month, int dayOfMonth);
+
+ int getYear();
+ int getMonth();
+ int getDayOfMonth();
+
+ void autofill(AutofillValue value);
+ AutofillValue getAutofillValue();
+
+ void setFirstDayOfWeek(int firstDayOfWeek);
+ int getFirstDayOfWeek();
+
+ void setMinDate(long minDate);
+ Calendar getMinDate();
+
+ void setMaxDate(long maxDate);
+ Calendar getMaxDate();
+
+ void setEnabled(boolean enabled);
+ boolean isEnabled();
+
+ CalendarView getCalendarView();
+
+ void setCalendarViewShown(boolean shown);
+ boolean getCalendarViewShown();
+
+ void setSpinnersShown(boolean shown);
+ boolean getSpinnersShown();
+
+ void setValidationCallback(ValidationCallback callback);
+
+ void onConfigurationChanged(Configuration newConfig);
+
+ Parcelable onSaveInstanceState(Parcelable superState);
+ void onRestoreInstanceState(Parcelable state);
+
+ boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+ void onPopulateAccessibilityEvent(AccessibilityEvent event);
+ }
+
+ /**
+ * An abstract class which can be used as a start for DatePicker implementations
+ */
+ abstract static class AbstractDatePickerDelegate implements DatePickerDelegate {
+ // The delegator
+ protected DatePicker mDelegator;
+
+ // The context
+ protected Context mContext;
+
+ // NOTE: when subclasses change this variable, they must call resetAutofilledValue().
+ protected Calendar mCurrentDate;
+
+ // The current locale
+ protected Locale mCurrentLocale;
+
+ // Callbacks
+ protected OnDateChangedListener mOnDateChangedListener;
+ protected OnDateChangedListener mAutoFillChangeListener;
+ protected ValidationCallback mValidationCallback;
+
+ // The value that was passed to autofill() - it must be stored because it getAutofillValue()
+ // must return the exact same value that was autofilled, otherwise the widget will not be
+ // properly highlighted after autofill().
+ private long mAutofilledValue;
+
+ public AbstractDatePickerDelegate(DatePicker delegator, Context context) {
+ mDelegator = delegator;
+ mContext = context;
+
+ setCurrentLocale(Locale.getDefault());
+ }
+
+ protected void setCurrentLocale(Locale locale) {
+ if (!locale.equals(mCurrentLocale)) {
+ mCurrentLocale = locale;
+ onLocaleChanged(locale);
+ }
+ }
+
+ @Override
+ public void setOnDateChangedListener(OnDateChangedListener callback) {
+ mOnDateChangedListener = callback;
+ }
+
+ @Override
+ public void setAutoFillChangeListener(OnDateChangedListener callback) {
+ mAutoFillChangeListener = callback;
+ }
+
+ @Override
+ public void setValidationCallback(ValidationCallback callback) {
+ mValidationCallback = callback;
+ }
+
+ @Override
+ public final void autofill(AutofillValue value) {
+ if (value == null || !value.isDate()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ final long time = value.getDateValue();
+
+ final Calendar cal = Calendar.getInstance(mCurrentLocale);
+ cal.setTimeInMillis(time);
+ updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
+ cal.get(Calendar.DAY_OF_MONTH));
+
+ // Must set mAutofilledValue *after* calling subclass method to make sure the value
+ // returned by getAutofillValue() matches it.
+ mAutofilledValue = time;
+ }
+
+ @Override
+ public final AutofillValue getAutofillValue() {
+ final long time = mAutofilledValue != 0
+ ? mAutofilledValue
+ : mCurrentDate.getTimeInMillis();
+ return AutofillValue.forDate(time);
+ }
+
+ /**
+ * This method must be called every time the value of the year, month, and/or day is
+ * changed by a subclass method.
+ */
+ protected void resetAutofilledValue() {
+ mAutofilledValue = 0;
+ }
+
+ protected void onValidationChanged(boolean valid) {
+ if (mValidationCallback != null) {
+ mValidationCallback.onValidationChanged(valid);
+ }
+ }
+
+ protected void onLocaleChanged(Locale locale) {
+ // Stub.
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ event.getText().add(getFormattedCurrentDate());
+ }
+
+ protected String getFormattedCurrentDate() {
+ return DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(),
+ DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
+ | DateUtils.FORMAT_SHOW_WEEKDAY);
+ }
+
+ /**
+ * Class for managing state storing/restoring.
+ */
+ static class SavedState extends View.BaseSavedState {
+ private final int mSelectedYear;
+ private final int mSelectedMonth;
+ private final int mSelectedDay;
+ private final long mMinDate;
+ private final long mMaxDate;
+ private final int mCurrentView;
+ private final int mListPosition;
+ private final int mListPositionOffset;
+
+ public SavedState(Parcelable superState, int year, int month, int day, long minDate,
+ long maxDate) {
+ this(superState, year, month, day, minDate, maxDate, 0, 0, 0);
+ }
+
+ /**
+ * Constructor called from {@link DatePicker#onSaveInstanceState()}
+ */
+ public SavedState(Parcelable superState, int year, int month, int day, long minDate,
+ long maxDate, int currentView, int listPosition, int listPositionOffset) {
+ super(superState);
+ mSelectedYear = year;
+ mSelectedMonth = month;
+ mSelectedDay = day;
+ mMinDate = minDate;
+ mMaxDate = maxDate;
+ mCurrentView = currentView;
+ mListPosition = listPosition;
+ mListPositionOffset = listPositionOffset;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ mSelectedYear = in.readInt();
+ mSelectedMonth = in.readInt();
+ mSelectedDay = in.readInt();
+ mMinDate = in.readLong();
+ mMaxDate = in.readLong();
+ mCurrentView = in.readInt();
+ mListPosition = in.readInt();
+ mListPositionOffset = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mSelectedYear);
+ dest.writeInt(mSelectedMonth);
+ dest.writeInt(mSelectedDay);
+ dest.writeLong(mMinDate);
+ dest.writeLong(mMaxDate);
+ dest.writeInt(mCurrentView);
+ dest.writeInt(mListPosition);
+ dest.writeInt(mListPositionOffset);
+ }
+
+ public int getSelectedDay() {
+ return mSelectedDay;
+ }
+
+ public int getSelectedMonth() {
+ return mSelectedMonth;
+ }
+
+ public int getSelectedYear() {
+ return mSelectedYear;
+ }
+
+ public long getMinDate() {
+ return mMinDate;
+ }
+
+ public long getMaxDate() {
+ return mMaxDate;
+ }
+
+ public int getCurrentView() {
+ return mCurrentView;
+ }
+
+ public int getListPosition() {
+ return mListPosition;
+ }
+
+ public int getListPositionOffset() {
+ return mListPositionOffset;
+ }
+
+ @SuppressWarnings("all")
+ // suppress unused and hiding
+ 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];
+ }
+ };
+ }
+ }
+
+ /**
+ * A callback interface for updating input validity when the date picker
+ * when included into a dialog.
+ *
+ * @hide
+ */
+ public interface ValidationCallback {
+ void onValidationChanged(boolean valid);
+ }
+
+ @Override
+ public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
+ // This view is self-sufficient for autofill, so it needs to call
+ // onProvideAutoFillStructure() to fill itself, but it does not need to call
+ // dispatchProvideAutoFillStructure() to fill its children.
+ structure.setAutofillId(getAutofillId());
+ onProvideAutofillStructure(structure, flags);
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!isEnabled()) return;
+
+ mDelegate.autofill(value);
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ return isEnabled() ? mDelegate.getAutofillValue() : null;
+ }
+}
diff --git a/android/widget/DatePickerCalendarDelegate.java b/android/widget/DatePickerCalendarDelegate.java
new file mode 100644
index 0000000..60b4757
--- /dev/null
+++ b/android/widget/DatePickerCalendarDelegate.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2014 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.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.icu.text.DisplayContext;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.os.Parcelable;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.DayPickerView.OnDaySelectedListener;
+import android.widget.YearPickerView.OnYearSelectedListener;
+
+import com.android.internal.R;
+
+import java.util.Locale;
+
+/**
+ * A delegate for picking up a date (day / month / year).
+ */
+class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
+ private static final int USE_LOCALE = 0;
+
+ private static final int UNINITIALIZED = -1;
+ private static final int VIEW_MONTH_DAY = 0;
+ private static final int VIEW_YEAR = 1;
+
+ private static final int DEFAULT_START_YEAR = 1900;
+ private static final int DEFAULT_END_YEAR = 2100;
+
+ private static final int ANIMATION_DURATION = 300;
+
+ private static final int[] ATTRS_TEXT_COLOR = new int[] {
+ com.android.internal.R.attr.textColor};
+ private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
+ com.android.internal.R.attr.disabledAlpha};
+
+ private SimpleDateFormat mYearFormat;
+ private SimpleDateFormat mMonthDayFormat;
+
+ // Top-level container.
+ private ViewGroup mContainer;
+
+ // Header views.
+ private TextView mHeaderYear;
+ private TextView mHeaderMonthDay;
+
+ // Picker views.
+ private ViewAnimator mAnimator;
+ private DayPickerView mDayPickerView;
+ private YearPickerView mYearPickerView;
+
+ // Accessibility strings.
+ private String mSelectDay;
+ private String mSelectYear;
+
+ private int mCurrentView = UNINITIALIZED;
+
+ private final Calendar mTempDate;
+ private final Calendar mMinDate;
+ private final Calendar mMaxDate;
+
+ private int mFirstDayOfWeek = USE_LOCALE;
+
+ public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ final Locale locale = mCurrentLocale;
+ mCurrentDate = Calendar.getInstance(locale);
+ mTempDate = Calendar.getInstance(locale);
+ mMinDate = Calendar.getInstance(locale);
+ mMaxDate = Calendar.getInstance(locale);
+
+ mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
+ mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
+
+ final Resources res = mDelegator.getResources();
+ final TypedArray a = mContext.obtainStyledAttributes(attrs,
+ R.styleable.DatePicker, defStyleAttr, defStyleRes);
+ final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final int layoutResourceId = a.getResourceId(
+ R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
+
+ // Set up and attach container.
+ mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
+ mContainer.setSaveFromParentEnabled(false);
+ mDelegator.addView(mContainer);
+
+ // Set up header views.
+ final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
+ mHeaderYear = header.findViewById(R.id.date_picker_header_year);
+ mHeaderYear.setOnClickListener(mOnHeaderClickListener);
+ mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
+ mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
+
+ // For the sake of backwards compatibility, attempt to extract the text
+ // color from the header month text appearance. If it's set, we'll let
+ // that override the "real" header text color.
+ ColorStateList headerTextColor = null;
+
+ @SuppressWarnings("deprecation")
+ final int monthHeaderTextAppearance = a.getResourceId(
+ R.styleable.DatePicker_headerMonthTextAppearance, 0);
+ if (monthHeaderTextAppearance != 0) {
+ final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
+ ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
+ final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
+ headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
+ textAppearance.recycle();
+ }
+
+ if (headerTextColor == null) {
+ headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
+ }
+
+ if (headerTextColor != null) {
+ mHeaderYear.setTextColor(headerTextColor);
+ mHeaderMonthDay.setTextColor(headerTextColor);
+ }
+
+ // Set up header background, if available.
+ if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
+ header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
+ }
+
+ a.recycle();
+
+ // Set up picker container.
+ mAnimator = mContainer.findViewById(R.id.animator);
+
+ // Set up day picker view.
+ mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker);
+ mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
+ mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
+ mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
+ mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+ mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
+
+ // Set up year picker view.
+ mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker);
+ mYearPickerView.setRange(mMinDate, mMaxDate);
+ mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
+ mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
+
+ // Set up content descriptions.
+ mSelectDay = res.getString(R.string.select_day);
+ mSelectYear = res.getString(R.string.select_year);
+
+ // Initialize for current locale. This also initializes the date, so no
+ // need to call onDateChanged.
+ onLocaleChanged(mCurrentLocale);
+
+ setCurrentView(VIEW_MONTH_DAY);
+ }
+
+ /**
+ * The legacy text color might have been poorly defined. Ensures that it
+ * has an appropriate activated state, using the selected state if one
+ * exists or modifying the default text color otherwise.
+ *
+ * @param color a legacy text color, or {@code null}
+ * @return a color state list with an appropriate activated state, or
+ * {@code null} if a valid activated state could not be generated
+ */
+ @Nullable
+ private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
+ if (color == null || color.hasState(R.attr.state_activated)) {
+ return color;
+ }
+
+ final int activatedColor;
+ final int defaultColor;
+ if (color.hasState(R.attr.state_selected)) {
+ activatedColor = color.getColorForState(StateSet.get(
+ StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
+ defaultColor = color.getColorForState(StateSet.get(
+ StateSet.VIEW_STATE_ENABLED), 0);
+ } else {
+ activatedColor = color.getDefaultColor();
+
+ // Generate a non-activated color using the disabled alpha.
+ final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
+ final float disabledAlpha = ta.getFloat(0, 0.30f);
+ defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
+ }
+
+ if (activatedColor == 0 || defaultColor == 0) {
+ // We somehow failed to obtain the colors.
+ return null;
+ }
+
+ final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
+ final int[] colors = new int[] { activatedColor, defaultColor };
+ return new ColorStateList(stateSet, colors);
+ }
+
+ private int multiplyAlphaComponent(int color, float alphaMod) {
+ final int srcRgb = color & 0xFFFFFF;
+ final int srcAlpha = (color >> 24) & 0xFF;
+ final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
+ return srcRgb | (dstAlpha << 24);
+ }
+
+ /**
+ * Listener called when the user selects a day in the day picker view.
+ */
+ private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
+ @Override
+ public void onDaySelected(DayPickerView view, Calendar day) {
+ mCurrentDate.setTimeInMillis(day.getTimeInMillis());
+ onDateChanged(true, true);
+ }
+ };
+
+ /**
+ * Listener called when the user selects a year in the year picker view.
+ */
+ private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
+ @Override
+ public void onYearChanged(YearPickerView view, int year) {
+ // If the newly selected month / year does not contain the
+ // currently selected day number, change the selected day number
+ // to the last day of the selected month or year.
+ // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
+ // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
+ final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+ final int month = mCurrentDate.get(Calendar.MONTH);
+ final int daysInMonth = getDaysInMonth(month, year);
+ if (day > daysInMonth) {
+ mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
+ }
+
+ mCurrentDate.set(Calendar.YEAR, year);
+ onDateChanged(true, true);
+
+ // Automatically switch to day picker.
+ setCurrentView(VIEW_MONTH_DAY);
+
+ // Switch focus back to the year text.
+ mHeaderYear.requestFocus();
+ }
+ };
+
+ /**
+ * Listener called when the user clicks on a header item.
+ */
+ private final OnClickListener mOnHeaderClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ tryVibrate();
+
+ switch (v.getId()) {
+ case R.id.date_picker_header_year:
+ setCurrentView(VIEW_YEAR);
+ break;
+ case R.id.date_picker_header_date:
+ setCurrentView(VIEW_MONTH_DAY);
+ break;
+ }
+ }
+ };
+
+ @Override
+ protected void onLocaleChanged(Locale locale) {
+ final TextView headerYear = mHeaderYear;
+ if (headerYear == null) {
+ // Abort, we haven't initialized yet. This method will get called
+ // again later after everything has been set up.
+ return;
+ }
+
+ // Update the date formatter.
+ final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd");
+ mMonthDayFormat = new SimpleDateFormat(datePattern, locale);
+ mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+ mYearFormat = new SimpleDateFormat("y", locale);
+
+ // Update the header text.
+ onCurrentDateChanged(false);
+ }
+
+ private void onCurrentDateChanged(boolean announce) {
+ if (mHeaderYear == null) {
+ // Abort, we haven't initialized yet. This method will get called
+ // again later after everything has been set up.
+ return;
+ }
+
+ final String year = mYearFormat.format(mCurrentDate.getTime());
+ mHeaderYear.setText(year);
+
+ final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
+ mHeaderMonthDay.setText(monthDay);
+
+ // TODO: This should use live regions.
+ if (announce) {
+ mAnimator.announceForAccessibility(getFormattedCurrentDate());
+ }
+ }
+
+ private void setCurrentView(final int viewIndex) {
+ switch (viewIndex) {
+ case VIEW_MONTH_DAY:
+ mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+
+ if (mCurrentView != viewIndex) {
+ mHeaderMonthDay.setActivated(true);
+ mHeaderYear.setActivated(false);
+ mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
+ mCurrentView = viewIndex;
+ }
+
+ mAnimator.announceForAccessibility(mSelectDay);
+ break;
+ case VIEW_YEAR:
+ final int year = mCurrentDate.get(Calendar.YEAR);
+ mYearPickerView.setYear(year);
+ mYearPickerView.post(new Runnable() {
+ @Override
+ public void run() {
+ mYearPickerView.requestFocus();
+ final View selected = mYearPickerView.getSelectedView();
+ if (selected != null) {
+ selected.requestFocus();
+ }
+ }
+ });
+
+ if (mCurrentView != viewIndex) {
+ mHeaderMonthDay.setActivated(false);
+ mHeaderYear.setActivated(true);
+ mAnimator.setDisplayedChild(VIEW_YEAR);
+ mCurrentView = viewIndex;
+ }
+
+ mAnimator.announceForAccessibility(mSelectYear);
+ break;
+ }
+ }
+
+ @Override
+ public void init(int year, int month, int dayOfMonth,
+ DatePicker.OnDateChangedListener callBack) {
+ setDate(year, month, dayOfMonth);
+ onDateChanged(false, false);
+
+ mOnDateChangedListener = callBack;
+ }
+
+ @Override
+ public void updateDate(int year, int month, int dayOfMonth) {
+ setDate(year, month, dayOfMonth);
+ onDateChanged(false, true);
+ }
+
+ private void setDate(int year, int month, int dayOfMonth) {
+ mCurrentDate.set(Calendar.YEAR, year);
+ mCurrentDate.set(Calendar.MONTH, month);
+ mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+ resetAutofilledValue();
+ }
+
+ private void onDateChanged(boolean fromUser, boolean callbackToClient) {
+ final int year = mCurrentDate.get(Calendar.YEAR);
+
+ if (callbackToClient
+ && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) {
+ final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
+ final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+ if (mOnDateChangedListener != null) {
+ mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
+ }
+ if (mAutoFillChangeListener != null) {
+ mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
+ }
+ }
+
+ mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+ mYearPickerView.setYear(year);
+
+ onCurrentDateChanged(fromUser);
+
+ if (fromUser) {
+ tryVibrate();
+ }
+ }
+
+ @Override
+ public int getYear() {
+ return mCurrentDate.get(Calendar.YEAR);
+ }
+
+ @Override
+ public int getMonth() {
+ return mCurrentDate.get(Calendar.MONTH);
+ }
+
+ @Override
+ public int getDayOfMonth() {
+ return mCurrentDate.get(Calendar.DAY_OF_MONTH);
+ }
+
+ @Override
+ public void setMinDate(long minDate) {
+ mTempDate.setTimeInMillis(minDate);
+ if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
+ && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
+ // Same day, no-op.
+ return;
+ }
+ if (mCurrentDate.before(mTempDate)) {
+ mCurrentDate.setTimeInMillis(minDate);
+ onDateChanged(false, true);
+ }
+ mMinDate.setTimeInMillis(minDate);
+ mDayPickerView.setMinDate(minDate);
+ mYearPickerView.setRange(mMinDate, mMaxDate);
+ }
+
+ @Override
+ public Calendar getMinDate() {
+ return mMinDate;
+ }
+
+ @Override
+ public void setMaxDate(long maxDate) {
+ mTempDate.setTimeInMillis(maxDate);
+ if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
+ && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
+ // Same day, no-op.
+ return;
+ }
+ if (mCurrentDate.after(mTempDate)) {
+ mCurrentDate.setTimeInMillis(maxDate);
+ onDateChanged(false, true);
+ }
+ mMaxDate.setTimeInMillis(maxDate);
+ mDayPickerView.setMaxDate(maxDate);
+ mYearPickerView.setRange(mMinDate, mMaxDate);
+ }
+
+ @Override
+ public Calendar getMaxDate() {
+ return mMaxDate;
+ }
+
+ @Override
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ mFirstDayOfWeek = firstDayOfWeek;
+
+ mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ @Override
+ public int getFirstDayOfWeek() {
+ if (mFirstDayOfWeek != USE_LOCALE) {
+ return mFirstDayOfWeek;
+ }
+ return mCurrentDate.getFirstDayOfWeek();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mContainer.setEnabled(enabled);
+ mDayPickerView.setEnabled(enabled);
+ mYearPickerView.setEnabled(enabled);
+ mHeaderYear.setEnabled(enabled);
+ mHeaderMonthDay.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mContainer.isEnabled();
+ }
+
+ @Override
+ public CalendarView getCalendarView() {
+ throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker");
+ }
+
+ @Override
+ public void setCalendarViewShown(boolean shown) {
+ // No-op for compatibility with the old DatePicker.
+ }
+
+ @Override
+ public boolean getCalendarViewShown() {
+ return false;
+ }
+
+ @Override
+ public void setSpinnersShown(boolean shown) {
+ // No-op for compatibility with the old DatePicker.
+ }
+
+ @Override
+ public boolean getSpinnersShown() {
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ setCurrentLocale(newConfig.locale);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState(Parcelable superState) {
+ final int year = mCurrentDate.get(Calendar.YEAR);
+ final int month = mCurrentDate.get(Calendar.MONTH);
+ final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+
+ int listPosition = -1;
+ int listPositionOffset = -1;
+
+ if (mCurrentView == VIEW_MONTH_DAY) {
+ listPosition = mDayPickerView.getMostVisiblePosition();
+ } else if (mCurrentView == VIEW_YEAR) {
+ listPosition = mYearPickerView.getFirstVisiblePosition();
+ listPositionOffset = mYearPickerView.getFirstPositionOffset();
+ }
+
+ return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
+ mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ final SavedState ss = (SavedState) state;
+
+ // TODO: Move instance state into DayPickerView, YearPickerView.
+ mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
+ mMinDate.setTimeInMillis(ss.getMinDate());
+ mMaxDate.setTimeInMillis(ss.getMaxDate());
+
+ onCurrentDateChanged(false);
+
+ final int currentView = ss.getCurrentView();
+ setCurrentView(currentView);
+
+ final int listPosition = ss.getListPosition();
+ if (listPosition != -1) {
+ if (currentView == VIEW_MONTH_DAY) {
+ mDayPickerView.setPosition(listPosition);
+ } else if (currentView == VIEW_YEAR) {
+ final int listPositionOffset = ss.getListPositionOffset();
+ mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ public CharSequence getAccessibilityClassName() {
+ return DatePicker.class.getName();
+ }
+
+ public static int getDaysInMonth(int month, int year) {
+ switch (month) {
+ case Calendar.JANUARY:
+ case Calendar.MARCH:
+ case Calendar.MAY:
+ case Calendar.JULY:
+ case Calendar.AUGUST:
+ case Calendar.OCTOBER:
+ case Calendar.DECEMBER:
+ return 31;
+ case Calendar.APRIL:
+ case Calendar.JUNE:
+ case Calendar.SEPTEMBER:
+ case Calendar.NOVEMBER:
+ return 30;
+ case Calendar.FEBRUARY:
+ return (year % 4 == 0) ? 29 : 28;
+ default:
+ throw new IllegalArgumentException("Invalid Month");
+ }
+ }
+
+ private void tryVibrate() {
+ mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
+ }
+}
diff --git a/android/widget/DatePickerController.java b/android/widget/DatePickerController.java
new file mode 100644
index 0000000..8f809ba
--- /dev/null
+++ b/android/widget/DatePickerController.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.Calendar;
+
+/**
+ * Controller class to communicate among the various components of the date picker dialog.
+ *
+ * @hide
+ */
+interface DatePickerController {
+
+ void onYearSelected(int year);
+
+ void registerOnDateChangedListener(OnDateChangedListener listener);
+
+ Calendar getSelectedDay();
+
+ void tryVibrate();
+}
diff --git a/android/widget/DatePickerSpinnerDelegate.java b/android/widget/DatePickerSpinnerDelegate.java
new file mode 100644
index 0000000..dba74b1
--- /dev/null
+++ b/android/widget/DatePickerSpinnerDelegate.java
@@ -0,0 +1,650 @@
+/*
+ * Copyright (C) 2016 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.icu.util.Calendar;
+import android.os.Parcelable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.DatePicker.AbstractDatePickerDelegate;
+import android.widget.NumberPicker.OnValueChangeListener;
+
+import libcore.icu.ICU;
+
+import java.text.DateFormatSymbols;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * A delegate implementing the basic DatePicker
+ */
+class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
+
+ private static final String DATE_FORMAT = "MM/dd/yyyy";
+
+ private static final int DEFAULT_START_YEAR = 1900;
+
+ private static final int DEFAULT_END_YEAR = 2100;
+
+ private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
+
+ private static final boolean DEFAULT_SPINNERS_SHOWN = true;
+
+ private static final boolean DEFAULT_ENABLED_STATE = true;
+
+ private final LinearLayout mSpinners;
+
+ private final NumberPicker mDaySpinner;
+
+ private final NumberPicker mMonthSpinner;
+
+ private final NumberPicker mYearSpinner;
+
+ private final EditText mDaySpinnerInput;
+
+ private final EditText mMonthSpinnerInput;
+
+ private final EditText mYearSpinnerInput;
+
+ private final CalendarView mCalendarView;
+
+ private String[] mShortMonths;
+
+ private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
+
+ private int mNumberOfMonths;
+
+ private Calendar mTempDate;
+
+ private Calendar mMinDate;
+
+ private Calendar mMaxDate;
+
+ private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+
+ DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ mDelegator = delegator;
+ mContext = context;
+
+ // initialization based on locale
+ setCurrentLocale(Locale.getDefault());
+
+ final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.DatePicker, defStyleAttr, defStyleRes);
+ boolean spinnersShown = attributesArray.getBoolean(com.android.internal.R.styleable.DatePicker_spinnersShown,
+ DEFAULT_SPINNERS_SHOWN);
+ boolean calendarViewShown = attributesArray.getBoolean(
+ com.android.internal.R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
+ int startYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_startYear,
+ DEFAULT_START_YEAR);
+ int endYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
+ String minDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_minDate);
+ String maxDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_maxDate);
+ int layoutResourceId = attributesArray.getResourceId(
+ com.android.internal.R.styleable.DatePicker_legacyLayout, com.android.internal.R.layout.date_picker_legacy);
+ attributesArray.recycle();
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ final View view = inflater.inflate(layoutResourceId, mDelegator, true);
+ view.setSaveFromParentEnabled(false);
+
+ OnValueChangeListener onChangeListener = new OnValueChangeListener() {
+ public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
+ updateInputState();
+ mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
+ // take care of wrapping of days and months to update greater fields
+ if (picker == mDaySpinner) {
+ int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
+ if (oldVal == maxDayOfMonth && newVal == 1) {
+ mTempDate.add(Calendar.DAY_OF_MONTH, 1);
+ } else if (oldVal == 1 && newVal == maxDayOfMonth) {
+ mTempDate.add(Calendar.DAY_OF_MONTH, -1);
+ } else {
+ mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
+ }
+ } else if (picker == mMonthSpinner) {
+ if (oldVal == 11 && newVal == 0) {
+ mTempDate.add(Calendar.MONTH, 1);
+ } else if (oldVal == 0 && newVal == 11) {
+ mTempDate.add(Calendar.MONTH, -1);
+ } else {
+ mTempDate.add(Calendar.MONTH, newVal - oldVal);
+ }
+ } else if (picker == mYearSpinner) {
+ mTempDate.set(Calendar.YEAR, newVal);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ // now set the date to the adjusted one
+ setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
+ mTempDate.get(Calendar.DAY_OF_MONTH));
+ updateSpinners();
+ updateCalendarView();
+ notifyDateChanged();
+ }
+ };
+
+ mSpinners = (LinearLayout) mDelegator.findViewById(com.android.internal.R.id.pickers);
+
+ // calendar view day-picker
+ mCalendarView = (CalendarView) mDelegator.findViewById(com.android.internal.R.id.calendar_view);
+ mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
+ public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
+ setDate(year, month, monthDay);
+ updateSpinners();
+ notifyDateChanged();
+ }
+ });
+
+ // day
+ mDaySpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.day);
+ mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
+ mDaySpinner.setOnLongPressUpdateInterval(100);
+ mDaySpinner.setOnValueChangedListener(onChangeListener);
+ mDaySpinnerInput = (EditText) mDaySpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+ // month
+ mMonthSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.month);
+ mMonthSpinner.setMinValue(0);
+ mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
+ mMonthSpinner.setDisplayedValues(mShortMonths);
+ mMonthSpinner.setOnLongPressUpdateInterval(200);
+ mMonthSpinner.setOnValueChangedListener(onChangeListener);
+ mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+ // year
+ mYearSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.year);
+ mYearSpinner.setOnLongPressUpdateInterval(100);
+ mYearSpinner.setOnValueChangedListener(onChangeListener);
+ mYearSpinnerInput = (EditText) mYearSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+ // show only what the user required but make sure we
+ // show something and the spinners have higher priority
+ if (!spinnersShown && !calendarViewShown) {
+ setSpinnersShown(true);
+ } else {
+ setSpinnersShown(spinnersShown);
+ setCalendarViewShown(calendarViewShown);
+ }
+
+ // set the min date giving priority of the minDate over startYear
+ mTempDate.clear();
+ if (!TextUtils.isEmpty(minDate)) {
+ if (!parseDate(minDate, mTempDate)) {
+ mTempDate.set(startYear, 0, 1);
+ }
+ } else {
+ mTempDate.set(startYear, 0, 1);
+ }
+ setMinDate(mTempDate.getTimeInMillis());
+
+ // set the max date giving priority of the maxDate over endYear
+ mTempDate.clear();
+ if (!TextUtils.isEmpty(maxDate)) {
+ if (!parseDate(maxDate, mTempDate)) {
+ mTempDate.set(endYear, 11, 31);
+ }
+ } else {
+ mTempDate.set(endYear, 11, 31);
+ }
+ setMaxDate(mTempDate.getTimeInMillis());
+
+ // initialize to current date
+ mCurrentDate.setTimeInMillis(System.currentTimeMillis());
+ init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
+ .get(Calendar.DAY_OF_MONTH), null);
+
+ // re-order the number spinners to match the current date format
+ reorderSpinners();
+
+ // accessibility
+ setContentDescriptions();
+
+ // If not explicitly specified this view is important for accessibility.
+ if (mDelegator.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ mDelegator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ @Override
+ public void init(int year, int monthOfYear, int dayOfMonth,
+ DatePicker.OnDateChangedListener onDateChangedListener) {
+ setDate(year, monthOfYear, dayOfMonth);
+ updateSpinners();
+ updateCalendarView();
+
+ mOnDateChangedListener = onDateChangedListener;
+ }
+
+ @Override
+ public void updateDate(int year, int month, int dayOfMonth) {
+ if (!isNewDate(year, month, dayOfMonth)) {
+ return;
+ }
+ setDate(year, month, dayOfMonth);
+ updateSpinners();
+ updateCalendarView();
+ notifyDateChanged();
+ }
+
+ @Override
+ public int getYear() {
+ return mCurrentDate.get(Calendar.YEAR);
+ }
+
+ @Override
+ public int getMonth() {
+ return mCurrentDate.get(Calendar.MONTH);
+ }
+
+ @Override
+ public int getDayOfMonth() {
+ return mCurrentDate.get(Calendar.DAY_OF_MONTH);
+ }
+
+ @Override
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ @Override
+ public int getFirstDayOfWeek() {
+ return mCalendarView.getFirstDayOfWeek();
+ }
+
+ @Override
+ public void setMinDate(long minDate) {
+ mTempDate.setTimeInMillis(minDate);
+ if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
+ && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
+ // Same day, no-op.
+ return;
+ }
+ mMinDate.setTimeInMillis(minDate);
+ mCalendarView.setMinDate(minDate);
+ if (mCurrentDate.before(mMinDate)) {
+ mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
+ updateCalendarView();
+ }
+ updateSpinners();
+ }
+
+ @Override
+ public Calendar getMinDate() {
+ final Calendar minDate = Calendar.getInstance();
+ minDate.setTimeInMillis(mCalendarView.getMinDate());
+ return minDate;
+ }
+
+ @Override
+ public void setMaxDate(long maxDate) {
+ mTempDate.setTimeInMillis(maxDate);
+ if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
+ && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
+ // Same day, no-op.
+ return;
+ }
+ mMaxDate.setTimeInMillis(maxDate);
+ mCalendarView.setMaxDate(maxDate);
+ if (mCurrentDate.after(mMaxDate)) {
+ mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
+ updateCalendarView();
+ }
+ updateSpinners();
+ }
+
+ @Override
+ public Calendar getMaxDate() {
+ final Calendar maxDate = Calendar.getInstance();
+ maxDate.setTimeInMillis(mCalendarView.getMaxDate());
+ return maxDate;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mDaySpinner.setEnabled(enabled);
+ mMonthSpinner.setEnabled(enabled);
+ mYearSpinner.setEnabled(enabled);
+ mCalendarView.setEnabled(enabled);
+ mIsEnabled = enabled;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ @Override
+ public CalendarView getCalendarView() {
+ return mCalendarView;
+ }
+
+ @Override
+ public void setCalendarViewShown(boolean shown) {
+ mCalendarView.setVisibility(shown ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean getCalendarViewShown() {
+ return (mCalendarView.getVisibility() == View.VISIBLE);
+ }
+
+ @Override
+ public void setSpinnersShown(boolean shown) {
+ mSpinners.setVisibility(shown ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean getSpinnersShown() {
+ return mSpinners.isShown();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ setCurrentLocale(newConfig.locale);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState(Parcelable superState) {
+ return new SavedState(superState, getYear(), getMonth(), getDayOfMonth(),
+ getMinDate().getTimeInMillis(), getMaxDate().getTimeInMillis());
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ final SavedState ss = (SavedState) state;
+ setDate(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
+ updateSpinners();
+ updateCalendarView();
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ /**
+ * Sets the current locale.
+ *
+ * @param locale The current locale.
+ */
+ @Override
+ protected void setCurrentLocale(Locale locale) {
+ super.setCurrentLocale(locale);
+
+ mTempDate = getCalendarForLocale(mTempDate, locale);
+ mMinDate = getCalendarForLocale(mMinDate, locale);
+ mMaxDate = getCalendarForLocale(mMaxDate, locale);
+ mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
+
+ mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
+ mShortMonths = new DateFormatSymbols().getShortMonths();
+
+ if (usingNumericMonths()) {
+ // We're in a locale where a date should either be all-numeric, or all-text.
+ // All-text would require custom NumberPicker formatters for day and year.
+ mShortMonths = new String[mNumberOfMonths];
+ for (int i = 0; i < mNumberOfMonths; ++i) {
+ mShortMonths[i] = String.format("%d", i + 1);
+ }
+ }
+ }
+
+ /**
+ * Tests whether the current locale is one where there are no real month names,
+ * such as Chinese, Japanese, or Korean locales.
+ */
+ private boolean usingNumericMonths() {
+ return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
+ }
+
+ /**
+ * Gets a calendar for locale bootstrapped with the value of a given calendar.
+ *
+ * @param oldCalendar The old calendar.
+ * @param locale The locale.
+ */
+ private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+ if (oldCalendar == null) {
+ return Calendar.getInstance(locale);
+ } else {
+ final long currentTimeMillis = oldCalendar.getTimeInMillis();
+ Calendar newCalendar = Calendar.getInstance(locale);
+ newCalendar.setTimeInMillis(currentTimeMillis);
+ return newCalendar;
+ }
+ }
+
+ /**
+ * Reorders the spinners according to the date format that is
+ * explicitly set by the user and if no such is set fall back
+ * to the current locale's default format.
+ */
+ private void reorderSpinners() {
+ mSpinners.removeAllViews();
+ // We use numeric spinners for year and day, but textual months. Ask icu4c what
+ // order the user's locale uses for that combination. http://b/7207103.
+ String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
+ char[] order = ICU.getDateFormatOrder(pattern);
+ final int spinnerCount = order.length;
+ for (int i = 0; i < spinnerCount; i++) {
+ switch (order[i]) {
+ case 'd':
+ mSpinners.addView(mDaySpinner);
+ setImeOptions(mDaySpinner, spinnerCount, i);
+ break;
+ case 'M':
+ mSpinners.addView(mMonthSpinner);
+ setImeOptions(mMonthSpinner, spinnerCount, i);
+ break;
+ case 'y':
+ mSpinners.addView(mYearSpinner);
+ setImeOptions(mYearSpinner, spinnerCount, i);
+ break;
+ default:
+ throw new IllegalArgumentException(Arrays.toString(order));
+ }
+ }
+ }
+
+ /**
+ * Parses the given <code>date</code> and in case of success sets the result
+ * to the <code>outDate</code>.
+ *
+ * @return True if the date was parsed.
+ */
+ private boolean parseDate(String date, Calendar outDate) {
+ try {
+ outDate.setTime(mDateFormat.parse(date));
+ return true;
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private boolean isNewDate(int year, int month, int dayOfMonth) {
+ return (mCurrentDate.get(Calendar.YEAR) != year
+ || mCurrentDate.get(Calendar.MONTH) != month
+ || mCurrentDate.get(Calendar.DAY_OF_MONTH) != dayOfMonth);
+ }
+
+ private void setDate(int year, int month, int dayOfMonth) {
+ mCurrentDate.set(year, month, dayOfMonth);
+ resetAutofilledValue();
+ if (mCurrentDate.before(mMinDate)) {
+ mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
+ } else if (mCurrentDate.after(mMaxDate)) {
+ mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
+ }
+ }
+
+ private void updateSpinners() {
+ // set the spinner ranges respecting the min and max dates
+ if (mCurrentDate.equals(mMinDate)) {
+ mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+ mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+ mDaySpinner.setWrapSelectorWheel(false);
+ mMonthSpinner.setDisplayedValues(null);
+ mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
+ mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
+ mMonthSpinner.setWrapSelectorWheel(false);
+ } else if (mCurrentDate.equals(mMaxDate)) {
+ mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
+ mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+ mDaySpinner.setWrapSelectorWheel(false);
+ mMonthSpinner.setDisplayedValues(null);
+ mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
+ mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
+ mMonthSpinner.setWrapSelectorWheel(false);
+ } else {
+ mDaySpinner.setMinValue(1);
+ mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+ mDaySpinner.setWrapSelectorWheel(true);
+ mMonthSpinner.setDisplayedValues(null);
+ mMonthSpinner.setMinValue(0);
+ mMonthSpinner.setMaxValue(11);
+ mMonthSpinner.setWrapSelectorWheel(true);
+ }
+
+ // make sure the month names are a zero based array
+ // with the months in the month spinner
+ String[] displayedValues = Arrays.copyOfRange(mShortMonths,
+ mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
+ mMonthSpinner.setDisplayedValues(displayedValues);
+
+ // year spinner range does not change based on the current date
+ mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
+ mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
+ mYearSpinner.setWrapSelectorWheel(false);
+
+ // set the spinner values
+ mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
+ mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
+ mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+
+ if (usingNumericMonths()) {
+ mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+ }
+ }
+
+ /**
+ * Updates the calendar view with the current date.
+ */
+ private void updateCalendarView() {
+ mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
+ }
+
+
+ /**
+ * Notifies the listener, if such, for a change in the selected date.
+ */
+ private void notifyDateChanged() {
+ mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ if (mOnDateChangedListener != null) {
+ mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
+ getDayOfMonth());
+ }
+ if (mAutoFillChangeListener != null) {
+ mAutoFillChangeListener.onDateChanged(mDelegator, getYear(), getMonth(),
+ getDayOfMonth());
+ }
+ }
+
+ /**
+ * Sets the IME options for a spinner based on its ordering.
+ *
+ * @param spinner The spinner.
+ * @param spinnerCount The total spinner count.
+ * @param spinnerIndex The index of the given spinner.
+ */
+ private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
+ final int imeOptions;
+ if (spinnerIndex < spinnerCount - 1) {
+ imeOptions = EditorInfo.IME_ACTION_NEXT;
+ } else {
+ imeOptions = EditorInfo.IME_ACTION_DONE;
+ }
+ TextView input = (TextView) spinner.findViewById(com.android.internal.R.id.numberpicker_input);
+ input.setImeOptions(imeOptions);
+ }
+
+ private void setContentDescriptions() {
+ // Day
+ trySetContentDescription(mDaySpinner, com.android.internal.R.id.increment,
+ com.android.internal.R.string.date_picker_increment_day_button);
+ trySetContentDescription(mDaySpinner, com.android.internal.R.id.decrement,
+ com.android.internal.R.string.date_picker_decrement_day_button);
+ // Month
+ trySetContentDescription(mMonthSpinner, com.android.internal.R.id.increment,
+ com.android.internal.R.string.date_picker_increment_month_button);
+ trySetContentDescription(mMonthSpinner, com.android.internal.R.id.decrement,
+ com.android.internal.R.string.date_picker_decrement_month_button);
+ // Year
+ trySetContentDescription(mYearSpinner, com.android.internal.R.id.increment,
+ com.android.internal.R.string.date_picker_increment_year_button);
+ trySetContentDescription(mYearSpinner, com.android.internal.R.id.decrement,
+ com.android.internal.R.string.date_picker_decrement_year_button);
+ }
+
+ private void trySetContentDescription(View root, int viewId, int contDescResId) {
+ View target = root.findViewById(viewId);
+ if (target != null) {
+ target.setContentDescription(mContext.getString(contDescResId));
+ }
+ }
+
+ private void updateInputState() {
+ // Make sure that if the user changes the value and the IME is active
+ // for one of the inputs if this widget, the IME is closed. If the user
+ // changed the value via the IME and there is a next input the IME will
+ // be shown, otherwise the user chose another means of changing the
+ // value and having the IME up makes no sense.
+ InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+ if (inputMethodManager != null) {
+ if (inputMethodManager.isActive(mYearSpinnerInput)) {
+ mYearSpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
+ mMonthSpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
+ mDaySpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ }
+ }
+ }
+}
diff --git a/android/widget/DateTimeView.java b/android/widget/DateTimeView.java
new file mode 100644
index 0000000..4db3607
--- /dev/null
+++ b/android/widget/DateTimeView.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2010 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 static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+import static android.text.format.Time.getJulianDay;
+
+import android.app.ActivityThread;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.icu.util.Calendar;
+import android.os.Handler;
+import android.text.format.Time;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.TimeZone;
+
+//
+// TODO
+// - listen for the next threshold time to update the view.
+// - listen for date format pref changed
+// - put the AM/PM in a smaller font
+//
+
+/**
+ * Displays a given time in a convenient human-readable foramt.
+ *
+ * @hide
+ */
+@RemoteView
+public class DateTimeView extends TextView {
+ private static final int SHOW_TIME = 0;
+ private static final int SHOW_MONTH_DAY_YEAR = 1;
+
+ Date mTime;
+ long mTimeMillis;
+
+ int mLastDisplay = -1;
+ DateFormat mLastFormat;
+
+ private long mUpdateTimeMillis;
+ private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
+ private String mNowText;
+ private boolean mShowRelativeTime;
+
+ public DateTimeView(Context context) {
+ this(context, null);
+ }
+
+ public DateTimeView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.DateTimeView, 0,
+ 0);
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.DateTimeView_showRelative:
+ boolean relative = a.getBoolean(i, false);
+ setShowRelativeTime(relative);
+ break;
+ }
+ }
+ a.recycle();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ReceiverInfo ri = sReceiverInfo.get();
+ if (ri == null) {
+ ri = new ReceiverInfo();
+ sReceiverInfo.set(ri);
+ }
+ ri.addView(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ final ReceiverInfo ri = sReceiverInfo.get();
+ if (ri != null) {
+ ri.removeView(this);
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ public void setTime(long time) {
+ Time t = new Time();
+ t.set(time);
+ mTimeMillis = t.toMillis(false);
+ mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
+ update();
+ }
+
+ @android.view.RemotableViewMethod
+ public void setShowRelativeTime(boolean showRelativeTime) {
+ mShowRelativeTime = showRelativeTime;
+ updateNowText();
+ update();
+ }
+
+ @Override
+ @android.view.RemotableViewMethod
+ public void setVisibility(@Visibility int visibility) {
+ boolean gotVisible = visibility != GONE && getVisibility() == GONE;
+ super.setVisibility(visibility);
+ if (gotVisible) {
+ update();
+ }
+ }
+
+ void update() {
+ if (mTime == null || getVisibility() == GONE) {
+ return;
+ }
+ if (mShowRelativeTime) {
+ updateRelativeTime();
+ return;
+ }
+
+ int display;
+ Date time = mTime;
+
+ Time t = new Time();
+ t.set(mTimeMillis);
+ t.second = 0;
+
+ t.hour -= 12;
+ long twelveHoursBefore = t.toMillis(false);
+ t.hour += 12;
+ long twelveHoursAfter = t.toMillis(false);
+ t.hour = 0;
+ t.minute = 0;
+ long midnightBefore = t.toMillis(false);
+ t.monthDay++;
+ long midnightAfter = t.toMillis(false);
+
+ long nowMillis = System.currentTimeMillis();
+ t.set(nowMillis);
+ t.second = 0;
+ nowMillis = t.normalize(false);
+
+ // Choose the display mode
+ choose_display: {
+ if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
+ || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
+ display = SHOW_TIME;
+ break choose_display;
+ }
+ // Else, show month day and year.
+ display = SHOW_MONTH_DAY_YEAR;
+ break choose_display;
+ }
+
+ // Choose the format
+ DateFormat format;
+ if (display == mLastDisplay && mLastFormat != null) {
+ // use cached format
+ format = mLastFormat;
+ } else {
+ switch (display) {
+ case SHOW_TIME:
+ format = getTimeFormat();
+ break;
+ case SHOW_MONTH_DAY_YEAR:
+ format = DateFormat.getDateInstance(DateFormat.SHORT);
+ break;
+ default:
+ throw new RuntimeException("unknown display value: " + display);
+ }
+ mLastFormat = format;
+ }
+
+ // Set the text
+ String text = format.format(mTime);
+ setText(text);
+
+ // Schedule the next update
+ if (display == SHOW_TIME) {
+ // Currently showing the time, update at the later of twelve hours after or midnight.
+ mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
+ } else {
+ // Currently showing the date
+ if (mTimeMillis < nowMillis) {
+ // If the time is in the past, don't schedule an update
+ mUpdateTimeMillis = 0;
+ } else {
+ // If hte time is in the future, schedule one at the earlier of twelve hours
+ // before or midnight before.
+ mUpdateTimeMillis = twelveHoursBefore < midnightBefore
+ ? twelveHoursBefore : midnightBefore;
+ }
+ }
+ }
+
+ private void updateRelativeTime() {
+ long now = System.currentTimeMillis();
+ long duration = Math.abs(now - mTimeMillis);
+ int count;
+ long millisIncrease;
+ boolean past = (now >= mTimeMillis);
+ String result;
+ if (duration < MINUTE_IN_MILLIS) {
+ setText(mNowText);
+ mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
+ return;
+ } else if (duration < HOUR_IN_MILLIS) {
+ count = (int)(duration / MINUTE_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.R.plurals.duration_minutes_shortest
+ : com.android.internal.R.plurals.duration_minutes_shortest_future,
+ count),
+ count);
+ millisIncrease = MINUTE_IN_MILLIS;
+ } else if (duration < DAY_IN_MILLIS) {
+ count = (int)(duration / HOUR_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.R.plurals.duration_hours_shortest
+ : com.android.internal.R.plurals.duration_hours_shortest_future,
+ count),
+ count);
+ millisIncrease = HOUR_IN_MILLIS;
+ } else if (duration < YEAR_IN_MILLIS) {
+ // In weird cases it can become 0 because of daylight savings
+ TimeZone timeZone = TimeZone.getDefault();
+ count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.R.plurals.duration_days_shortest
+ : com.android.internal.R.plurals.duration_days_shortest_future,
+ count),
+ count);
+ if (past || count != 1) {
+ mUpdateTimeMillis = computeNextMidnight(timeZone);
+ millisIncrease = -1;
+ } else {
+ millisIncrease = DAY_IN_MILLIS;
+ }
+
+ } else {
+ count = (int)(duration / YEAR_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.R.plurals.duration_years_shortest
+ : com.android.internal.R.plurals.duration_years_shortest_future,
+ count),
+ count);
+ millisIncrease = YEAR_IN_MILLIS;
+ }
+ if (millisIncrease != -1) {
+ if (past) {
+ mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
+ } else {
+ mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
+ }
+ }
+ setText(result);
+ }
+
+ /**
+ * @param timeZone the timezone we are in
+ * @return the timepoint in millis at UTC at midnight in the current timezone
+ */
+ private long computeNextMidnight(TimeZone timeZone) {
+ Calendar c = Calendar.getInstance();
+ c.setTimeZone(libcore.icu.DateUtilsBridge.icuTimeZone(timeZone));
+ c.add(Calendar.DAY_OF_MONTH, 1);
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ return c.getTimeInMillis();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateNowText();
+ update();
+ }
+
+ private void updateNowText() {
+ if (!mShowRelativeTime) {
+ return;
+ }
+ mNowText = getContext().getResources().getString(
+ com.android.internal.R.string.now_string_shortest);
+ }
+
+ // Return the date difference for the two times in a given timezone.
+ private static int dayDistance(TimeZone timeZone, long startTime,
+ long endTime) {
+ return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000)
+ - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000);
+ }
+
+ private DateFormat getTimeFormat() {
+ return android.text.format.DateFormat.getTimeFormat(getContext());
+ }
+
+ void clearFormatAndUpdate() {
+ mLastFormat = null;
+ update();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (mShowRelativeTime) {
+ // The short version of the time might not be completely understandable and for
+ // accessibility we rather have a longer version.
+ long now = System.currentTimeMillis();
+ long duration = Math.abs(now - mTimeMillis);
+ int count;
+ boolean past = (now >= mTimeMillis);
+ String result;
+ if (duration < MINUTE_IN_MILLIS) {
+ result = mNowText;
+ } else if (duration < HOUR_IN_MILLIS) {
+ count = (int)(duration / MINUTE_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.
+ R.plurals.duration_minutes_relative
+ : com.android.internal.
+ R.plurals.duration_minutes_relative_future,
+ count),
+ count);
+ } else if (duration < DAY_IN_MILLIS) {
+ count = (int)(duration / HOUR_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.
+ R.plurals.duration_hours_relative
+ : com.android.internal.
+ R.plurals.duration_hours_relative_future,
+ count),
+ count);
+ } else if (duration < YEAR_IN_MILLIS) {
+ // In weird cases it can become 0 because of daylight savings
+ TimeZone timeZone = TimeZone.getDefault();
+ count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.
+ R.plurals.duration_days_relative
+ : com.android.internal.
+ R.plurals.duration_days_relative_future,
+ count),
+ count);
+
+ } else {
+ count = (int)(duration / YEAR_IN_MILLIS);
+ result = String.format(getContext().getResources().getQuantityString(past
+ ? com.android.internal.
+ R.plurals.duration_years_relative
+ : com.android.internal.
+ R.plurals.duration_years_relative_future,
+ count),
+ count);
+ }
+ info.setText(result);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static void setReceiverHandler(Handler handler) {
+ ReceiverInfo ri = sReceiverInfo.get();
+ if (ri == null) {
+ ri = new ReceiverInfo();
+ sReceiverInfo.set(ri);
+ }
+ ri.setHandler(handler);
+ }
+
+ private static class ReceiverInfo {
+ private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_TIME_TICK.equals(action)) {
+ if (System.currentTimeMillis() < getSoonestUpdateTime()) {
+ // The update() function takes a few milliseconds to run because of
+ // all of the time conversions it needs to do, so we can't do that
+ // every minute.
+ return;
+ }
+ }
+ // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
+ updateAll();
+ }
+ };
+
+ private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateAll();
+ }
+ };
+
+ private Handler mHandler = new Handler();
+
+ public void addView(DateTimeView v) {
+ synchronized (mAttachedViews) {
+ final boolean register = mAttachedViews.isEmpty();
+ mAttachedViews.add(v);
+ if (register) {
+ register(getApplicationContextIfAvailable(v.getContext()));
+ }
+ }
+ }
+
+ public void removeView(DateTimeView v) {
+ synchronized (mAttachedViews) {
+ mAttachedViews.remove(v);
+ if (mAttachedViews.isEmpty()) {
+ unregister(getApplicationContextIfAvailable(v.getContext()));
+ }
+ }
+ }
+
+ void updateAll() {
+ synchronized (mAttachedViews) {
+ final int count = mAttachedViews.size();
+ for (int i = 0; i < count; i++) {
+ DateTimeView view = mAttachedViews.get(i);
+ view.post(() -> view.clearFormatAndUpdate());
+ }
+ }
+ }
+
+ long getSoonestUpdateTime() {
+ long result = Long.MAX_VALUE;
+ synchronized (mAttachedViews) {
+ final int count = mAttachedViews.size();
+ for (int i = 0; i < count; i++) {
+ final long time = mAttachedViews.get(i).mUpdateTimeMillis;
+ if (time < result) {
+ result = time;
+ }
+ }
+ }
+ return result;
+ }
+
+ static final Context getApplicationContextIfAvailable(Context context) {
+ final Context ac = context.getApplicationContext();
+ return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
+ }
+
+ void register(Context context) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ context.registerReceiver(mReceiver, filter, null, mHandler);
+ }
+
+ void unregister(Context context) {
+ context.unregisterReceiver(mReceiver);
+ }
+
+ public void setHandler(Handler handler) {
+ mHandler = handler;
+ synchronized (mAttachedViews) {
+ if (!mAttachedViews.isEmpty()) {
+ unregister(mAttachedViews.get(0).getContext());
+ register(mAttachedViews.get(0).getContext());
+ }
+ }
+ }
+ }
+}
diff --git a/android/widget/DayPickerPagerAdapter.java b/android/widget/DayPickerPagerAdapter.java
new file mode 100644
index 0000000..63621e1
--- /dev/null
+++ b/android/widget/DayPickerPagerAdapter.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2015 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.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SimpleMonthView.OnDayClickListener;
+
+import com.android.internal.widget.PagerAdapter;
+
+/**
+ * An adapter for a list of {@link android.widget.SimpleMonthView} items.
+ */
+class DayPickerPagerAdapter extends PagerAdapter {
+ private static final int MONTHS_IN_YEAR = 12;
+
+ private final Calendar mMinDate = Calendar.getInstance();
+ private final Calendar mMaxDate = Calendar.getInstance();
+
+ private final SparseArray<ViewHolder> mItems = new SparseArray<>();
+
+ private final LayoutInflater mInflater;
+ private final int mLayoutResId;
+ private final int mCalendarViewId;
+
+ private Calendar mSelectedDay = null;
+
+ private int mMonthTextAppearance;
+ private int mDayOfWeekTextAppearance;
+ private int mDayTextAppearance;
+
+ private ColorStateList mCalendarTextColor;
+ private ColorStateList mDaySelectorColor;
+ private ColorStateList mDayHighlightColor;
+
+ private OnDaySelectedListener mOnDaySelectedListener;
+
+ private int mCount;
+ private int mFirstDayOfWeek;
+
+ public DayPickerPagerAdapter(@NonNull Context context, @LayoutRes int layoutResId,
+ @IdRes int calendarViewId) {
+ mInflater = LayoutInflater.from(context);
+ mLayoutResId = layoutResId;
+ mCalendarViewId = calendarViewId;
+
+ final TypedArray ta = context.obtainStyledAttributes(new int[] {
+ com.android.internal.R.attr.colorControlHighlight});
+ mDayHighlightColor = ta.getColorStateList(0);
+ ta.recycle();
+ }
+
+ public void setRange(@NonNull Calendar min, @NonNull Calendar max) {
+ mMinDate.setTimeInMillis(min.getTimeInMillis());
+ mMaxDate.setTimeInMillis(max.getTimeInMillis());
+
+ final int diffYear = mMaxDate.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR);
+ final int diffMonth = mMaxDate.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH);
+ mCount = diffMonth + MONTHS_IN_YEAR * diffYear + 1;
+
+ // Positions are now invalid, clear everything and start over.
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Sets the first day of the week.
+ *
+ * @param weekStart which day the week should start on, valid values are
+ * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+ */
+ public void setFirstDayOfWeek(int weekStart) {
+ mFirstDayOfWeek = weekStart;
+
+ // Update displayed views.
+ final int count = mItems.size();
+ for (int i = 0; i < count; i++) {
+ final SimpleMonthView monthView = mItems.valueAt(i).calendar;
+ monthView.setFirstDayOfWeek(weekStart);
+ }
+ }
+
+ public int getFirstDayOfWeek() {
+ return mFirstDayOfWeek;
+ }
+
+ public boolean getBoundsForDate(Calendar day, Rect outBounds) {
+ final int position = getPositionForDay(day);
+ final ViewHolder monthView = mItems.get(position, null);
+ if (monthView == null) {
+ return false;
+ } else {
+ final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+ return monthView.calendar.getBoundsForDay(dayOfMonth, outBounds);
+ }
+ }
+
+ /**
+ * Sets the selected day.
+ *
+ * @param day the selected day
+ */
+ public void setSelectedDay(@Nullable Calendar day) {
+ final int oldPosition = getPositionForDay(mSelectedDay);
+ final int newPosition = getPositionForDay(day);
+
+ // Clear the old position if necessary.
+ if (oldPosition != newPosition && oldPosition >= 0) {
+ final ViewHolder oldMonthView = mItems.get(oldPosition, null);
+ if (oldMonthView != null) {
+ oldMonthView.calendar.setSelectedDay(-1);
+ }
+ }
+
+ // Set the new position.
+ if (newPosition >= 0) {
+ final ViewHolder newMonthView = mItems.get(newPosition, null);
+ if (newMonthView != null) {
+ final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+ newMonthView.calendar.setSelectedDay(dayOfMonth);
+ }
+ }
+
+ mSelectedDay = day;
+ }
+
+ /**
+ * Sets the listener to call when the user selects a day.
+ *
+ * @param listener The listener to call.
+ */
+ public void setOnDaySelectedListener(OnDaySelectedListener listener) {
+ mOnDaySelectedListener = listener;
+ }
+
+ void setCalendarTextColor(ColorStateList calendarTextColor) {
+ mCalendarTextColor = calendarTextColor;
+ notifyDataSetChanged();
+ }
+
+ void setDaySelectorColor(ColorStateList selectorColor) {
+ mDaySelectorColor = selectorColor;
+ notifyDataSetChanged();
+ }
+
+ void setMonthTextAppearance(int resId) {
+ mMonthTextAppearance = resId;
+ notifyDataSetChanged();
+ }
+
+ void setDayOfWeekTextAppearance(int resId) {
+ mDayOfWeekTextAppearance = resId;
+ notifyDataSetChanged();
+ }
+
+ int getDayOfWeekTextAppearance() {
+ return mDayOfWeekTextAppearance;
+ }
+
+ void setDayTextAppearance(int resId) {
+ mDayTextAppearance = resId;
+ notifyDataSetChanged();
+ }
+
+ int getDayTextAppearance() {
+ return mDayTextAppearance;
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ final ViewHolder holder = (ViewHolder) object;
+ return view == holder.container;
+ }
+
+ private int getMonthForPosition(int position) {
+ return (position + mMinDate.get(Calendar.MONTH)) % MONTHS_IN_YEAR;
+ }
+
+ private int getYearForPosition(int position) {
+ final int yearOffset = (position + mMinDate.get(Calendar.MONTH)) / MONTHS_IN_YEAR;
+ return yearOffset + mMinDate.get(Calendar.YEAR);
+ }
+
+ private int getPositionForDay(@Nullable Calendar day) {
+ if (day == null) {
+ return -1;
+ }
+
+ final int yearOffset = day.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR);
+ final int monthOffset = day.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH);
+ final int position = yearOffset * MONTHS_IN_YEAR + monthOffset;
+ return position;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ final View itemView = mInflater.inflate(mLayoutResId, container, false);
+
+ final SimpleMonthView v = itemView.findViewById(mCalendarViewId);
+ v.setOnDayClickListener(mOnDayClickListener);
+ v.setMonthTextAppearance(mMonthTextAppearance);
+ v.setDayOfWeekTextAppearance(mDayOfWeekTextAppearance);
+ v.setDayTextAppearance(mDayTextAppearance);
+
+ if (mDaySelectorColor != null) {
+ v.setDaySelectorColor(mDaySelectorColor);
+ }
+
+ if (mDayHighlightColor != null) {
+ v.setDayHighlightColor(mDayHighlightColor);
+ }
+
+ if (mCalendarTextColor != null) {
+ v.setMonthTextColor(mCalendarTextColor);
+ v.setDayOfWeekTextColor(mCalendarTextColor);
+ v.setDayTextColor(mCalendarTextColor);
+ }
+
+ final int month = getMonthForPosition(position);
+ final int year = getYearForPosition(position);
+
+ final int selectedDay;
+ if (mSelectedDay != null && mSelectedDay.get(Calendar.MONTH) == month) {
+ selectedDay = mSelectedDay.get(Calendar.DAY_OF_MONTH);
+ } else {
+ selectedDay = -1;
+ }
+
+ final int enabledDayRangeStart;
+ if (mMinDate.get(Calendar.MONTH) == month && mMinDate.get(Calendar.YEAR) == year) {
+ enabledDayRangeStart = mMinDate.get(Calendar.DAY_OF_MONTH);
+ } else {
+ enabledDayRangeStart = 1;
+ }
+
+ final int enabledDayRangeEnd;
+ if (mMaxDate.get(Calendar.MONTH) == month && mMaxDate.get(Calendar.YEAR) == year) {
+ enabledDayRangeEnd = mMaxDate.get(Calendar.DAY_OF_MONTH);
+ } else {
+ enabledDayRangeEnd = 31;
+ }
+
+ v.setMonthParams(selectedDay, month, year, mFirstDayOfWeek,
+ enabledDayRangeStart, enabledDayRangeEnd);
+
+ final ViewHolder holder = new ViewHolder(position, itemView, v);
+ mItems.put(position, holder);
+
+ container.addView(itemView);
+
+ return holder;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ final ViewHolder holder = (ViewHolder) object;
+ container.removeView(holder.container);
+
+ mItems.remove(position);
+ }
+
+ @Override
+ public int getItemPosition(Object object) {
+ final ViewHolder holder = (ViewHolder) object;
+ return holder.position;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ final SimpleMonthView v = mItems.get(position).calendar;
+ if (v != null) {
+ return v.getMonthYearLabel();
+ }
+ return null;
+ }
+
+ SimpleMonthView getView(Object object) {
+ if (object == null) {
+ return null;
+ }
+ final ViewHolder holder = (ViewHolder) object;
+ return holder.calendar;
+ }
+
+ private final OnDayClickListener mOnDayClickListener = new OnDayClickListener() {
+ @Override
+ public void onDayClick(SimpleMonthView view, Calendar day) {
+ if (day != null) {
+ setSelectedDay(day);
+
+ if (mOnDaySelectedListener != null) {
+ mOnDaySelectedListener.onDaySelected(DayPickerPagerAdapter.this, day);
+ }
+ }
+ }
+ };
+
+ private static class ViewHolder {
+ public final int position;
+ public final View container;
+ public final SimpleMonthView calendar;
+
+ public ViewHolder(int position, View container, SimpleMonthView calendar) {
+ this.position = position;
+ this.container = container;
+ this.calendar = calendar;
+ }
+ }
+
+ public interface OnDaySelectedListener {
+ public void onDaySelected(DayPickerPagerAdapter view, Calendar day);
+ }
+}
diff --git a/android/widget/DayPickerView.java b/android/widget/DayPickerView.java
new file mode 100644
index 0000000..f712d5f
--- /dev/null
+++ b/android/widget/DayPickerView.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2015 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.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+import com.android.internal.widget.ViewPager;
+import com.android.internal.widget.ViewPager.OnPageChangeListener;
+
+import libcore.icu.LocaleData;
+
+import java.util.Locale;
+
+class DayPickerView extends ViewGroup {
+ private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material;
+ private static final int DEFAULT_START_YEAR = 1900;
+ private static final int DEFAULT_END_YEAR = 2100;
+
+ private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor };
+
+ private final Calendar mSelectedDay = Calendar.getInstance();
+ private final Calendar mMinDate = Calendar.getInstance();
+ private final Calendar mMaxDate = Calendar.getInstance();
+
+ private final AccessibilityManager mAccessibilityManager;
+
+ private final ViewPager mViewPager;
+ private final ImageButton mPrevButton;
+ private final ImageButton mNextButton;
+
+ private final DayPickerPagerAdapter mAdapter;
+
+ /** Temporary calendar used for date calculations. */
+ private Calendar mTempCalendar;
+
+ private OnDaySelectedListener mOnDaySelectedListener;
+
+ public DayPickerView(Context context) {
+ this(context, null);
+ }
+
+ public DayPickerView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, R.attr.calendarViewStyle);
+ }
+
+ public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CalendarView, defStyleAttr, defStyleRes);
+
+ final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
+ LocaleData.get(Locale.getDefault()).firstDayOfWeek);
+
+ final String minDate = a.getString(R.styleable.CalendarView_minDate);
+ final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
+
+ final int monthTextAppearanceResId = a.getResourceId(
+ R.styleable.CalendarView_monthTextAppearance,
+ R.style.TextAppearance_Material_Widget_Calendar_Month);
+ final int dayOfWeekTextAppearanceResId = a.getResourceId(
+ R.styleable.CalendarView_weekDayTextAppearance,
+ R.style.TextAppearance_Material_Widget_Calendar_DayOfWeek);
+ final int dayTextAppearanceResId = a.getResourceId(
+ R.styleable.CalendarView_dateTextAppearance,
+ R.style.TextAppearance_Material_Widget_Calendar_Day);
+
+ final ColorStateList daySelectorColor = a.getColorStateList(
+ R.styleable.CalendarView_daySelectorColor);
+
+ a.recycle();
+
+ // Set up adapter.
+ mAdapter = new DayPickerPagerAdapter(context,
+ R.layout.date_picker_month_item_material, R.id.month_view);
+ mAdapter.setMonthTextAppearance(monthTextAppearanceResId);
+ mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId);
+ mAdapter.setDayTextAppearance(dayTextAppearanceResId);
+ mAdapter.setDaySelectorColor(daySelectorColor);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false);
+
+ // Transfer all children from content to here.
+ while (content.getChildCount() > 0) {
+ final View child = content.getChildAt(0);
+ content.removeViewAt(0);
+ addView(child);
+ }
+
+ mPrevButton = findViewById(R.id.prev);
+ mPrevButton.setOnClickListener(mOnClickListener);
+
+ mNextButton = findViewById(R.id.next);
+ mNextButton.setOnClickListener(mOnClickListener);
+
+ mViewPager = findViewById(R.id.day_picker_view_pager);
+ mViewPager.setAdapter(mAdapter);
+ mViewPager.setOnPageChangeListener(mOnPageChangedListener);
+
+ // Proxy the month text color into the previous and next buttons.
+ if (monthTextAppearanceResId != 0) {
+ final TypedArray ta = mContext.obtainStyledAttributes(null,
+ ATTRS_TEXT_COLOR, 0, monthTextAppearanceResId);
+ final ColorStateList monthColor = ta.getColorStateList(0);
+ if (monthColor != null) {
+ mPrevButton.setImageTintList(monthColor);
+ mNextButton.setImageTintList(monthColor);
+ }
+ ta.recycle();
+ }
+
+ // Set up min and max dates.
+ final Calendar tempDate = Calendar.getInstance();
+ if (!CalendarView.parseDate(minDate, tempDate)) {
+ tempDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
+ }
+ final long minDateMillis = tempDate.getTimeInMillis();
+
+ if (!CalendarView.parseDate(maxDate, tempDate)) {
+ tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
+ }
+ final long maxDateMillis = tempDate.getTimeInMillis();
+
+ if (maxDateMillis < minDateMillis) {
+ throw new IllegalArgumentException("maxDate must be >= minDate");
+ }
+
+ final long setDateMillis = MathUtils.constrain(
+ System.currentTimeMillis(), minDateMillis, maxDateMillis);
+
+ setFirstDayOfWeek(firstDayOfWeek);
+ setMinDate(minDateMillis);
+ setMaxDate(maxDateMillis);
+ setDate(setDateMillis, false);
+
+ // Proxy selection callbacks to our own listener.
+ mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() {
+ @Override
+ public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) {
+ if (mOnDaySelectedListener != null) {
+ mOnDaySelectedListener.onDaySelected(DayPickerView.this, day);
+ }
+ }
+ });
+ }
+
+ private void updateButtonVisibility(int position) {
+ final boolean hasPrev = position > 0;
+ final boolean hasNext = position < (mAdapter.getCount() - 1);
+ mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE);
+ mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final ViewPager viewPager = mViewPager;
+ measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
+
+ final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
+ final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
+ setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
+
+ final int pagerWidth = viewPager.getMeasuredWidth();
+ final int pagerHeight = viewPager.getMeasuredHeight();
+ final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
+ final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
+ mPrevButton.measure(buttonWidthSpec, buttonHeightSpec);
+ mNextButton.measure(buttonWidthSpec, buttonHeightSpec);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ requestLayout();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final ImageButton leftButton;
+ final ImageButton rightButton;
+ if (isLayoutRtl()) {
+ leftButton = mNextButton;
+ rightButton = mPrevButton;
+ } else {
+ leftButton = mPrevButton;
+ rightButton = mNextButton;
+ }
+
+ final int width = right - left;
+ final int height = bottom - top;
+ mViewPager.layout(0, 0, width, height);
+
+ final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
+ final int monthHeight = monthView.getMonthHeight();
+ final int cellWidth = monthView.getCellWidth();
+
+ // Vertically center the previous/next buttons within the month
+ // header, horizontally center within the day cell.
+ final int leftDW = leftButton.getMeasuredWidth();
+ final int leftDH = leftButton.getMeasuredHeight();
+ final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
+ final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2;
+ leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
+
+ final int rightDW = rightButton.getMeasuredWidth();
+ final int rightDH = rightButton.getMeasuredHeight();
+ final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
+ final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2;
+ rightButton.layout(rightIconRight - rightDW, rightIconTop,
+ rightIconRight, rightIconTop + rightDH);
+ }
+
+ public void setDayOfWeekTextAppearance(int resId) {
+ mAdapter.setDayOfWeekTextAppearance(resId);
+ }
+
+ public int getDayOfWeekTextAppearance() {
+ return mAdapter.getDayOfWeekTextAppearance();
+ }
+
+ public void setDayTextAppearance(int resId) {
+ mAdapter.setDayTextAppearance(resId);
+ }
+
+ public int getDayTextAppearance() {
+ return mAdapter.getDayTextAppearance();
+ }
+
+ /**
+ * Sets the currently selected date to the specified timestamp. Jumps
+ * immediately to the new date. To animate to the new date, use
+ * {@link #setDate(long, boolean)}.
+ *
+ * @param timeInMillis the target day in milliseconds
+ */
+ public void setDate(long timeInMillis) {
+ setDate(timeInMillis, false);
+ }
+
+ /**
+ * Sets the currently selected date to the specified timestamp. Jumps
+ * immediately to the new date, optionally animating the transition.
+ *
+ * @param timeInMillis the target day in milliseconds
+ * @param animate whether to smooth scroll to the new position
+ */
+ public void setDate(long timeInMillis, boolean animate) {
+ setDate(timeInMillis, animate, true);
+ }
+
+ /**
+ * Moves to the month containing the specified day, optionally setting the
+ * day as selected.
+ *
+ * @param timeInMillis the target day in milliseconds
+ * @param animate whether to smooth scroll to the new position
+ * @param setSelected whether to set the specified day as selected
+ */
+ private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
+ boolean dateClamped = false;
+ // Clamp the target day in milliseconds to the min or max if outside the range.
+ if (timeInMillis < mMinDate.getTimeInMillis()) {
+ timeInMillis = mMinDate.getTimeInMillis();
+ dateClamped = true;
+ } else if (timeInMillis > mMaxDate.getTimeInMillis()) {
+ timeInMillis = mMaxDate.getTimeInMillis();
+ dateClamped = true;
+ }
+
+ getTempCalendarForTime(timeInMillis);
+
+ if (setSelected || dateClamped) {
+ mSelectedDay.setTimeInMillis(timeInMillis);
+ }
+
+ final int position = getPositionFromDay(timeInMillis);
+ if (position != mViewPager.getCurrentItem()) {
+ mViewPager.setCurrentItem(position, animate);
+ }
+
+ mAdapter.setSelectedDay(mTempCalendar);
+ }
+
+ public long getDate() {
+ return mSelectedDay.getTimeInMillis();
+ }
+
+ public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
+ final int position = getPositionFromDay(timeInMillis);
+ if (position != mViewPager.getCurrentItem()) {
+ return false;
+ }
+
+ mTempCalendar.setTimeInMillis(timeInMillis);
+ return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
+ }
+
+ public void setFirstDayOfWeek(int firstDayOfWeek) {
+ mAdapter.setFirstDayOfWeek(firstDayOfWeek);
+ }
+
+ public int getFirstDayOfWeek() {
+ return mAdapter.getFirstDayOfWeek();
+ }
+
+ public void setMinDate(long timeInMillis) {
+ mMinDate.setTimeInMillis(timeInMillis);
+ onRangeChanged();
+ }
+
+ public long getMinDate() {
+ return mMinDate.getTimeInMillis();
+ }
+
+ public void setMaxDate(long timeInMillis) {
+ mMaxDate.setTimeInMillis(timeInMillis);
+ onRangeChanged();
+ }
+
+ public long getMaxDate() {
+ return mMaxDate.getTimeInMillis();
+ }
+
+ /**
+ * Handles changes to date range.
+ */
+ public void onRangeChanged() {
+ mAdapter.setRange(mMinDate, mMaxDate);
+
+ // Changing the min/max date changes the selection position since we
+ // don't really have stable IDs. Jumps immediately to the new position.
+ setDate(mSelectedDay.getTimeInMillis(), false, false);
+
+ updateButtonVisibility(mViewPager.getCurrentItem());
+ }
+
+ /**
+ * Sets the listener to call when the user selects a day.
+ *
+ * @param listener The listener to call.
+ */
+ public void setOnDaySelectedListener(OnDaySelectedListener listener) {
+ mOnDaySelectedListener = listener;
+ }
+
+ private int getDiffMonths(Calendar start, Calendar end) {
+ final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
+ return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
+ }
+
+ private int getPositionFromDay(long timeInMillis) {
+ final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
+ final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
+ return MathUtils.constrain(diffMonth, 0, diffMonthMax);
+ }
+
+ private Calendar getTempCalendarForTime(long timeInMillis) {
+ if (mTempCalendar == null) {
+ mTempCalendar = Calendar.getInstance();
+ }
+ mTempCalendar.setTimeInMillis(timeInMillis);
+ return mTempCalendar;
+ }
+
+ /**
+ * Gets the position of the view that is most prominently displayed within the list view.
+ */
+ public int getMostVisiblePosition() {
+ return mViewPager.getCurrentItem();
+ }
+
+ public void setPosition(int position) {
+ mViewPager.setCurrentItem(position, false);
+ }
+
+ private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
+ mPrevButton.setAlpha(alpha);
+ mNextButton.setAlpha(alpha);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ @Override
+ public void onPageSelected(int position) {
+ updateButtonVisibility(position);
+ }
+ };
+
+ private final OnClickListener mOnClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final int direction;
+ if (v == mPrevButton) {
+ direction = -1;
+ } else if (v == mNextButton) {
+ direction = 1;
+ } else {
+ return;
+ }
+
+ // Animation is expensive for accessibility services since it sends
+ // lots of scroll and content change events.
+ final boolean animate = !mAccessibilityManager.isEnabled();
+
+ // ViewPager clamps input values, so we don't need to worry
+ // about passing invalid indices.
+ final int nextItem = mViewPager.getCurrentItem() + direction;
+ mViewPager.setCurrentItem(nextItem, animate);
+ }
+ };
+
+ public interface OnDaySelectedListener {
+ void onDaySelected(DayPickerView view, Calendar day);
+ }
+}
diff --git a/android/widget/DayPickerViewPager.java b/android/widget/DayPickerViewPager.java
new file mode 100644
index 0000000..1704ed7
--- /dev/null
+++ b/android/widget/DayPickerViewPager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 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.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.internal.widget.ViewPager;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+/**
+ * This displays a list of months in a calendar format with selectable days.
+ */
+class DayPickerViewPager extends ViewPager {
+ private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+ public DayPickerViewPager(Context context) {
+ this(context, null);
+ }
+
+ public DayPickerViewPager(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DayPickerViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public DayPickerViewPager(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ populate();
+
+ // Everything below is mostly copied from FrameLayout.
+ int count = getChildCount();
+
+ final boolean measureMatchParentChildren =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+ MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ if (measureMatchParentChildren) {
+ if (lp.width == LayoutParams.MATCH_PARENT ||
+ lp.height == LayoutParams.MATCH_PARENT) {
+ mMatchParentChildren.add(child);
+ }
+ }
+ }
+ }
+
+ // Account for padding too
+ maxWidth += getPaddingLeft() + getPaddingRight();
+ maxHeight += getPaddingTop() + getPaddingBottom();
+
+ // 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(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ count = mMatchParentChildren.size();
+ if (count > 1) {
+ for (int i = 0; i < count; i++) {
+ final View child = mMatchParentChildren.get(i);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childWidthMeasureSpec;
+ final int childHeightMeasureSpec;
+
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeft() + getPaddingRight(),
+ lp.width);
+ }
+
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTop() + getPaddingBottom(),
+ lp.height);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+
+ mMatchParentChildren.clear();
+ }
+
+ @Override
+ protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
+ View childToSkip) {
+ if (predicate.test(this)) {
+ return (T) this;
+ }
+
+ // Always try the selected view first.
+ final DayPickerPagerAdapter adapter = (DayPickerPagerAdapter) getAdapter();
+ final SimpleMonthView current = adapter.getView(getCurrent());
+ if (current != childToSkip && current != null) {
+ final View v = current.findViewByPredicate(predicate);
+ if (v != null) {
+ return (T) v;
+ }
+ }
+
+ final int len = getChildCount();
+ for (int i = 0; i < len; i++) {
+ final View child = getChildAt(i);
+
+ if (child != childToSkip && child != current) {
+ final View v = child.findViewByPredicate(predicate);
+
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/android/widget/DialerFilter.java b/android/widget/DialerFilter.java
new file mode 100644
index 0000000..8f9c96c
--- /dev/null
+++ b/android/widget/DialerFilter.java
@@ -0,0 +1,422 @@
+/*
+ * 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.Rect;
+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.view.KeyEvent;
+import android.view.View;
+
+/**
+ * This widget is a layout that contains several specifically-named child views that
+ * handle keyboard entry interpreted as standard phone dialpad digits.
+ *
+ * @deprecated Use a custom view or layout to handle this functionality instead
+ */
+@Deprecated
+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);
+
+ // 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/android/widget/DigitalClock.java b/android/widget/DigitalClock.java
new file mode 100644
index 0000000..c503ef2
--- /dev/null
+++ b/android/widget/DigitalClock.java
@@ -0,0 +1,123 @@
+/*
+ * 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.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+
+import java.util.Calendar;
+
+/**
+ * Like AnalogClock, but digital. Shows seconds.
+ *
+ * @deprecated It is recommended you use {@link TextClock} instead.
+ */
+@Deprecated
+public class DigitalClock extends TextView {
+ // FIXME: implement separate views for hours/minutes/seconds, so
+ // proportional fonts don't shake rendering
+
+ Calendar mCalendar;
+ @SuppressWarnings("FieldCanBeLocal") // We must keep a reference to this observer
+ private FormatChangeObserver mFormatChangeObserver;
+
+ private Runnable mTicker;
+ private Handler mHandler;
+
+ private boolean mTickerStopped = false;
+
+ String mFormat;
+
+ public DigitalClock(Context context) {
+ super(context);
+ initClock();
+ }
+
+ public DigitalClock(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initClock();
+ }
+
+ private void initClock() {
+ if (mCalendar == null) {
+ mCalendar = Calendar.getInstance();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mTickerStopped = false;
+ super.onAttachedToWindow();
+
+ mFormatChangeObserver = new FormatChangeObserver();
+ getContext().getContentResolver().registerContentObserver(
+ Settings.System.CONTENT_URI, true, mFormatChangeObserver);
+ setFormat();
+
+ 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;
+ getContext().getContentResolver().unregisterContentObserver(
+ mFormatChangeObserver);
+ }
+
+ private void setFormat() {
+ mFormat = DateFormat.getTimeFormatString(getContext());
+ }
+
+ private class FormatChangeObserver extends ContentObserver {
+ public FormatChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ setFormat();
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ //noinspection deprecation
+ return DigitalClock.class.getName();
+ }
+}
diff --git a/android/widget/DoubleDigitManager.java b/android/widget/DoubleDigitManager.java
new file mode 100644
index 0000000..1eea1fb
--- /dev/null
+++ b/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/android/widget/DropDownListView.java b/android/widget/DropDownListView.java
new file mode 100644
index 0000000..e9c4728
--- /dev/null
+++ b/android/widget/DropDownListView.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2015 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.NonNull;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
+
+/**
+ * Wrapper class for a ListView. This wrapper can hijack 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 in this mode; the list only looks focused.
+ *
+ * @hide
+ */
+public class DropDownListView extends ListView {
+ /*
+ * WARNING: This is a workaround for a touch mode issue.
+ *
+ * Touch mode is propagated lazily to windows. This causes problems in
+ * the following scenario:
+ * - Type something in the AutoCompleteTextView and get some results
+ * - Move down with the d-pad to select an item in the list
+ * - Move up with the d-pad until the selection disappears
+ * - Type more text in the AutoCompleteTextView *using the soft keyboard*
+ * and get new results; you are now in touch mode
+ * - The selection comes back on the first item in the list, even though
+ * the list is supposed to be in touch mode
+ *
+ * Using the soft keyboard triggers the touch mode change but that change
+ * is propagated to our window only after the first list layout, therefore
+ * after the list attempts to resurrect the selection.
+ *
+ * The trick to work around this issue is to pretend the list is in touch
+ * mode when we know that the selection should not appear, that is when
+ * we know the user moved the selection away from the list.
+ *
+ * This boolean is set to true whenever we explicitly hide the list's
+ * selection and reset to false whenever we know the user moved the
+ * selection back to the list.
+ *
+ * When this boolean is true, isInTouchMode() returns true, otherwise it
+ * returns super.isInTouchMode().
+ */
+ private boolean mListSelectionHidden;
+
+ /**
+ * True if this wrapper should fake focus.
+ */
+ private boolean mHijackFocus;
+
+ /** Whether to force drawing of the pressed state selector. */
+ private boolean mDrawsInPressedState;
+
+ /** Helper for drag-to-open auto scrolling. */
+ private AbsListViewAutoScroller mScrollHelper;
+
+ /**
+ * Runnable posted when we are awaiting hover event resolution. When set,
+ * drawable state changes are postponed.
+ */
+ private ResolveHoverRunnable mResolveHoverRunnable;
+
+ /**
+ * Creates a new list view wrapper.
+ *
+ * @param context this view's context
+ */
+ public DropDownListView(@NonNull Context context, boolean hijackFocus) {
+ this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
+ }
+
+ /**
+ * Creates a new list view wrapper.
+ *
+ * @param context this view's context
+ */
+ public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
+ super(context, null, defStyleAttr);
+ mHijackFocus = hijackFocus;
+ // TODO: Add an API to control this
+ setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+ }
+
+ @Override
+ boolean shouldShowSelector() {
+ return isHovered() || super.shouldShowSelector();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mResolveHoverRunnable != null) {
+ // Resolved hover event as hover => touch transition.
+ mResolveHoverRunnable.cancel();
+ }
+
+ return super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onHoverEvent(@NonNull MotionEvent ev) {
+ final int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) {
+ // This may be transitioning to TOUCH_DOWN. Postpone drawable state
+ // updates until either the next frame or the next touch event.
+ mResolveHoverRunnable = new ResolveHoverRunnable();
+ mResolveHoverRunnable.post();
+ }
+
+ // Allow the super class to handle hover state management first.
+ final boolean handled = super.onHoverEvent(ev);
+
+ if (action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_MOVE) {
+ final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
+ if (position != INVALID_POSITION && position != mSelectedPosition) {
+ final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
+ if (hoveredItem.isEnabled()) {
+ // Force a focus so that the proper selector state gets
+ // used when we update.
+ requestFocus();
+
+ positionSelector(position, hoveredItem);
+ setSelectedPositionInt(position);
+ setNextSelectedPositionInt(position);
+ }
+ updateSelectorState();
+ }
+ } else {
+ // Do not cancel the selected position if the selection is visible
+ // by other means.
+ if (!super.shouldShowSelector()) {
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ }
+ }
+
+ return handled;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ if (mResolveHoverRunnable == null) {
+ super.drawableStateChanged();
+ }
+ }
+
+ /**
+ * Handles forwarded events.
+ *
+ * @param activePointerId id of the pointer that activated forwarding
+ * @return whether the event was handled
+ */
+ public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
+ boolean handledEvent = true;
+ boolean clearPressedItem = false;
+
+ final int actionMasked = event.getActionMasked();
+ switch (actionMasked) {
+ case MotionEvent.ACTION_CANCEL:
+ handledEvent = false;
+ break;
+ case MotionEvent.ACTION_UP:
+ handledEvent = false;
+ // $FALL-THROUGH$
+ case MotionEvent.ACTION_MOVE:
+ final int activeIndex = event.findPointerIndex(activePointerId);
+ if (activeIndex < 0) {
+ handledEvent = false;
+ break;
+ }
+
+ final int x = (int) event.getX(activeIndex);
+ final int y = (int) event.getY(activeIndex);
+ final int position = pointToPosition(x, y);
+ if (position == INVALID_POSITION) {
+ clearPressedItem = true;
+ break;
+ }
+
+ final View child = getChildAt(position - getFirstVisiblePosition());
+ setPressedItem(child, position, x, y);
+ handledEvent = true;
+
+ if (actionMasked == MotionEvent.ACTION_UP) {
+ final long id = getItemIdAtPosition(position);
+ performItemClick(child, position, id);
+ }
+ break;
+ }
+
+ // Failure to handle the event cancels forwarding.
+ if (!handledEvent || clearPressedItem) {
+ clearPressedItem();
+ }
+
+ // Manage automatic scrolling.
+ if (handledEvent) {
+ if (mScrollHelper == null) {
+ mScrollHelper = new AbsListViewAutoScroller(this);
+ }
+ mScrollHelper.setEnabled(true);
+ mScrollHelper.onTouch(this, event);
+ } else if (mScrollHelper != null) {
+ mScrollHelper.setEnabled(false);
+ }
+
+ return handledEvent;
+ }
+
+ /**
+ * Sets whether the list selection is hidden, as part of a workaround for a
+ * touch mode issue (see the declaration for mListSelectionHidden).
+ *
+ * @param hideListSelection {@code true} to hide list selection,
+ * {@code false} to show
+ */
+ public void setListSelectionHidden(boolean hideListSelection) {
+ mListSelectionHidden = hideListSelection;
+ }
+
+ private void clearPressedItem() {
+ mDrawsInPressedState = false;
+ setPressed(false);
+ updateSelectorState();
+
+ final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ }
+
+ private void setPressedItem(@NonNull View child, int position, float x, float y) {
+ mDrawsInPressedState = true;
+
+ // Ordering is essential. First, update the container's pressed state.
+ drawableHotspotChanged(x, y);
+ if (!isPressed()) {
+ setPressed(true);
+ }
+
+ // Next, run layout if we need to stabilize child positions.
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ // Manage the pressed view based on motion position. This allows us to
+ // play nicely with actual touch and scroll events.
+ final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null && motionView != child && motionView.isPressed()) {
+ motionView.setPressed(false);
+ }
+ mMotionPosition = position;
+
+ // Offset for child coordinates.
+ final float childX = x - child.getLeft();
+ final float childY = y - child.getTop();
+ child.drawableHotspotChanged(childX, childY);
+ if (!child.isPressed()) {
+ child.setPressed(true);
+ }
+
+ // Ensure that keyboard focus starts from the last touched position.
+ setSelectedPositionInt(position);
+ positionSelectorLikeTouch(position, child, x, y);
+
+ // Refresh the drawable state to reflect the new pressed state,
+ // which will also update the selector state.
+ refreshDrawableState();
+ }
+
+ @Override
+ boolean touchModeDrawsInPressedState() {
+ return mDrawsInPressedState || super.touchModeDrawsInPressedState();
+ }
+
+ /**
+ * Avoids jarring scrolling effect by ensuring that list elements
+ * made of a text view fit on a single line.
+ *
+ * @param position the item index in the list to get a view for
+ * @return the view for the specified item
+ */
+ @Override
+ View obtainView(int position, boolean[] isScrap) {
+ View view = super.obtainView(position, isScrap);
+
+ if (view instanceof TextView) {
+ ((TextView) view).setHorizontallyScrolling(true);
+ }
+
+ return view;
+ }
+
+ @Override
+ public boolean isInTouchMode() {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return mHijackFocus || super.hasWindowFocus();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean isFocused() {
+ return mHijackFocus || super.isFocused();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return mHijackFocus || super.hasFocus();
+ }
+
+ /**
+ * Runnable that forces hover event resolution and updates drawable state.
+ */
+ private class ResolveHoverRunnable implements Runnable {
+ @Override
+ public void run() {
+ // Resolved hover event as standard hover exit.
+ mResolveHoverRunnable = null;
+ drawableStateChanged();
+ }
+
+ public void cancel() {
+ mResolveHoverRunnable = null;
+ removeCallbacks(this);
+ }
+
+ public void post() {
+ DropDownListView.this.post(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/widget/EdgeEffect.java b/android/widget/EdgeEffect.java
new file mode 100644
index 0000000..f9f5901
--- /dev/null
+++ b/android/widget/EdgeEffect.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2010 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.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+ @SuppressWarnings("UnusedDeclaration")
+ private static final String TAG = "EdgeEffect";
+
+ // Time it will take the effect to fully recede in ms
+ private static final int RECEDE_TIME = 600;
+
+ // Time it will take before a pulled glow begins receding in ms
+ private static final int PULL_TIME = 167;
+
+ // Time it will take in ms for a pulled glow to decay to partial strength before release
+ private static final int PULL_DECAY_TIME = 2000;
+
+ private static final float MAX_ALPHA = 0.15f;
+ private static final float GLOW_ALPHA_START = .09f;
+
+ private static final float MAX_GLOW_SCALE = 2.f;
+
+ private static final float PULL_GLOW_BEGIN = 0.f;
+
+ // Minimum velocity that will be absorbed
+ private static final int MIN_VELOCITY = 100;
+ // Maximum velocity, clamps at this value
+ private static final int MAX_VELOCITY = 10000;
+
+ private static final float EPSILON = 0.001f;
+
+ private static final double ANGLE = Math.PI / 6;
+ private static final float SIN = (float) Math.sin(ANGLE);
+ private static final float COS = (float) Math.cos(ANGLE);
+ private static final float RADIUS_FACTOR = 0.6f;
+
+ private float mGlowAlpha;
+ private float mGlowScaleY;
+
+ private float mGlowAlphaStart;
+ private float mGlowAlphaFinish;
+ private float mGlowScaleYStart;
+ private float mGlowScaleYFinish;
+
+ private long mStartTime;
+ private float mDuration;
+
+ private final Interpolator mInterpolator;
+
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PULL = 1;
+ private static final int STATE_ABSORB = 2;
+ private static final int STATE_RECEDE = 3;
+ private static final int STATE_PULL_DECAY = 4;
+
+ private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
+
+ private static final int VELOCITY_GLOW_FACTOR = 6;
+
+ private int mState = STATE_IDLE;
+
+ private float mPullDistance;
+
+ private final Rect mBounds = new Rect();
+ private final Paint mPaint = new Paint();
+ private float mRadius;
+ private float mBaseGlowScale;
+ private float mDisplacement = 0.5f;
+ private float mTargetDisplacement = 0.5f;
+
+ /**
+ * Construct a new EdgeEffect with a theme appropriate for the provided context.
+ * @param context Context used to provide theming and resource information for the EdgeEffect
+ */
+ public EdgeEffect(Context context) {
+ mPaint.setAntiAlias(true);
+ final TypedArray a = context.obtainStyledAttributes(
+ com.android.internal.R.styleable.EdgeEffect);
+ final int themeColor = a.getColor(
+ com.android.internal.R.styleable.EdgeEffect_colorEdgeEffect, 0xff666666);
+ a.recycle();
+ mPaint.setColor((themeColor & 0xffffff) | 0x33000000);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+ mInterpolator = new DecelerateInterpolator();
+ }
+
+ /**
+ * Set the size of this edge effect in pixels.
+ *
+ * @param width Effect width in pixels
+ * @param height Effect height in pixels
+ */
+ public void setSize(int width, int height) {
+ final float r = width * RADIUS_FACTOR / SIN;
+ final float y = COS * r;
+ final float h = r - y;
+ final float or = height * RADIUS_FACTOR / SIN;
+ final float oy = COS * or;
+ final float oh = or - oy;
+
+ mRadius = r;
+ mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f;
+
+ mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h));
+ }
+
+ /**
+ * Reports if this EdgeEffect's animation is finished. If this method returns false
+ * after a call to {@link #draw(Canvas)} the host widget should schedule another
+ * drawing pass to continue the animation.
+ *
+ * @return true if animation is finished, false if drawing should continue on the next frame.
+ */
+ public boolean isFinished() {
+ return mState == STATE_IDLE;
+ }
+
+ /**
+ * Immediately finish the current animation.
+ * After this call {@link #isFinished()} will return true.
+ */
+ public void finish() {
+ mState = STATE_IDLE;
+ }
+
+ /**
+ * A view should call this when content is pulled away from an edge by the user.
+ * This will update the state of the current visual effect and its associated animation.
+ * The host view should always {@link android.view.View#invalidate()} after this
+ * and draw the results accordingly.
+ *
+ * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement
+ * of the pull point is known.</p>
+ *
+ * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+ * 1.f (full length of the view) or negative values to express change
+ * back toward the edge reached to initiate the effect.
+ */
+ public void onPull(float deltaDistance) {
+ onPull(deltaDistance, 0.5f);
+ }
+
+ /**
+ * A view should call this when content is pulled away from an edge by the user.
+ * This will update the state of the current visual effect and its associated animation.
+ * The host view should always {@link android.view.View#invalidate()} after this
+ * and draw the results accordingly.
+ *
+ * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+ * 1.f (full length of the view) or negative values to express change
+ * back toward the edge reached to initiate the effect.
+ * @param displacement The displacement from the starting side of the effect of the point
+ * initiating the pull. In the case of touch this is the finger position.
+ * Values may be from 0-1.
+ */
+ public void onPull(float deltaDistance, float displacement) {
+ final long now = AnimationUtils.currentAnimationTimeMillis();
+ mTargetDisplacement = displacement;
+ if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+ return;
+ }
+ if (mState != STATE_PULL) {
+ mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY);
+ }
+ mState = STATE_PULL;
+
+ mStartTime = now;
+ mDuration = PULL_TIME;
+
+ mPullDistance += deltaDistance;
+
+ final float absdd = Math.abs(deltaDistance);
+ mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+ mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+ if (mPullDistance == 0) {
+ mGlowScaleY = mGlowScaleYStart = 0;
+ } else {
+ final float scale = (float) (Math.max(0, 1 - 1 /
+ Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d);
+
+ mGlowScaleY = mGlowScaleYStart = scale;
+ }
+
+ mGlowAlphaFinish = mGlowAlpha;
+ mGlowScaleYFinish = mGlowScaleY;
+ }
+
+ /**
+ * Call when the object is released after being pulled.
+ * This will begin the "decay" phase of the effect. After calling this method
+ * the host view should {@link android.view.View#invalidate()} and thereby
+ * draw the results accordingly.
+ */
+ public void onRelease() {
+ mPullDistance = 0;
+
+ if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+ return;
+ }
+
+ mState = STATE_RECEDE;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = RECEDE_TIME;
+ }
+
+ /**
+ * Call when the effect absorbs an impact at the given velocity.
+ * Used when a fling reaches the scroll boundary.
+ *
+ * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+ * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+ * to use here.</p>
+ *
+ * @param velocity Velocity at impact in pixels per second.
+ */
+ public void onAbsorb(int velocity) {
+ mState = STATE_ABSORB;
+ velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = 0.15f + (velocity * 0.02f);
+
+ // The glow depends more on the velocity, and therefore starts out
+ // nearly invisible.
+ mGlowAlphaStart = GLOW_ALPHA_START;
+ mGlowScaleYStart = Math.max(mGlowScaleY, 0.f);
+
+
+ // Growth for the size of the glow should be quadratic to properly
+ // respond
+ // to a user's scrolling speed. The faster the scrolling speed, the more
+ // intense the effect should be for both the size and the saturation.
+ mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f);
+ // Alpha should change for the glow as well as size.
+ mGlowAlphaFinish = Math.max(
+ mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+ mTargetDisplacement = 0.5f;
+ }
+
+ /**
+ * Set the color of this edge effect in argb.
+ *
+ * @param color Color in argb
+ */
+ public void setColor(@ColorInt int color) {
+ mPaint.setColor(color);
+ }
+
+ /**
+ * Return the color of this edge effect in argb.
+ * @return The color of this edge effect in argb
+ */
+ @ColorInt
+ public int getColor() {
+ return mPaint.getColor();
+ }
+
+ /**
+ * Draw into the provided canvas. Assumes that the canvas has been rotated
+ * accordingly and the size has been set. The effect will be drawn the full
+ * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+ * 1.f of height.
+ *
+ * @param canvas Canvas to draw into
+ * @return true if drawing should continue beyond this frame to continue the
+ * animation
+ */
+ public boolean draw(Canvas canvas) {
+ update();
+
+ final int count = canvas.save();
+
+ final float centerX = mBounds.centerX();
+ final float centerY = mBounds.height() - mRadius;
+
+ canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);
+
+ final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
+ float translateX = mBounds.width() * displacement / 2;
+
+ canvas.clipRect(mBounds);
+ canvas.translate(translateX, 0);
+ mPaint.setAlpha((int) (0xff * mGlowAlpha));
+ canvas.drawCircle(centerX, centerY, mRadius, mPaint);
+ canvas.restoreToCount(count);
+
+ boolean oneLastFrame = false;
+ if (mState == STATE_RECEDE && mGlowScaleY == 0) {
+ mState = STATE_IDLE;
+ oneLastFrame = true;
+ }
+
+ return mState != STATE_IDLE || oneLastFrame;
+ }
+
+ /**
+ * Return the maximum height that the edge effect will be drawn at given the original
+ * {@link #setSize(int, int) input size}.
+ * @return The maximum height of the edge effect
+ */
+ public int getMaxHeight() {
+ return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f);
+ }
+
+ private void update() {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+ final float interp = mInterpolator.getInterpolation(t);
+
+ mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+ mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+ mDisplacement = (mDisplacement + mTargetDisplacement) / 2;
+
+ if (t >= 1.f - EPSILON) {
+ switch (mState) {
+ case STATE_ABSORB:
+ mState = STATE_RECEDE;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = RECEDE_TIME;
+
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After absorb, the glow should fade to nothing.
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL:
+ mState = STATE_PULL_DECAY;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = PULL_DECAY_TIME;
+
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After pull, the glow should fade to nothing.
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL_DECAY:
+ mState = STATE_RECEDE;
+ break;
+ case STATE_RECEDE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+ }
+}
diff --git a/android/widget/EditText.java b/android/widget/EditText.java
new file mode 100644
index 0000000..56c3e4a
--- /dev/null
+++ b/android/widget/EditText.java
@@ -0,0 +1,182 @@
+/*
+ * 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.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.method.MovementMethod;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/*
+ * 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!
+ */
+
+/**
+ * A user interface element for entering and modifying text.
+ * When you define an edit text widget, you must specify the
+ * {@link android.R.styleable#TextView_inputType}
+ * attribute. For example, for plain text input set inputType to "text":
+ * <p>
+ * <pre>
+ * <EditText
+ * android:id="@+id/plain_text_input"
+ * android:layout_height="wrap_content"
+ * android:layout_width="match_parent"
+ * android:inputType="text"/></pre>
+ *
+ * Choosing the input type configures the keyboard type that is shown, acceptable characters,
+ * and appearance of the edit text.
+ * For example, if you want to accept a secret number, like a unique pin or serial number,
+ * you can set inputType to "numericPassword".
+ * An inputType of "numericPassword" results in an edit text that accepts numbers only,
+ * shows a numeric keyboard when focused, and masks the text that is entered for privacy.
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
+ * guide for examples of other
+ * {@link android.R.styleable#TextView_inputType} settings.
+ * </p>
+ * <p>You also can receive callbacks as a user changes text by
+ * adding a {@link android.text.TextWatcher} to the edit text.
+ * This is useful when you want to add auto-save functionality as changes are made,
+ * or validate the format of user input, for example.
+ * You add a text watcher using the {@link TextView#addTextChangedListener} method.
+ * </p>
+ * <p>
+ * This widget does not support auto-sizing text.
+ * <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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean getFreezesText() {
+ return true;
+ }
+
+ @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);
+ }
+
+ /**
+ * Causes words in the text that are longer than the view's width to be ellipsized instead of
+ * broken in the middle. {@link TextUtils.TruncateAt#MARQUEE
+ * TextUtils.TruncateAt#MARQUEE} is not supported.
+ *
+ * @param ellipsis Type of ellipsis to be applied.
+ * @throws IllegalArgumentException When the value of <code>ellipsis</code> parameter is
+ * {@link TextUtils.TruncateAt#MARQUEE}.
+ * @see TextView#setEllipsize(TextUtils.TruncateAt)
+ */
+ @Override
+ public void setEllipsize(TextUtils.TruncateAt ellipsis) {
+ if (ellipsis == TextUtils.TruncateAt.MARQUEE) {
+ throw new IllegalArgumentException("EditText cannot use the ellipsize mode "
+ + "TextUtils.TruncateAt.MARQUEE");
+ }
+ super.setEllipsize(ellipsis);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return EditText.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ protected boolean supportsAutoSizeText() {
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (isEnabled()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+ }
+ }
+}
diff --git a/android/widget/EditTextBackspacePerfTest.java b/android/widget/EditTextBackspacePerfTest.java
new file mode 100644
index 0000000..40b56f4
--- /dev/null
+++ b/android/widget/EditTextBackspacePerfTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.text.Selection;
+import android.view.KeyEvent;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextBackspacePerfTest {
+
+ private static final String BOY = "\uD83D\uDC66"; // U+1F466
+ private static final String US_FLAG = "\uD83C\uDDFA\uD83C\uDDF8"; // U+1F1FA U+1F1F8
+ private static final String FAMILY =
+ // U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467
+ "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67";
+ private static final String EMOJI_MODIFIER = "\uD83C\uDFFD"; // U+1F3FD
+ private static final String KEYCAP = "\u20E3";
+ private static final String COLOR_COPYRIGHT = "\u00A9\uFE0F";
+
+ @Parameters(name = "{0}")
+ public static Collection cases() {
+ return Arrays.asList(new Object[][] {
+ { "Latin", "aaa", 1 },
+ { "Flags", US_FLAG + US_FLAG + US_FLAG, 4 },
+ { "EmojiModifier",
+ BOY + EMOJI_MODIFIER + BOY + EMOJI_MODIFIER + BOY + EMOJI_MODIFIER, 4 },
+ { "KeyCap", "1" + KEYCAP + "1" + KEYCAP + "1" + KEYCAP, 2 },
+ { "ZwjSequence", FAMILY + FAMILY + FAMILY, 11 },
+ { "VariationSelector", COLOR_COPYRIGHT + COLOR_COPYRIGHT + COLOR_COPYRIGHT, 2 },
+ });
+ }
+
+ private final String mMetricKey;
+ private final String mText;
+ private final int mCursorPos;
+
+ private static final KeyEvent BACKSPACE_KEY_EVENT =
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
+ private static final KeyEvent RIGHT_ARROW_KEY_EVENT =
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public EditTextBackspacePerfTest(String metricKey, String text, int cursorPos) {
+ mMetricKey = metricKey;
+ mText = text;
+ mCursorPos = cursorPos;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private void prepareTextForBackspace(EditText editText) {
+ editText.setText(mText, TextView.BufferType.EDITABLE);
+ Selection.setSelection(editText.getText(), 0, 0);
+
+ // Do layout it here since the cursor movement requires layout information but it
+ // happens asynchronously even if the view is attached to an Activity.
+ editText.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ editText.invalidate();
+ editText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ editText.layout(0, 0, 1024, 768);
+
+ // mText contains three grapheme clusters. Move the cursor to the 2nd grapheme
+ // cluster by forwarding right arrow key event.
+ editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+ Assert.assertEquals(mCursorPos, Selection.getSelectionStart(editText.getText()));
+ }
+
+ @Test
+ public void testBackspace() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ EditText editText = new EditText(mActivityRule.getActivity());
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ // Prepare the test data for this iteration with pausing timer.
+ state.pauseTiming();
+ prepareTextForBackspace(editText);
+ state.resumeTiming();
+
+ editText.onKeyDown(BACKSPACE_KEY_EVENT.getKeyCode(), BACKSPACE_KEY_EVENT);
+ }
+ });
+ }
+}
diff --git a/android/widget/EditTextCursorMovementPerfTest.java b/android/widget/EditTextCursorMovementPerfTest.java
new file mode 100644
index 0000000..b100acb
--- /dev/null
+++ b/android/widget/EditTextCursorMovementPerfTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.text.Selection;
+import android.view.KeyEvent;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextCursorMovementPerfTest {
+
+ private static final String BOY = "\uD83D\uDC66"; // U+1F466
+ private static final String US_FLAG = "\uD83C\uDDFA\uD83C\uDDF8"; // U+1F1FA U+1F1F8
+ private static final String FAMILY =
+ // U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467
+ "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67";
+
+ @Parameters(name = "{0}")
+ public static Collection cases() {
+ return Arrays.asList(new Object[][] {
+ { "Latin", "aaa", 1 },
+ { "Emoji", BOY + BOY + BOY, 2 },
+ { "Flags", US_FLAG + US_FLAG + US_FLAG, 4 },
+ { "ZwjSequence", FAMILY + FAMILY + FAMILY, 11 },
+ });
+ }
+
+ private final String mMetricKey;
+ private final String mText;
+ private final int mCursorPos;
+
+ private static final KeyEvent LEFT_ARROW_KEY_EVENT =
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
+ private static final KeyEvent RIGHT_ARROW_KEY_EVENT =
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public EditTextCursorMovementPerfTest(String metricKey, String text, int cursorPos) {
+ mMetricKey = metricKey;
+ mText = text;
+ mCursorPos = cursorPos;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testCursorMovement() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ EditText editText = new EditText(mActivityRule.getActivity());
+
+ editText.setText(mText, TextView.BufferType.EDITABLE);
+ Selection.setSelection(editText.getText(), 0, 0);
+
+ // Layout it here since the cursor movement requires layout information but it
+ // happens asynchronously even if the view is attached to an Activity.
+ editText.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ editText.invalidate();
+ editText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ editText.layout(0, 0, 1024, 768);
+
+ // mText contains three grapheme clusters. Move the cursor to the 2nd grapheme
+ // cluster by forwarding right arrow key event.
+ editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+ Assert.assertEquals(mCursorPos, Selection.getSelectionStart(editText.getText()));
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+ editText.onKeyDown(LEFT_ARROW_KEY_EVENT.getKeyCode(), LEFT_ARROW_KEY_EVENT);
+ }
+ }
+ });
+ }
+}
diff --git a/android/widget/EditTextLongTextPerfTest.java b/android/widget/EditTextLongTextPerfTest.java
new file mode 100644
index 0000000..ce0c357
--- /dev/null
+++ b/android/widget/EditTextLongTextPerfTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 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.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Random;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.RenderNodeAnimator;
+import android.view.ViewGroup;
+import android.view.View.MeasureSpec;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextLongTextPerfTest {
+ @Parameters(name = "{0}")
+ public static Collection cases() {
+ return Arrays.asList(new Object[][] {
+ { "10x30K", 10, 30000 },
+ { "300x1K", 300, 1000 },
+ });
+ }
+
+ private final String mMetricKey;
+ private final int mChars;
+ private final int mLines;
+
+ public EditTextLongTextPerfTest(String metricKey, int chars, int lines) {
+ mMetricKey = metricKey;
+ mChars = chars;
+ mLines = lines;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private EditText setupEditText() {
+ final EditText editText = new EditText(mActivityRule.getActivity());
+
+ String alphabet = "abcdefghijklmnopqrstuvwxyz";
+ final long seed = 1234567890;
+ Random r = new Random(seed);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mLines; i++) {
+ for (int j = 0; j < mChars; j++) {
+ char c = alphabet.charAt(r.nextInt(alphabet.length()));
+ sb.append(c);
+ }
+ sb.append('\n');
+ }
+
+ final int height = 1000;
+ final int width = 1000;
+ editText.setHeight(height);
+ editText.setWidth(width);
+ editText.setLayoutParams(new ViewGroup.LayoutParams(width, height));
+
+ Activity activity = mActivityRule.getActivity();
+ activity.setContentView(editText);
+
+ editText.setText(sb.toString(), TextView.BufferType.EDITABLE);
+ editText.invalidate();
+ editText.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ editText.layout(0, 0, height, width);
+
+ return editText;
+ }
+
+ @Test
+ public void testEditText() throws Throwable {
+ mActivityRule.runOnUiThread(() -> {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final EditText editText = setupEditText();
+ final KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
+ final int steps = 100;
+ while (state.keepRunning()) {
+ for (int i = 0; i < steps; i++) {
+ int offset = (editText.getText().length() * i) / steps;
+ editText.setSelection(offset);
+ editText.bringPointIntoView(offset);
+ editText.onKeyDown(keyEvent.getKeyCode(), keyEvent);
+ editText.updateDisplayListIfDirty();
+ }
+ }
+ });
+ }
+}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
new file mode 100644
index 0000000..d23dfe4
--- /dev/null
+++ b/android/widget/Editor.java
@@ -0,0 +1,6494 @@
+/*
+ * Copyright (C) 2012 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.R;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UndoManager;
+import android.content.UndoOperation;
+import android.content.UndoOwner;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableParcel;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.WordIterator;
+import android.text.style.EasyEditSpan;
+import android.text.style.SuggestionRangeSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.URLSpan;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
+import android.view.DisplayListCanvas;
+import android.view.DragAndDropPermissions;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.RenderNode;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.textclassifier.TextClassification;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView.Drawables;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.widget.EditableInputConnection;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * Helper class used by TextView to handle editable text views.
+ *
+ * @hide
+ */
+public class Editor {
+ private static final String TAG = "Editor";
+ private static final boolean DEBUG_UNDO = false;
+
+ static final int BLINK = 500;
+ private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+ private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
+ private static final int UNSET_X_VALUE = -1;
+ private static final int UNSET_LINE = -1;
+ // Tag used when the Editor maintains its own separate UndoManager.
+ private static final String UNDO_OWNER_TAG = "Editor";
+
+ // Ordering constants used to place the Action Mode or context menu items in their menu.
+ private static final int MENU_ITEM_ORDER_ASSIST = 0;
+ private static final int MENU_ITEM_ORDER_UNDO = 2;
+ private static final int MENU_ITEM_ORDER_REDO = 3;
+ private static final int MENU_ITEM_ORDER_CUT = 4;
+ private static final int MENU_ITEM_ORDER_COPY = 5;
+ private static final int MENU_ITEM_ORDER_PASTE = 6;
+ private static final int MENU_ITEM_ORDER_SHARE = 7;
+ private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
+ private static final int MENU_ITEM_ORDER_REPLACE = 9;
+ private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
+ private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
+ private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // Each Editor manages its own undo stack.
+ private final UndoManager mUndoManager = new UndoManager();
+ private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+ final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
+ boolean mAllowUndo = true;
+
+ private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+ // Cursor Controllers.
+ private InsertionPointCursorController mInsertionPointCursorController;
+ SelectionModifierCursorController mSelectionModifierCursorController;
+ // Action mode used when text is selected or when actions on an insertion cursor are triggered.
+ private ActionMode mTextActionMode;
+ private boolean mInsertionControllerEnabled;
+ private boolean mSelectionControllerEnabled;
+
+ private final boolean mHapticTextHandleEnabled;
+
+ // Used to highlight a word when it is corrected by the IME
+ private CorrectionHighlighter mCorrectionHighlighter;
+
+ InputContentType mInputContentType;
+ InputMethodState mInputMethodState;
+
+ private static class TextRenderNode {
+ // Render node has 3 recording states:
+ // 1. Recorded operations are valid.
+ // #needsRecord() returns false, but needsToBeShifted is false.
+ // 2. Recorded operations are not valid, but just the position needed to be updated.
+ // #needsRecord() returns false, but needsToBeShifted is true.
+ // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
+ // true.
+ RenderNode renderNode;
+ boolean isDirty;
+ // Becomes true when recorded operations can be reused, but the position has to be updated.
+ boolean needsToBeShifted;
+ public TextRenderNode(String name) {
+ renderNode = RenderNode.create(name, null);
+ isDirty = true;
+ needsToBeShifted = true;
+ }
+ boolean needsRecord() {
+ return isDirty || !renderNode.isValid();
+ }
+ }
+ private TextRenderNode[] mTextRenderNodes;
+
+ boolean mFrozenWithFocus;
+ boolean mSelectionMoved;
+ boolean mTouchFocusSelected;
+
+ KeyListener mKeyListener;
+ int mInputType = EditorInfo.TYPE_NULL;
+
+ boolean mDiscardNextActionUp;
+ boolean mIgnoreActionUpEvent;
+
+ long mShowCursor;
+ private Blink mBlink;
+
+ boolean mCursorVisible = true;
+ boolean mSelectAllOnFocus;
+ boolean mTextIsSelectable;
+
+ CharSequence mError;
+ boolean mErrorWasChanged;
+ private ErrorPopup mErrorPopup;
+
+ /**
+ * This flag is set if the TextView tries to display an error before it
+ * is attached to the window (so its position is still unknown).
+ * It causes the error to be shown later, when onAttachedToWindow()
+ * is called.
+ */
+ private boolean mShowErrorAfterAttach;
+
+ boolean mInBatchEditControllers;
+ boolean mShowSoftInputOnFocus = true;
+ private boolean mPreserveSelection;
+ private boolean mRestartActionModeOnNextRefresh;
+
+ private SelectionActionModeHelper mSelectionActionModeHelper;
+
+ boolean mIsBeingLongClicked;
+
+ private SuggestionsPopupWindow mSuggestionsPopupWindow;
+ SuggestionRangeSpan mSuggestionRangeSpan;
+ private Runnable mShowSuggestionRunnable;
+
+ Drawable mCursorDrawable = null;
+
+ private Drawable mSelectHandleLeft;
+ private Drawable mSelectHandleRight;
+ private Drawable mSelectHandleCenter;
+
+ // Global listener that detects changes in the global position of the TextView
+ private PositionListener mPositionListener;
+
+ private float mLastDownPositionX, mLastDownPositionY;
+ private float mLastUpPositionX, mLastUpPositionY;
+ private float mContextMenuAnchorX, mContextMenuAnchorY;
+ Callback mCustomSelectionActionModeCallback;
+ Callback mCustomInsertionActionModeCallback;
+
+ // Set when this TextView gained focus with some text selected. Will start selection mode.
+ boolean mCreatedWithASelection;
+
+ // Indicates the current tap state (first tap, double tap, or triple click).
+ private int mTapState = TAP_STATE_INITIAL;
+ private long mLastTouchUpTime = 0;
+ private static final int TAP_STATE_INITIAL = 0;
+ private static final int TAP_STATE_FIRST_TAP = 1;
+ private static final int TAP_STATE_DOUBLE_TAP = 2;
+ // Only for mouse input.
+ private static final int TAP_STATE_TRIPLE_CLICK = 3;
+
+ // The button state as of the last time #onTouchEvent is called.
+ private int mLastButtonState;
+
+ private Runnable mInsertionActionModeRunnable;
+
+ // The span controller helps monitoring the changes to which the Editor needs to react:
+ // - EasyEditSpans, for which we have some UI to display on attach and on hide
+ // - SelectionSpans, for which we need to call updateSelection if an IME is attached
+ private SpanController mSpanController;
+
+ private WordIterator mWordIterator;
+ SpellChecker mSpellChecker;
+
+ // This word iterator is set with text and used to determine word boundaries
+ // when a user is selecting text.
+ private WordIterator mWordIteratorWithText;
+ // Indicate that the text in the word iterator needs to be updated.
+ private boolean mUpdateWordIteratorText;
+
+ private Rect mTempRect;
+
+ private final TextView mTextView;
+
+ final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
+
+ private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
+ new CursorAnchorInfoNotifier();
+
+ private final Runnable mShowFloatingToolbar = new Runnable() {
+ @Override
+ public void run() {
+ if (mTextActionMode != null) {
+ mTextActionMode.hide(0); // hide off.
+ }
+ }
+ };
+
+ boolean mIsInsertionActionModeStartPending = false;
+
+ private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
+
+ Editor(TextView textView) {
+ mTextView = textView;
+ // Synchronize the filter list, which places the undo input filter at the end.
+ mTextView.setFilters(mTextView.getFilters());
+ mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
+ mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_enableHapticTextHandle);
+ }
+
+ ParcelableParcel saveInstanceState() {
+ ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
+ Parcel parcel = state.getParcel();
+ mUndoManager.saveInstanceState(parcel);
+ mUndoInputFilter.saveInstanceState(parcel);
+ return state;
+ }
+
+ void restoreInstanceState(ParcelableParcel state) {
+ Parcel parcel = state.getParcel();
+ mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
+ mUndoInputFilter.restoreInstanceState(parcel);
+ // Re-associate this object as the owner of undo state.
+ mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+ }
+
+ /**
+ * Forgets all undo and redo operations for this Editor.
+ */
+ void forgetUndoRedo() {
+ UndoOwner[] owners = { mUndoOwner };
+ mUndoManager.forgetUndos(owners, -1 /* all */);
+ mUndoManager.forgetRedos(owners, -1 /* all */);
+ }
+
+ boolean canUndo() {
+ UndoOwner[] owners = { mUndoOwner };
+ return mAllowUndo && mUndoManager.countUndos(owners) > 0;
+ }
+
+ boolean canRedo() {
+ UndoOwner[] owners = { mUndoOwner };
+ return mAllowUndo && mUndoManager.countRedos(owners) > 0;
+ }
+
+ void undo() {
+ if (!mAllowUndo) {
+ return;
+ }
+ UndoOwner[] owners = { mUndoOwner };
+ mUndoManager.undo(owners, 1); // Undo 1 action.
+ }
+
+ void redo() {
+ if (!mAllowUndo) {
+ return;
+ }
+ UndoOwner[] owners = { mUndoOwner };
+ mUndoManager.redo(owners, 1); // Redo 1 action.
+ }
+
+ void replace() {
+ if (mSuggestionsPopupWindow == null) {
+ mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+ }
+ hideCursorAndSpanControllers();
+ mSuggestionsPopupWindow.show();
+
+ int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+ Selection.setSelection((Spannable) mTextView.getText(), middle);
+ }
+
+ void onAttachedToWindow() {
+ if (mShowErrorAfterAttach) {
+ showError();
+ mShowErrorAfterAttach = false;
+ }
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ // No need to create the controller.
+ // The get method will add the listener on controller creation.
+ if (mInsertionPointCursorController != null) {
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.resetTouchOffsets();
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+ updateSpellCheckSpans(0, mTextView.getText().length(),
+ true /* create the spell checker if needed */);
+
+ if (mTextView.hasSelection()) {
+ refreshTextActionMode();
+ }
+
+ getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
+ resumeBlink();
+ }
+
+ void onDetachedFromWindow() {
+ getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
+
+ if (mError != null) {
+ hideError();
+ }
+
+ suspendBlink();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ }
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ }
+
+ // Cancel the single tap delayed runnable.
+ if (mInsertionActionModeRunnable != null) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+
+ mTextView.removeCallbacks(mShowFloatingToolbar);
+
+ discardTextDisplayLists();
+
+ if (mSpellChecker != null) {
+ mSpellChecker.closeSession();
+ // Forces the creation of a new SpellChecker next time this window is created.
+ // Will handle the cases where the settings has been changed in the meantime.
+ mSpellChecker = null;
+ }
+
+ hideCursorAndSpanControllers();
+ stopTextActionModeWithPreservingSelection();
+ }
+
+ private void discardTextDisplayLists() {
+ if (mTextRenderNodes != null) {
+ for (int i = 0; i < mTextRenderNodes.length; i++) {
+ RenderNode displayList = mTextRenderNodes[i] != null
+ ? mTextRenderNodes[i].renderNode : null;
+ if (displayList != null && displayList.isValid()) {
+ displayList.discardDisplayList();
+ }
+ }
+ }
+ }
+
+ private void showError() {
+ if (mTextView.getWindowToken() == null) {
+ mShowErrorAfterAttach = true;
+ return;
+ }
+
+ if (mErrorPopup == null) {
+ LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
+ final TextView err = (TextView) inflater.inflate(
+ com.android.internal.R.layout.textview_hint, null);
+
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ mErrorPopup =
+ new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
+ mErrorPopup.setFocusable(false);
+ // The user is entering text, so the input method is needed. We
+ // don't want the popup to be displayed on top of it.
+ mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ }
+
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ tv.setText(mError);
+
+ mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
+ mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
+ }
+
+ public void setError(CharSequence error, Drawable icon) {
+ mError = TextUtils.stringOrSpannedString(error);
+ mErrorWasChanged = true;
+
+ if (mError == null) {
+ setErrorIcon(null);
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+
+ mErrorPopup = null;
+ }
+ mShowErrorAfterAttach = false;
+ } else {
+ setErrorIcon(icon);
+ if (mTextView.isFocused()) {
+ showError();
+ }
+ }
+ }
+
+ private void setErrorIcon(Drawable icon) {
+ Drawables dr = mTextView.mDrawables;
+ if (dr == null) {
+ mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
+ }
+ dr.setErrorDrawable(icon, mTextView);
+
+ mTextView.resetResolvedDrawables();
+ mTextView.invalidate();
+ mTextView.requestLayout();
+ }
+
+ private void hideError() {
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+ }
+
+ mShowErrorAfterAttach = false;
+ }
+
+ /**
+ * Returns the X 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
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+
+ final Drawables dr = mTextView.mDrawables;
+
+ final int layoutDirection = mTextView.getLayoutDirection();
+ int errorX;
+ int offset;
+ switch (layoutDirection) {
+ default:
+ case View.LAYOUT_DIRECTION_LTR:
+ offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
+ errorX = mTextView.getWidth() - mErrorPopup.getWidth()
+ - mTextView.getPaddingRight() + offset;
+ break;
+ case View.LAYOUT_DIRECTION_RTL:
+ offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
+ errorX = mTextView.getPaddingLeft() + offset;
+ break;
+ }
+ return errorX;
+ }
+
+ /**
+ * 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.
+ */
+ final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
+ int vspace = mTextView.getBottom() - mTextView.getTop()
+ - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
+
+ final Drawables dr = mTextView.mDrawables;
+
+ final int layoutDirection = mTextView.getLayoutDirection();
+ int height;
+ switch (layoutDirection) {
+ default:
+ case View.LAYOUT_DIRECTION_LTR:
+ height = (dr != null ? dr.mDrawableHeightRight : 0);
+ break;
+ case View.LAYOUT_DIRECTION_RTL:
+ height = (dr != null ? dr.mDrawableHeightLeft : 0);
+ break;
+ }
+
+ int icontop = compoundPaddingTop + (vspace - height) / 2;
+
+ /*
+ * The "2" is the distance between the point and the top edge
+ * of the background.
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
+ }
+
+ void createInputContentTypeIfNeeded() {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ }
+
+ void createInputMethodStateIfNeeded() {
+ if (mInputMethodState == null) {
+ mInputMethodState = new InputMethodState();
+ }
+ }
+
+ boolean isCursorVisible() {
+ // The default value is true, even when there is no associated Editor
+ return mCursorVisible && mTextView.isTextEditable();
+ }
+
+ void prepareCursorControllers() {
+ boolean windowSupportsHandles = false;
+
+ ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
+ if (params instanceof WindowManager.LayoutParams) {
+ WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
+ windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
+ || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
+ }
+
+ boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
+ mInsertionControllerEnabled = enabled && isCursorVisible();
+ mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
+
+ if (!mInsertionControllerEnabled) {
+ hideInsertionPointCursorController();
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ mInsertionPointCursorController = null;
+ }
+ }
+
+ if (!mSelectionControllerEnabled) {
+ stopTextActionMode();
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ mSelectionModifierCursorController = null;
+ }
+ }
+ }
+
+ void hideInsertionPointCursorController() {
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ }
+
+ /**
+ * Hides the insertion and span controllers.
+ */
+ void hideCursorAndSpanControllers() {
+ hideCursorControllers();
+ hideSpanControllers();
+ }
+
+ private void hideSpanControllers() {
+ if (mSpanController != null) {
+ mSpanController.hide();
+ }
+ }
+
+ private void hideCursorControllers() {
+ // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
+ // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
+ // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
+ // to distinguish one from the other.
+ if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
+ || !mSuggestionsPopupWindow.isShowingUp())) {
+ // Should be done before hide insertion point controller since it triggers a show of it
+ mSuggestionsPopupWindow.hide();
+ }
+ hideInsertionPointCursorController();
+ }
+
+ /**
+ * Create new SpellCheckSpans on the modified region.
+ */
+ private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
+ // Remove spans whose adjacent characters are text not punctuation
+ mTextView.removeAdjacentSuggestionSpans(start);
+ mTextView.removeAdjacentSuggestionSpans(end);
+
+ if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
+ && !(mTextView.isInExtractedMode())) {
+ if (mSpellChecker == null && createSpellChecker) {
+ mSpellChecker = new SpellChecker(mTextView);
+ }
+ if (mSpellChecker != null) {
+ mSpellChecker.spellCheck(start, end);
+ }
+ }
+ }
+
+ void onScreenStateChanged(int screenState) {
+ switch (screenState) {
+ case View.SCREEN_STATE_ON:
+ resumeBlink();
+ break;
+ case View.SCREEN_STATE_OFF:
+ suspendBlink();
+ break;
+ }
+ }
+
+ private void suspendBlink() {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ }
+
+ private void resumeBlink() {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ }
+
+ void adjustInputType(boolean password, boolean passwordInputType,
+ boolean webPasswordInputType, boolean numberPasswordInputType) {
+ // mInputType has been set from inputType, possibly modified by mInputMethod.
+ // Specialize mInputType to [web]password if we have a text class and the original input
+ // type was a password.
+ if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ if (password || passwordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ if (webPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ }
+ } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
+ if (numberPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+ }
+ }
+ }
+
+ private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
+ @NonNull TextView tv) {
+ final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.textview_error_popup_default_width);
+ final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
+ defaultWidthInPixels)
+ .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
+ .build();
+
+ 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 capped
+ * to DEFAULT_MAX_POPUP_WIDTH
+ */
+ pop.setWidth(wid + (int) Math.ceil(max));
+ pop.setHeight(ht + l.getHeight());
+ }
+
+ void setFrame() {
+ if (mErrorPopup != null) {
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
+ mErrorPopup.getWidth(), mErrorPopup.getHeight());
+ }
+ }
+
+ private int getWordStart(int offset) {
+ // FIXME - For this and similar methods we're not doing anything to check if there's
+ // a LocaleSpan in the text, this may be something we should try handling or checking for.
+ int retOffset = getWordIteratorWithText().prevBoundary(offset);
+ if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
+ // On punctuation boundary or within group of punctuation, find punctuation start.
+ retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
+ } else {
+ // Not on a punctuation boundary, find the word start.
+ retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
+ }
+ if (retOffset == BreakIterator.DONE) {
+ return offset;
+ }
+ return retOffset;
+ }
+
+ private int getWordEnd(int offset) {
+ int retOffset = getWordIteratorWithText().nextBoundary(offset);
+ if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
+ // On punctuation boundary or within group of punctuation, find punctuation end.
+ retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
+ } else {
+ // Not on a punctuation boundary, find the word end.
+ retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
+ }
+ if (retOffset == BreakIterator.DONE) {
+ return offset;
+ }
+ return retOffset;
+ }
+
+ private boolean needsToSelectAllToSelectWordOrParagraph() {
+ if (mTextView.hasPasswordTransformationMethod()) {
+ // Always select all on a password field.
+ // Cut/copy menu entries are not available for passwords, but being able to select all
+ // is however useful to delete or paste to replace the entire content.
+ return true;
+ }
+
+ int inputType = mTextView.getInputType();
+ int klass = inputType & InputType.TYPE_MASK_CLASS;
+ int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ // Specific text field types: select the entire text for these
+ if (klass == InputType.TYPE_CLASS_NUMBER
+ || klass == InputType.TYPE_CLASS_PHONE
+ || klass == InputType.TYPE_CLASS_DATETIME
+ || variation == InputType.TYPE_TEXT_VARIATION_URI
+ || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+ || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adjusts selection to the word under last touch offset. Return true if the operation was
+ * successfully performed.
+ */
+ boolean selectCurrentWord() {
+ if (!mTextView.canSelectText()) {
+ return false;
+ }
+
+ if (needsToSelectAllToSelectWordOrParagraph()) {
+ return mTextView.selectAllText();
+ }
+
+ long lastTouchOffsets = getLastTouchOffsets();
+ final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+ final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+ // Safety check in case standard touch event handling has been bypassed
+ if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
+ if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
+
+ int selectionStart, selectionEnd;
+
+ // If a URLSpan (web address, email, phone...) is found at that position, select it.
+ URLSpan[] urlSpans =
+ ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
+ if (urlSpans.length >= 1) {
+ URLSpan urlSpan = urlSpans[0];
+ selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
+ selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
+ } else {
+ // FIXME - We should check if there's a LocaleSpan in the text, this may be
+ // something we should try handling or checking for.
+ final WordIterator wordIterator = getWordIterator();
+ wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
+
+ selectionStart = wordIterator.getBeginning(minOffset);
+ selectionEnd = wordIterator.getEnd(maxOffset);
+
+ if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
+ || selectionStart == selectionEnd) {
+ // Possible when the word iterator does not properly handle the text's language
+ long range = getCharClusterRange(minOffset);
+ selectionStart = TextUtils.unpackRangeStartFromLong(range);
+ selectionEnd = TextUtils.unpackRangeEndFromLong(range);
+ }
+ }
+
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ return selectionEnd > selectionStart;
+ }
+
+ /**
+ * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
+ * successfully performed.
+ */
+ private boolean selectCurrentParagraph() {
+ if (!mTextView.canSelectText()) {
+ return false;
+ }
+
+ if (needsToSelectAllToSelectWordOrParagraph()) {
+ return mTextView.selectAllText();
+ }
+
+ long lastTouchOffsets = getLastTouchOffsets();
+ final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+ final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+ final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
+ final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+ final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+ if (start < end) {
+ Selection.setSelection((Spannable) mTextView.getText(), start, end);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the minimum range of paragraphs that contains startOffset and endOffset.
+ */
+ private long getParagraphsRange(int startOffset, int endOffset) {
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ return TextUtils.packRangeInLong(-1, -1);
+ }
+ final CharSequence text = mTextView.getText();
+ int minLine = layout.getLineForOffset(startOffset);
+ // Search paragraph start.
+ while (minLine > 0) {
+ final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
+ if (text.charAt(prevLineEndOffset - 1) == '\n') {
+ break;
+ }
+ minLine--;
+ }
+ int maxLine = layout.getLineForOffset(endOffset);
+ // Search paragraph end.
+ while (maxLine < layout.getLineCount() - 1) {
+ final int lineEndOffset = layout.getLineEnd(maxLine);
+ if (text.charAt(lineEndOffset - 1) == '\n') {
+ break;
+ }
+ maxLine++;
+ }
+ return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
+ }
+
+ void onLocaleChanged() {
+ // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
+ // proper new locale
+ mWordIterator = null;
+ mWordIteratorWithText = null;
+ }
+
+ public WordIterator getWordIterator() {
+ if (mWordIterator == null) {
+ mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
+ }
+ return mWordIterator;
+ }
+
+ private WordIterator getWordIteratorWithText() {
+ if (mWordIteratorWithText == null) {
+ mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
+ mUpdateWordIteratorText = true;
+ }
+ if (mUpdateWordIteratorText) {
+ // FIXME - Shouldn't copy all of the text as only the area of the text relevant
+ // to the user's selection is needed. A possible solution would be to
+ // copy some number N of characters near the selection and then when the
+ // user approaches N then we'd do another copy of the next N characters.
+ CharSequence text = mTextView.getText();
+ mWordIteratorWithText.setCharSequence(text, 0, text.length());
+ mUpdateWordIteratorText = false;
+ }
+ return mWordIteratorWithText;
+ }
+
+ private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) return offset;
+ return findAfterGivenOffset == layout.isRtlCharAt(offset)
+ ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
+ }
+
+ private long getCharClusterRange(int offset) {
+ final int textLength = mTextView.getText().length();
+ if (offset < textLength) {
+ final int clusterEndOffset = getNextCursorOffset(offset, true);
+ return TextUtils.packRangeInLong(
+ getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
+ }
+ if (offset - 1 >= 0) {
+ final int clusterStartOffset = getNextCursorOffset(offset, false);
+ return TextUtils.packRangeInLong(clusterStartOffset,
+ getNextCursorOffset(clusterStartOffset, true));
+ }
+ return TextUtils.packRangeInLong(offset, offset);
+ }
+
+ private boolean touchPositionIsInSelection() {
+ int selectionStart = mTextView.getSelectionStart();
+ int selectionEnd = mTextView.getSelectionEnd();
+
+ if (selectionStart == selectionEnd) {
+ return false;
+ }
+
+ if (selectionStart > selectionEnd) {
+ int tmp = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tmp;
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ }
+
+ SelectionModifierCursorController selectionController = getSelectionController();
+ int minOffset = selectionController.getMinTouchOffset();
+ int maxOffset = selectionController.getMaxTouchOffset();
+
+ return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
+ }
+
+ private PositionListener getPositionListener() {
+ if (mPositionListener == null) {
+ mPositionListener = new PositionListener();
+ }
+ return mPositionListener;
+ }
+
+ private interface TextViewPositionListener {
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled);
+ }
+
+ private boolean isOffsetVisible(int offset) {
+ Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ final int line = layout.getLineForOffset(offset);
+ final int lineBottom = layout.getLineBottom(line);
+ final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
+ return mTextView.isPositionVisible(
+ primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
+ lineBottom + mTextView.viewportToContentVerticalOffset());
+ }
+
+ /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+ * in the view. Returns false when the position is in the empty space of left/right of text.
+ */
+ private boolean isPositionOnText(float x, float y) {
+ Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ final int line = mTextView.getLineAtCoordinate(y);
+ x = mTextView.convertToLocalHorizontalCoordinate(x);
+
+ if (x < layout.getLineLeft(line)) return false;
+ if (x > layout.getLineRight(line)) return false;
+ return true;
+ }
+
+ private void startDragAndDrop() {
+ getSelectionActionModeHelper().onSelectionDrag();
+
+ // TODO: Fix drag and drop in full screen extracted mode.
+ if (mTextView.isInExtractedMode()) {
+ return;
+ }
+ final int start = mTextView.getSelectionStart();
+ final int end = mTextView.getSelectionEnd();
+ CharSequence selectedText = mTextView.getTransformedText(start, end);
+ ClipData data = ClipData.newPlainText(null, selectedText);
+ DragLocalState localState = new DragLocalState(mTextView, start, end);
+ mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
+ View.DRAG_FLAG_GLOBAL);
+ stopTextActionMode();
+ if (hasSelectionController()) {
+ getSelectionController().resetTouchOffsets();
+ }
+ }
+
+ public boolean performLongClick(boolean handled) {
+ // Long press in empty space moves cursor and starts the insertion action mode.
+ if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
+ && mInsertionControllerEnabled) {
+ final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
+ mLastDownPositionY);
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ getInsertionController().show();
+ mIsInsertionActionModeStartPending = true;
+ handled = true;
+ MetricsLogger.action(
+ mTextView.getContext(),
+ MetricsEvent.TEXT_LONGPRESS,
+ TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
+ }
+
+ if (!handled && mTextActionMode != null) {
+ if (touchPositionIsInSelection()) {
+ startDragAndDrop();
+ MetricsLogger.action(
+ mTextView.getContext(),
+ MetricsEvent.TEXT_LONGPRESS,
+ TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
+ } else {
+ stopTextActionMode();
+ selectCurrentWordAndStartDrag();
+ MetricsLogger.action(
+ mTextView.getContext(),
+ MetricsEvent.TEXT_LONGPRESS,
+ TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
+ }
+ handled = true;
+ }
+
+ // Start a new selection
+ if (!handled) {
+ handled = selectCurrentWordAndStartDrag();
+ if (handled) {
+ MetricsLogger.action(
+ mTextView.getContext(),
+ MetricsEvent.TEXT_LONGPRESS,
+ TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
+ }
+ }
+
+ return handled;
+ }
+
+ float getLastUpPositionX() {
+ return mLastUpPositionX;
+ }
+
+ float getLastUpPositionY() {
+ return mLastUpPositionY;
+ }
+
+ private long getLastTouchOffsets() {
+ SelectionModifierCursorController selectionController = getSelectionController();
+ final int minOffset = selectionController.getMinTouchOffset();
+ final int maxOffset = selectionController.getMaxTouchOffset();
+ return TextUtils.packRangeInLong(minOffset, maxOffset);
+ }
+
+ void onFocusChanged(boolean focused, int direction) {
+ mShowCursor = SystemClock.uptimeMillis();
+ ensureEndedBatchEdit();
+
+ if (focused) {
+ int selStart = mTextView.getSelectionStart();
+ int selEnd = mTextView.getSelectionEnd();
+
+ // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
+ // mode for these, unless there was a specific selection already started.
+ final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
+ && selEnd == mTextView.getText().length();
+
+ mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
+ && !isFocusHighlighted;
+
+ if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+ // If a tap was used to give focus to that view, move cursor at tap position.
+ // Has to be done before onTakeFocus, which can be overloaded.
+ final int lastTapPosition = getLastTapPosition();
+ if (lastTapPosition >= 0) {
+ Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
+ }
+
+ // Note this may have to be moved out of the Editor class
+ MovementMethod mMovement = mTextView.getMovementMethod();
+ if (mMovement != null) {
+ mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
+ }
+
+ // The DecorView does not have focus when the 'Done' ExtractEditText button is
+ // pressed. Since it is the ViewAncestor's mView, it requests focus before
+ // ExtractEditText clears focus, which gives focus to the ExtractEditText.
+ // This special case ensure that we keep current selection in that case.
+ // It would be better to know why the DecorView does not have focus at that time.
+ if (((mTextView.isInExtractedMode()) || mSelectionMoved)
+ && 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) mTextView.getText(), selStart, selEnd);
+ }
+
+ if (mSelectAllOnFocus) {
+ mTextView.selectAllText();
+ }
+
+ mTouchFocusSelected = true;
+ }
+
+ mFrozenWithFocus = false;
+ mSelectionMoved = false;
+
+ if (mError != null) {
+ showError();
+ }
+
+ makeBlink();
+ } else {
+ if (mError != null) {
+ hideError();
+ }
+ // Don't leave us in the middle of a batch edit.
+ mTextView.onEndBatchEdit();
+
+ if (mTextView.isInExtractedMode()) {
+ hideCursorAndSpanControllers();
+ stopTextActionModeWithPreservingSelection();
+ } else {
+ hideCursorAndSpanControllers();
+ if (mTextView.isTemporarilyDetached()) {
+ stopTextActionModeWithPreservingSelection();
+ } else {
+ stopTextActionMode();
+ }
+ downgradeEasyCorrectionSpans();
+ }
+ // No need to create the controller
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.resetTouchOffsets();
+ }
+ }
+ }
+
+ /**
+ * Downgrades to simple suggestions all the easy correction spans that are not a spell check
+ * span.
+ */
+ private void downgradeEasyCorrectionSpans() {
+ CharSequence text = mTextView.getText();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
+ spannable.length(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ int flags = suggestionSpans[i].getFlags();
+ if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+ && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
+ flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+ suggestionSpans[i].setFlags(flags);
+ }
+ }
+ }
+ }
+
+ void sendOnTextChanged(int start, int before, int after) {
+ getSelectionActionModeHelper().onTextChanged(start, start + before);
+ updateSpellCheckSpans(start, start + after, false);
+
+ // Flip flag to indicate the word iterator needs to have the text reset.
+ mUpdateWordIteratorText = true;
+
+ // Hide the controllers as soon as text is modified (typing, procedural...)
+ // We do not hide the span controllers, since they can be added when a new text is
+ // inserted into the text view (voice IME).
+ hideCursorControllers();
+ // Reset drag accelerator.
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.resetTouchOffsets();
+ }
+ stopTextActionMode();
+ }
+
+ private int getLastTapPosition() {
+ // No need to create the controller at that point, no last tap position saved
+ if (mSelectionModifierCursorController != null) {
+ int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
+ if (lastTapPosition >= 0) {
+ // Safety check, should not be possible.
+ if (lastTapPosition > mTextView.getText().length()) {
+ lastTapPosition = mTextView.getText().length();
+ }
+ return lastTapPosition;
+ }
+ }
+
+ return -1;
+ }
+
+ void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (hasWindowFocus) {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
+ refreshTextActionMode();
+ }
+ } else {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ if (mInputContentType != null) {
+ mInputContentType.enterDown = false;
+ }
+ // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
+ hideCursorAndSpanControllers();
+ stopTextActionModeWithPreservingSelection();
+ if (mSuggestionsPopupWindow != null) {
+ mSuggestionsPopupWindow.onParentLostFocus();
+ }
+
+ // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
+ ensureEndedBatchEdit();
+ }
+ }
+
+ private void updateTapState(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+ // Detect double tap and triple click.
+ if (((mTapState == TAP_STATE_FIRST_TAP)
+ || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
+ && (SystemClock.uptimeMillis() - mLastTouchUpTime)
+ <= ViewConfiguration.getDoubleTapTimeout()) {
+ if (mTapState == TAP_STATE_FIRST_TAP) {
+ mTapState = TAP_STATE_DOUBLE_TAP;
+ } else {
+ mTapState = TAP_STATE_TRIPLE_CLICK;
+ }
+ } else {
+ mTapState = TAP_STATE_FIRST_TAP;
+ }
+ }
+ if (action == MotionEvent.ACTION_UP) {
+ mLastTouchUpTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ private boolean shouldFilterOutTouchEvent(MotionEvent event) {
+ if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ return false;
+ }
+ final boolean primaryButtonStateChanged =
+ ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
+ final int action = event.getActionMasked();
+ if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
+ && !primaryButtonStateChanged) {
+ return true;
+ }
+ if (action == MotionEvent.ACTION_MOVE
+ && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
+ return true;
+ }
+ return false;
+ }
+
+ void onTouchEvent(MotionEvent event) {
+ final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
+ mLastButtonState = event.getButtonState();
+ if (filterOutEvent) {
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mDiscardNextActionUp = true;
+ }
+ return;
+ }
+ updateTapState(event);
+ updateFloatingToolbarVisibility(event);
+
+ if (hasSelectionController()) {
+ getSelectionController().onTouchEvent(event);
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ mShowSuggestionRunnable = null;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mLastUpPositionX = event.getX();
+ mLastUpPositionY = event.getY();
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mLastDownPositionX = event.getX();
+ mLastDownPositionY = event.getY();
+
+ // Reset this state; it will be re-set if super.onTouchEvent
+ // causes focus to move to the view.
+ mTouchFocusSelected = false;
+ mIgnoreActionUpEvent = false;
+ }
+ }
+
+ private void updateFloatingToolbarVisibility(MotionEvent event) {
+ if (mTextActionMode != null) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
+ break;
+ case MotionEvent.ACTION_UP: // fall through
+ case MotionEvent.ACTION_CANCEL:
+ showFloatingToolbar();
+ }
+ }
+ }
+
+ void hideFloatingToolbar(int duration) {
+ if (mTextActionMode != null) {
+ mTextView.removeCallbacks(mShowFloatingToolbar);
+ mTextActionMode.hide(duration);
+ }
+ }
+
+ private void showFloatingToolbar() {
+ if (mTextActionMode != null) {
+ // Delay "show" so it doesn't interfere with click confirmations
+ // or double-clicks that could "dismiss" the floating toolbar.
+ int delay = ViewConfiguration.getDoubleTapTimeout();
+ mTextView.postDelayed(mShowFloatingToolbar, delay);
+
+ // This classifies the text and most likely returns before the toolbar is actually
+ // shown. If not, it will update the toolbar with the result when classification
+ // returns. We would rather not wait for a long running classification process.
+ invalidateActionModeAsync();
+ }
+ }
+
+ public void beginBatchEdit() {
+ mInBatchEditControllers = true;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = ++ims.mBatchEditNesting;
+ if (nesting == 1) {
+ ims.mCursorChanged = false;
+ ims.mChangedDelta = 0;
+ if (ims.mContentChanged) {
+ // We already have a pending change from somewhere else,
+ // so turn this into a full update.
+ ims.mChangedStart = 0;
+ ims.mChangedEnd = mTextView.getText().length();
+ } else {
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mContentChanged = false;
+ }
+ mUndoInputFilter.beginBatchEdit();
+ mTextView.onBeginBatchEdit();
+ }
+ }
+ }
+
+ public void endBatchEdit() {
+ mInBatchEditControllers = false;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = --ims.mBatchEditNesting;
+ if (nesting == 0) {
+ finishBatchEdit(ims);
+ }
+ }
+ }
+
+ void ensureEndedBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting != 0) {
+ ims.mBatchEditNesting = 0;
+ finishBatchEdit(ims);
+ }
+ }
+
+ void finishBatchEdit(final InputMethodState ims) {
+ mTextView.onEndBatchEdit();
+ mUndoInputFilter.endBatchEdit();
+
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ mTextView.updateAfterEdit();
+ reportExtractedText();
+ } else if (ims.mCursorChanged) {
+ // Cheesy way to get us to report the current cursor location.
+ mTextView.invalidateCursor();
+ }
+ // sendUpdateSelection knows to avoid sending if the selection did
+ // not actually change.
+ sendUpdateSelection();
+
+ // Show drag handles if they were blocked by batch edit mode.
+ if (mTextActionMode != null) {
+ final CursorController cursorController = mTextView.hasSelection()
+ ? getSelectionController() : getInsertionController();
+ if (cursorController != null && !cursorController.isActive()
+ && !cursorController.isCursorBeingModified()) {
+ cursorController.show();
+ }
+ }
+ }
+
+ static final int EXTRACT_NOTHING = -2;
+ static final int EXTRACT_UNKNOWN = -1;
+
+ boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+ return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
+ EXTRACT_UNKNOWN, outText);
+ }
+
+ private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
+ int partialStartOffset, int partialEndOffset, int delta,
+ @Nullable ExtractedText outText) {
+ if (request == null || outText == null) {
+ return false;
+ }
+
+ final CharSequence content = mTextView.getText();
+ if (content == null) {
+ return false;
+ }
+
+ if (partialStartOffset != EXTRACT_NOTHING) {
+ final int N = content.length();
+ if (partialStartOffset < 0) {
+ outText.partialStartOffset = outText.partialEndOffset = -1;
+ partialStartOffset = 0;
+ partialEndOffset = N;
+ } else {
+ // Now use the delta to determine the actual amount of text
+ // we need.
+ partialEndOffset += delta;
+ // Adjust offsets to ensure we contain full spans.
+ if (content instanceof Spanned) {
+ Spanned spanned = (Spanned) content;
+ Object[] spans = spanned.getSpans(partialStartOffset,
+ partialEndOffset, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ int j = spanned.getSpanStart(spans[i]);
+ if (j < partialStartOffset) partialStartOffset = j;
+ j = spanned.getSpanEnd(spans[i]);
+ if (j > partialEndOffset) partialEndOffset = j;
+ }
+ }
+ outText.partialStartOffset = partialStartOffset;
+ outText.partialEndOffset = partialEndOffset - delta;
+
+ if (partialStartOffset > N) {
+ partialStartOffset = N;
+ } else if (partialStartOffset < 0) {
+ partialStartOffset = 0;
+ }
+ if (partialEndOffset > N) {
+ partialEndOffset = N;
+ } else if (partialEndOffset < 0) {
+ partialEndOffset = 0;
+ }
+ }
+ if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
+ outText.text = content.subSequence(partialStartOffset,
+ partialEndOffset);
+ } else {
+ outText.text = TextUtils.substring(content, partialStartOffset,
+ partialEndOffset);
+ }
+ } else {
+ outText.partialStartOffset = 0;
+ outText.partialEndOffset = 0;
+ outText.text = "";
+ }
+ outText.flags = 0;
+ if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
+ outText.flags |= ExtractedText.FLAG_SELECTING;
+ }
+ if (mTextView.isSingleLine()) {
+ outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
+ }
+ outText.startOffset = 0;
+ outText.selectionStart = mTextView.getSelectionStart();
+ outText.selectionEnd = mTextView.getSelectionEnd();
+ return true;
+ }
+
+ boolean reportExtractedText() {
+ final Editor.InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ final boolean contentChanged = ims.mContentChanged;
+ if (contentChanged || ims.mSelectionModeChanged) {
+ ims.mContentChanged = false;
+ ims.mSelectionModeChanged = false;
+ final ExtractedTextRequest req = ims.mExtractedTextRequest;
+ if (req != null) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (TextView.DEBUG_EXTRACT) {
+ Log.v(TextView.LOG_TAG, "Retrieving extracted start="
+ + ims.mChangedStart
+ + " end=" + ims.mChangedEnd
+ + " delta=" + ims.mChangedDelta);
+ }
+ if (ims.mChangedStart < 0 && !contentChanged) {
+ ims.mChangedStart = EXTRACT_NOTHING;
+ }
+ if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
+ ims.mChangedDelta, ims.mExtractedText)) {
+ if (TextView.DEBUG_EXTRACT) {
+ Log.v(TextView.LOG_TAG,
+ "Reporting extracted start="
+ + ims.mExtractedText.partialStartOffset
+ + " end=" + ims.mExtractedText.partialEndOffset
+ + ": " + ims.mExtractedText.text);
+ }
+
+ imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mChangedDelta = 0;
+ ims.mContentChanged = false;
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private void sendUpdateSelection() {
+ if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ if (null != imm) {
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+ int candStart = -1;
+ int candEnd = -1;
+ if (mTextView.getText() instanceof Spannable) {
+ final Spannable sp = (Spannable) mTextView.getText();
+ candStart = EditableInputConnection.getComposingSpanStart(sp);
+ candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ }
+ // InputMethodManager#updateSelection skips sending the message if
+ // none of the parameters have changed since the last time we called it.
+ imm.updateSelection(mTextView,
+ selectionStart, selectionEnd, candStart, candEnd);
+ }
+ }
+ }
+
+ void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
+ int cursorOffsetVertical) {
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting == 0) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (imm.isActive(mTextView)) {
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ // We are in extract mode and the content has changed
+ // in some way... just report complete new text to the
+ // input method.
+ reportExtractedText();
+ }
+ }
+ }
+ }
+
+ if (mCorrectionHighlighter != null) {
+ mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
+ }
+
+ if (highlight != null && selectionStart == selectionEnd && mCursorDrawable != null) {
+ drawCursor(canvas, cursorOffsetVertical);
+ // Rely on the drawable entirely, do not draw the cursor line.
+ // Has to be done after the IMM related code above which relies on the highlight.
+ highlight = null;
+ }
+
+ if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
+ drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
+ cursorOffsetVertical);
+ } else {
+ layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
+ }
+
+ if (mSelectionActionModeHelper != null) {
+ mSelectionActionModeHelper.onDraw(canvas);
+ }
+ }
+
+ private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+ Paint highlightPaint, int cursorOffsetVertical) {
+ final long lineRange = layout.getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+
+ layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
+ firstLine, lastLine);
+
+ if (layout instanceof DynamicLayout) {
+ if (mTextRenderNodes == null) {
+ mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
+ }
+
+ DynamicLayout dynamicLayout = (DynamicLayout) layout;
+ int[] blockEndLines = dynamicLayout.getBlockEndLines();
+ int[] blockIndices = dynamicLayout.getBlockIndices();
+ final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+ final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
+
+ final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
+ if (blockSet != null) {
+ for (int i = 0; i < blockSet.size(); i++) {
+ final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
+ if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+ && mTextRenderNodes[blockIndex] != null) {
+ mTextRenderNodes[blockIndex].needsToBeShifted = true;
+ }
+ }
+ }
+
+ int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
+ if (startBlock < 0) {
+ startBlock = -(startBlock + 1);
+ }
+ startBlock = Math.min(indexFirstChangedBlock, startBlock);
+
+ int startIndexToFindAvailableRenderNode = 0;
+ int lastIndex = numberOfBlocks;
+
+ for (int i = startBlock; i < numberOfBlocks; i++) {
+ final int blockIndex = blockIndices[i];
+ if (i >= indexFirstChangedBlock
+ && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+ && mTextRenderNodes[blockIndex] != null) {
+ mTextRenderNodes[blockIndex].needsToBeShifted = true;
+ }
+ if (blockEndLines[i] < firstLine) {
+ // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
+ // be redrawn after they get scrolled into drawing range.
+ continue;
+ }
+ startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
+ highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
+ blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
+ if (blockEndLines[i] >= lastLine) {
+ lastIndex = Math.max(indexFirstChangedBlock, i + 1);
+ break;
+ }
+ }
+ if (blockSet != null) {
+ for (int i = 0; i < blockSet.size(); i++) {
+ final int block = blockSet.valueAt(i);
+ final int blockIndex = dynamicLayout.getBlockIndex(block);
+ if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
+ || mTextRenderNodes[blockIndex] == null
+ || mTextRenderNodes[blockIndex].needsToBeShifted) {
+ startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
+ layout, highlight, highlightPaint, cursorOffsetVertical,
+ blockEndLines, blockIndices, block, numberOfBlocks,
+ startIndexToFindAvailableRenderNode);
+ }
+ }
+ }
+
+ dynamicLayout.setIndexFirstChangedBlock(lastIndex);
+ } else {
+ // Boring layout is used for empty and hint text
+ layout.drawText(canvas, firstLine, lastLine);
+ }
+ }
+
+ private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
+ Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
+ int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
+ int startIndexToFindAvailableRenderNode) {
+ final int blockEndLine = blockEndLines[blockInfoIndex];
+ int blockIndex = blockIndices[blockInfoIndex];
+
+ final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+ if (blockIsInvalid) {
+ blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+ startIndexToFindAvailableRenderNode);
+ // Note how dynamic layout's internal block indices get updated from Editor
+ blockIndices[blockInfoIndex] = blockIndex;
+ if (mTextRenderNodes[blockIndex] != null) {
+ mTextRenderNodes[blockIndex].isDirty = true;
+ }
+ startIndexToFindAvailableRenderNode = blockIndex + 1;
+ }
+
+ if (mTextRenderNodes[blockIndex] == null) {
+ mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
+ }
+
+ final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
+ RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
+ if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
+ final int blockBeginLine = blockInfoIndex == 0 ?
+ 0 : blockEndLines[blockInfoIndex - 1] + 1;
+ final int top = layout.getLineTop(blockBeginLine);
+ final int bottom = layout.getLineBottom(blockEndLine);
+ int left = 0;
+ int right = mTextView.getWidth();
+ if (mTextView.getHorizontallyScrolling()) {
+ float min = Float.MAX_VALUE;
+ float max = Float.MIN_VALUE;
+ for (int line = blockBeginLine; line <= blockEndLine; line++) {
+ min = Math.min(min, layout.getLineLeft(line));
+ max = Math.max(max, layout.getLineRight(line));
+ }
+ left = (int) min;
+ right = (int) (max + 0.5f);
+ }
+
+ // Rebuild display list if it is invalid
+ if (blockDisplayListIsInvalid) {
+ final DisplayListCanvas displayListCanvas = blockDisplayList.start(
+ right - left, bottom - top);
+ try {
+ // drawText is always relative to TextView's origin, this translation
+ // brings this range of text back to the top left corner of the viewport
+ displayListCanvas.translate(-left, -top);
+ layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
+ mTextRenderNodes[blockIndex].isDirty = false;
+ // No need to untranslate, previous context is popped after
+ // drawDisplayList
+ } finally {
+ blockDisplayList.end(displayListCanvas);
+ // Same as drawDisplayList below, handled by our TextView's parent
+ blockDisplayList.setClipToBounds(false);
+ }
+ }
+
+ // Valid display list only needs to update its drawing location.
+ blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
+ mTextRenderNodes[blockIndex].needsToBeShifted = false;
+ }
+ ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
+ return startIndexToFindAvailableRenderNode;
+ }
+
+ private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
+ int searchStartIndex) {
+ int length = mTextRenderNodes.length;
+ for (int i = searchStartIndex; i < length; i++) {
+ boolean blockIndexFound = false;
+ for (int j = 0; j < numberOfBlocks; j++) {
+ if (blockIndices[j] == i) {
+ blockIndexFound = true;
+ break;
+ }
+ }
+ if (blockIndexFound) continue;
+ return i;
+ }
+
+ // No available index found, the pool has to grow
+ mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
+ return length;
+ }
+
+ private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
+ final boolean translate = cursorOffsetVertical != 0;
+ if (translate) canvas.translate(0, cursorOffsetVertical);
+ if (mCursorDrawable != null) {
+ mCursorDrawable.draw(canvas);
+ }
+ if (translate) canvas.translate(0, -cursorOffsetVertical);
+ }
+
+ void invalidateHandlesAndActionMode() {
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.invalidateHandles();
+ }
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.invalidateHandle();
+ }
+ if (mTextActionMode != null) {
+ invalidateActionMode();
+ }
+ }
+
+ /**
+ * Invalidates all the sub-display lists that overlap the specified character range
+ */
+ void invalidateTextDisplayList(Layout layout, int start, int end) {
+ if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
+ final int firstLine = layout.getLineForOffset(start);
+ final int lastLine = layout.getLineForOffset(end);
+
+ DynamicLayout dynamicLayout = (DynamicLayout) layout;
+ int[] blockEndLines = dynamicLayout.getBlockEndLines();
+ int[] blockIndices = dynamicLayout.getBlockIndices();
+ final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+
+ int i = 0;
+ // Skip the blocks before firstLine
+ while (i < numberOfBlocks) {
+ if (blockEndLines[i] >= firstLine) break;
+ i++;
+ }
+
+ // Invalidate all subsequent blocks until lastLine is passed
+ while (i < numberOfBlocks) {
+ final int blockIndex = blockIndices[i];
+ if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
+ mTextRenderNodes[blockIndex].isDirty = true;
+ }
+ if (blockEndLines[i] >= lastLine) break;
+ i++;
+ }
+ }
+ }
+
+ void invalidateTextDisplayList() {
+ if (mTextRenderNodes != null) {
+ for (int i = 0; i < mTextRenderNodes.length; i++) {
+ if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
+ }
+ }
+ }
+
+ void updateCursorPosition() {
+ if (mTextView.mCursorDrawableRes == 0) {
+ mCursorDrawable = null;
+ return;
+ }
+
+ final Layout layout = mTextView.getLayout();
+ final int offset = mTextView.getSelectionStart();
+ final int line = layout.getLineForOffset(offset);
+ final int top = layout.getLineTop(line);
+ final int bottom = layout.getLineBottomWithoutSpacing(line);
+
+ final boolean clamped = layout.shouldClampCursor(line);
+ updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
+ }
+
+ void refreshTextActionMode() {
+ if (extractedTextModeWillBeStarted()) {
+ mRestartActionModeOnNextRefresh = false;
+ return;
+ }
+ final boolean hasSelection = mTextView.hasSelection();
+ final SelectionModifierCursorController selectionController = getSelectionController();
+ final InsertionPointCursorController insertionController = getInsertionController();
+ if ((selectionController != null && selectionController.isCursorBeingModified())
+ || (insertionController != null && insertionController.isCursorBeingModified())) {
+ // ActionMode should be managed by the currently active cursor controller.
+ mRestartActionModeOnNextRefresh = false;
+ return;
+ }
+ if (hasSelection) {
+ hideInsertionPointCursorController();
+ if (mTextActionMode == null) {
+ if (mRestartActionModeOnNextRefresh) {
+ // To avoid distraction, newly start action mode only when selection action
+ // mode is being restarted.
+ startSelectionActionModeAsync(false);
+ }
+ } else if (selectionController == null || !selectionController.isActive()) {
+ // Insertion action mode is active. Avoid dismissing the selection.
+ stopTextActionModeWithPreservingSelection();
+ startSelectionActionModeAsync(false);
+ } else {
+ mTextActionMode.invalidateContentRect();
+ }
+ } else {
+ // Insertion action mode is started only when insertion controller is explicitly
+ // activated.
+ if (insertionController == null || !insertionController.isActive()) {
+ stopTextActionMode();
+ } else if (mTextActionMode != null) {
+ mTextActionMode.invalidateContentRect();
+ }
+ }
+ mRestartActionModeOnNextRefresh = false;
+ }
+
+ /**
+ * Start an Insertion action mode.
+ */
+ void startInsertionActionMode() {
+ if (mInsertionActionModeRunnable != null) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+ if (extractedTextModeWillBeStarted()) {
+ return;
+ }
+ stopTextActionMode();
+
+ ActionMode.Callback actionModeCallback =
+ new TextActionModeCallback(false /* hasSelection */);
+ mTextActionMode = mTextView.startActionMode(
+ actionModeCallback, ActionMode.TYPE_FLOATING);
+ if (mTextActionMode != null && getInsertionController() != null) {
+ getInsertionController().show();
+ }
+ }
+
+ @NonNull
+ TextView getTextView() {
+ return mTextView;
+ }
+
+ @Nullable
+ ActionMode getTextActionMode() {
+ return mTextActionMode;
+ }
+
+ void setRestartActionModeOnNextRefresh(boolean value) {
+ mRestartActionModeOnNextRefresh = value;
+ }
+
+ /**
+ * Asynchronously starts a selection action mode using the TextClassifier.
+ */
+ void startSelectionActionModeAsync(boolean adjustSelection) {
+ getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
+ }
+
+ /**
+ * Asynchronously invalidates an action mode using the TextClassifier.
+ */
+ void invalidateActionModeAsync() {
+ getSelectionActionModeHelper().invalidateActionModeAsync();
+ }
+
+ /**
+ * Synchronously invalidates an action mode without the TextClassifier.
+ */
+ private void invalidateActionMode() {
+ if (mTextActionMode != null) {
+ mTextActionMode.invalidate();
+ }
+ }
+
+ private SelectionActionModeHelper getSelectionActionModeHelper() {
+ if (mSelectionActionModeHelper == null) {
+ mSelectionActionModeHelper = new SelectionActionModeHelper(this);
+ }
+ return mSelectionActionModeHelper;
+ }
+
+ /**
+ * If the TextView allows text selection, selects the current word when no existing selection
+ * was available and starts a drag.
+ *
+ * @return true if the drag was started.
+ */
+ private boolean selectCurrentWordAndStartDrag() {
+ if (mInsertionActionModeRunnable != null) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+ if (extractedTextModeWillBeStarted()) {
+ return false;
+ }
+ if (!checkField()) {
+ return false;
+ }
+ if (!mTextView.hasSelection() && !selectCurrentWord()) {
+ // No selection and cannot select a word.
+ return false;
+ }
+ stopTextActionModeWithPreservingSelection();
+ getSelectionController().enterDrag(
+ SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
+ return true;
+ }
+
+ /**
+ * Checks whether a selection can be performed on the current TextView.
+ *
+ * @return true if a selection can be performed
+ */
+ boolean checkField() {
+ if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
+ Log.w(TextView.LOG_TAG,
+ "TextView does not support text selection. Selection cancelled.");
+ return false;
+ }
+ return true;
+ }
+
+ boolean startSelectionActionModeInternal() {
+ if (extractedTextModeWillBeStarted()) {
+ return false;
+ }
+ if (mTextActionMode != null) {
+ // Text action mode is already started
+ invalidateActionMode();
+ return false;
+ }
+
+ if (!checkField() || !mTextView.hasSelection()) {
+ return false;
+ }
+
+ ActionMode.Callback actionModeCallback =
+ new TextActionModeCallback(true /* hasSelection */);
+ mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
+
+ final boolean selectionStarted = mTextActionMode != null;
+ if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
+ // Show the IME to be able to replace text, except when selecting non editable text.
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.showSoftInput(mTextView, 0, null);
+ }
+ }
+ return selectionStarted;
+ }
+
+ private boolean extractedTextModeWillBeStarted() {
+ if (!(mTextView.isInExtractedMode())) {
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ return imm != null && imm.isFullscreenMode();
+ }
+ return false;
+ }
+
+ /**
+ * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
+ * the current cursor position or selection range. This method is consistent with the
+ * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
+ */
+ private boolean shouldOfferToShowSuggestions() {
+ CharSequence text = mTextView.getText();
+ if (!(text instanceof Spannable)) return false;
+
+ final Spannable spannable = (Spannable) text;
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+ final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
+ SuggestionSpan.class);
+ if (suggestionSpans.length == 0) {
+ return false;
+ }
+ if (selectionStart == selectionEnd) {
+ // Spans overlap the cursor.
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ if (suggestionSpans[i].getSuggestions().length > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ int minSpanStart = mTextView.getText().length();
+ int maxSpanEnd = 0;
+ int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
+ int unionOfSpansCoveringSelectionStartEnd = 0;
+ boolean hasValidSuggestions = false;
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
+ final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
+ minSpanStart = Math.min(minSpanStart, spanStart);
+ maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
+ if (selectionStart < spanStart || selectionStart > spanEnd) {
+ // The span doesn't cover the current selection start point.
+ continue;
+ }
+ hasValidSuggestions =
+ hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
+ unionOfSpansCoveringSelectionStartStart =
+ Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
+ unionOfSpansCoveringSelectionStartEnd =
+ Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
+ }
+ if (!hasValidSuggestions) {
+ return false;
+ }
+ if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
+ // No spans cover the selection start point.
+ return false;
+ }
+ if (minSpanStart < unionOfSpansCoveringSelectionStartStart
+ || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
+ // There is a span that is not covered by the union. In this case, we soouldn't offer
+ // to show suggestions as it's confusing.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
+ * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
+ */
+ private boolean isCursorInsideEasyCorrectionSpan() {
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
+ mTextView.getSelectionEnd(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void onTouchUpEvent(MotionEvent event) {
+ if (getSelectionActionModeHelper().resetSelection(
+ getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
+ return;
+ }
+
+ boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
+ hideCursorAndSpanControllers();
+ stopTextActionMode();
+ CharSequence text = mTextView.getText();
+ if (!selectAllGotFocus && text.length() > 0) {
+ // Move cursor
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection((Spannable) text, offset);
+ if (mSpellChecker != null) {
+ // When the cursor moves, the word that was typed may need spell check
+ mSpellChecker.onSelectionChanged();
+ }
+
+ if (!extractedTextModeWillBeStarted()) {
+ if (isCursorInsideEasyCorrectionSpan()) {
+ // Cancel the single tap delayed runnable.
+ if (mInsertionActionModeRunnable != null) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+
+ mShowSuggestionRunnable = new Runnable() {
+ public void run() {
+ replace();
+ }
+ };
+ // removeCallbacks is performed on every touch
+ mTextView.postDelayed(mShowSuggestionRunnable,
+ ViewConfiguration.getDoubleTapTimeout());
+ } else if (hasInsertionController()) {
+ getInsertionController().show();
+ }
+ }
+ }
+ }
+
+ protected void stopTextActionMode() {
+ if (mTextActionMode != null) {
+ // This will hide the mSelectionModifierCursorController
+ mTextActionMode.finish();
+ }
+ }
+
+ private void stopTextActionModeWithPreservingSelection() {
+ if (mTextActionMode != null) {
+ mRestartActionModeOnNextRefresh = true;
+ }
+ mPreserveSelection = true;
+ stopTextActionMode();
+ mPreserveSelection = false;
+ }
+
+ /**
+ * @return True if this view supports insertion handles.
+ */
+ boolean hasInsertionController() {
+ return mInsertionControllerEnabled;
+ }
+
+ /**
+ * @return True if this view supports selection handles.
+ */
+ boolean hasSelectionController() {
+ return mSelectionControllerEnabled;
+ }
+
+ private InsertionPointCursorController getInsertionController() {
+ if (!mInsertionControllerEnabled) {
+ return null;
+ }
+
+ if (mInsertionPointCursorController == null) {
+ mInsertionPointCursorController = new InsertionPointCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+
+ return mInsertionPointCursorController;
+ }
+
+ @Nullable
+ SelectionModifierCursorController getSelectionController() {
+ if (!mSelectionControllerEnabled) {
+ return null;
+ }
+
+ if (mSelectionModifierCursorController == null) {
+ mSelectionModifierCursorController = new SelectionModifierCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+
+ return mSelectionModifierCursorController;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public Drawable getCursorDrawable() {
+ return mCursorDrawable;
+ }
+
+ private void updateCursorPosition(int top, int bottom, float horizontal) {
+ if (mCursorDrawable == null) {
+ mCursorDrawable = mTextView.getContext().getDrawable(
+ mTextView.mCursorDrawableRes);
+ }
+ final int left = clampHorizontalPosition(mCursorDrawable, horizontal);
+ final int width = mCursorDrawable.getIntrinsicWidth();
+ mCursorDrawable.setBounds(left, top - mTempRect.top, left + width,
+ bottom + mTempRect.bottom);
+ }
+
+ /**
+ * Return clamped position for the drawable. If the drawable is within the boundaries of the
+ * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
+ * the beginning or the end of the text then its drawable edge is aligned with left or right of
+ * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
+ * of the view.
+ *
+ * @param drawable Drawable. Can be null.
+ * @param horizontal Horizontal position for the drawable.
+ * @return The clamped horizontal position for the drawable.
+ */
+ private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
+ horizontal = Math.max(0.5f, horizontal - 0.5f);
+ if (mTempRect == null) mTempRect = new Rect();
+
+ int drawableWidth = 0;
+ if (drawable != null) {
+ drawable.getPadding(mTempRect);
+ drawableWidth = drawable.getIntrinsicWidth();
+ } else {
+ mTempRect.setEmpty();
+ }
+
+ int scrollX = mTextView.getScrollX();
+ float horizontalDiff = horizontal - scrollX;
+ int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
+ - mTextView.getCompoundPaddingRight();
+
+ final int left;
+ if (horizontalDiff >= (viewClippedWidth - 1f)) {
+ // at the rightmost position
+ left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
+ } else if (Math.abs(horizontalDiff) <= 1f
+ || (TextUtils.isEmpty(mTextView.getText())
+ && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
+ && horizontal <= 1f)) {
+ // at the leftmost position
+ left = scrollX - mTempRect.left;
+ } else {
+ left = (int) horizontal - mTempRect.left;
+ }
+ return left;
+ }
+
+ /**
+ * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+ * a dictionary) from the current input method, provided by it calling
+ * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
+ * implementation flashes the background of the corrected word to provide feedback to the user.
+ *
+ * @param info The auto correct info about the text that was corrected.
+ */
+ public void onCommitCorrection(CorrectionInfo info) {
+ if (mCorrectionHighlighter == null) {
+ mCorrectionHighlighter = new CorrectionHighlighter();
+ } else {
+ mCorrectionHighlighter.invalidate(false);
+ }
+
+ mCorrectionHighlighter.highlight(info);
+ mUndoInputFilter.freezeLastEdit();
+ }
+
+ void onScrollChanged() {
+ if (mPositionListener != null) {
+ mPositionListener.onScrollChanged();
+ }
+ if (mTextActionMode != null) {
+ mTextActionMode.invalidateContentRect();
+ }
+ }
+
+ /**
+ * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
+ */
+ private boolean shouldBlink() {
+ if (!isCursorVisible() || !mTextView.isFocused()) return false;
+
+ final int start = mTextView.getSelectionStart();
+ if (start < 0) return false;
+
+ final int end = mTextView.getSelectionEnd();
+ if (end < 0) return false;
+
+ return start == end;
+ }
+
+ void makeBlink() {
+ if (shouldBlink()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ if (mBlink == null) mBlink = new Blink();
+ mTextView.removeCallbacks(mBlink);
+ mTextView.postDelayed(mBlink, BLINK);
+ } else {
+ if (mBlink != null) mTextView.removeCallbacks(mBlink);
+ }
+ }
+
+ private class Blink implements Runnable {
+ private boolean mCancelled;
+
+ public void run() {
+ if (mCancelled) {
+ return;
+ }
+
+ mTextView.removeCallbacks(this);
+
+ if (shouldBlink()) {
+ if (mTextView.getLayout() != null) {
+ mTextView.invalidateCursorPath();
+ }
+
+ mTextView.postDelayed(this, BLINK);
+ }
+ }
+
+ void cancel() {
+ if (!mCancelled) {
+ mTextView.removeCallbacks(this);
+ mCancelled = true;
+ }
+ }
+
+ void uncancel() {
+ mCancelled = false;
+ }
+ }
+
+ private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
+ TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
+ com.android.internal.R.layout.text_drag_thumbnail, null);
+
+ if (shadowView == null) {
+ throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
+ }
+
+ if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+ final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
+ end = TextUtils.unpackRangeEndFromLong(range);
+ }
+ final CharSequence text = mTextView.getTransformedText(start, end);
+ shadowView.setText(text);
+ shadowView.setTextColor(mTextView.getTextColors());
+
+ shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
+ shadowView.setGravity(Gravity.CENTER);
+
+ shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ shadowView.measure(size, size);
+
+ shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
+ shadowView.invalidate();
+ return new DragShadowBuilder(shadowView);
+ }
+
+ private static class DragLocalState {
+ public TextView sourceTextView;
+ public int start, end;
+
+ public DragLocalState(TextView sourceTextView, int start, int end) {
+ this.sourceTextView = sourceTextView;
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ void onDrop(DragEvent event) {
+ SpannableStringBuilder content = new SpannableStringBuilder();
+
+ final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
+ if (permissions != null) {
+ permissions.takeTransient();
+ }
+
+ try {
+ ClipData clipData = event.getClipData();
+ final int itemCount = clipData.getItemCount();
+ for (int i = 0; i < itemCount; i++) {
+ Item item = clipData.getItemAt(i);
+ content.append(item.coerceToStyledText(mTextView.getContext()));
+ }
+ } finally {
+ if (permissions != null) {
+ permissions.release();
+ }
+ }
+
+ mTextView.beginBatchEdit();
+ mUndoInputFilter.freezeLastEdit();
+ try {
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+ Object localState = event.getLocalState();
+ DragLocalState dragLocalState = null;
+ if (localState instanceof DragLocalState) {
+ dragLocalState = (DragLocalState) localState;
+ }
+ boolean dragDropIntoItself = dragLocalState != null
+ && dragLocalState.sourceTextView == mTextView;
+
+ if (dragDropIntoItself) {
+ if (offset >= dragLocalState.start && offset < dragLocalState.end) {
+ // A drop inside the original selection discards the drop.
+ return;
+ }
+ }
+
+ final int originalLength = mTextView.getText().length();
+ int min = offset;
+ int max = offset;
+
+ Selection.setSelection((Spannable) mTextView.getText(), max);
+ mTextView.replaceText_internal(min, max, content);
+
+ if (dragDropIntoItself) {
+ int dragSourceStart = dragLocalState.start;
+ int dragSourceEnd = dragLocalState.end;
+ if (max <= dragSourceStart) {
+ // Inserting text before selection has shifted positions
+ final int shift = mTextView.getText().length() - originalLength;
+ dragSourceStart += shift;
+ dragSourceEnd += shift;
+ }
+
+ // Delete original selection
+ mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
+
+ // Make sure we do not leave two adjacent spaces.
+ final int prevCharIdx = Math.max(0, dragSourceStart - 1);
+ final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
+ if (nextCharIdx > prevCharIdx + 1) {
+ CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
+ if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
+ mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
+ }
+ }
+ }
+ } finally {
+ mTextView.endBatchEdit();
+ mUndoInputFilter.freezeLastEdit();
+ }
+ }
+
+ public void addSpanWatchers(Spannable text) {
+ final int textLength = text.length();
+
+ if (mKeyListener != null) {
+ text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ if (mSpanController == null) {
+ mSpanController = new SpanController();
+ }
+ text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ void setContextMenuAnchor(float x, float y) {
+ mContextMenuAnchorX = x;
+ mContextMenuAnchorY = y;
+ }
+
+ void onCreateContextMenu(ContextMenu menu) {
+ if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
+ || Float.isNaN(mContextMenuAnchorY)) {
+ return;
+ }
+ final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
+ if (offset == -1) {
+ return;
+ }
+
+ stopTextActionModeWithPreservingSelection();
+ if (mTextView.canSelectText()) {
+ final boolean isOnSelection = mTextView.hasSelection()
+ && offset >= mTextView.getSelectionStart()
+ && offset <= mTextView.getSelectionEnd();
+ if (!isOnSelection) {
+ // Right clicked position is not on the selection. Remove the selection and move the
+ // cursor to the right clicked position.
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ stopTextActionMode();
+ }
+ }
+
+ if (shouldOfferToShowSuggestions()) {
+ final SuggestionInfo[] suggestionInfoArray =
+ new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
+ for (int i = 0; i < suggestionInfoArray.length; i++) {
+ suggestionInfoArray[i] = new SuggestionInfo();
+ }
+ final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
+ com.android.internal.R.string.replace);
+ final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
+ for (int i = 0; i < numItems; i++) {
+ final SuggestionInfo info = suggestionInfoArray[i];
+ subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ replaceWithSuggestion(info);
+ return true;
+ }
+ });
+ }
+ }
+
+ menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
+ com.android.internal.R.string.undo)
+ .setAlphabeticShortcut('z')
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+ .setEnabled(mTextView.canUndo());
+ menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
+ com.android.internal.R.string.redo)
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+ .setEnabled(mTextView.canRedo());
+
+ menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+ com.android.internal.R.string.cut)
+ .setAlphabeticShortcut('x')
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+ .setEnabled(mTextView.canCut());
+ menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+ com.android.internal.R.string.copy)
+ .setAlphabeticShortcut('c')
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+ .setEnabled(mTextView.canCopy());
+ menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+ com.android.internal.R.string.paste)
+ .setAlphabeticShortcut('v')
+ .setEnabled(mTextView.canPaste())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+ menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ com.android.internal.R.string.paste_as_plain_text)
+ .setEnabled(mTextView.canPasteAsPlainText())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+ menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+ com.android.internal.R.string.share)
+ .setEnabled(mTextView.canShare())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+ menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+ com.android.internal.R.string.selectAll)
+ .setAlphabeticShortcut('a')
+ .setEnabled(mTextView.canSelectAllText())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+ menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+ android.R.string.autofill)
+ .setEnabled(mTextView.canRequestAutofill())
+ .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+
+ mPreserveSelection = true;
+ }
+
+ @Nullable
+ private SuggestionSpan findEquivalentSuggestionSpan(
+ @NonNull SuggestionSpanInfo suggestionSpanInfo) {
+ final Editable editable = (Editable) mTextView.getText();
+ if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
+ // Exactly same span is found.
+ return suggestionSpanInfo.mSuggestionSpan;
+ }
+ // Suggestion span couldn't be found. Try to find a suggestion span that has the same
+ // contents.
+ final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
+ suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
+ for (final SuggestionSpan suggestionSpan : suggestionSpans) {
+ final int start = editable.getSpanStart(suggestionSpan);
+ if (start != suggestionSpanInfo.mSpanStart) {
+ continue;
+ }
+ final int end = editable.getSpanEnd(suggestionSpan);
+ if (end != suggestionSpanInfo.mSpanEnd) {
+ continue;
+ }
+ if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
+ return suggestionSpan;
+ }
+ }
+ return null;
+ }
+
+ private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
+ final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
+ suggestionInfo.mSuggestionSpanInfo);
+ if (targetSuggestionSpan == null) {
+ // Span has been removed
+ return;
+ }
+ final Editable editable = (Editable) mTextView.getText();
+ final int spanStart = editable.getSpanStart(targetSuggestionSpan);
+ final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
+ if (spanStart < 0 || spanEnd <= spanStart) {
+ // Span has been removed
+ return;
+ }
+
+ final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
+ // SuggestionSpans are removed by replace: save them before
+ SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
+ SuggestionSpan.class);
+ final int length = suggestionSpans.length;
+ int[] suggestionSpansStarts = new int[length];
+ int[] suggestionSpansEnds = new int[length];
+ int[] suggestionSpansFlags = new int[length];
+ for (int i = 0; i < length; i++) {
+ final SuggestionSpan suggestionSpan = suggestionSpans[i];
+ suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
+ suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
+ suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
+
+ // Remove potential misspelled flags
+ int suggestionSpanFlags = suggestionSpan.getFlags();
+ if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+ suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
+ suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+ suggestionSpan.setFlags(suggestionSpanFlags);
+ }
+ }
+
+ // Notify source IME of the suggestion pick. Do this before swapping texts.
+ targetSuggestionSpan.notifySelection(
+ mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
+
+ // Swap text content between actual text and Suggestion span
+ final int suggestionStart = suggestionInfo.mSuggestionStart;
+ final int suggestionEnd = suggestionInfo.mSuggestionEnd;
+ final String suggestion = suggestionInfo.mText.subSequence(
+ suggestionStart, suggestionEnd).toString();
+ mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
+
+ String[] suggestions = targetSuggestionSpan.getSuggestions();
+ suggestions[suggestionInfo.mSuggestionIndex] = originalText;
+
+ // Restore previous SuggestionSpans
+ final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
+ for (int i = 0; i < length; i++) {
+ // Only spans that include the modified region make sense after replacement
+ // Spans partially included in the replaced region are removed, there is no
+ // way to assign them a valid range after replacement
+ if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
+ mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
+ suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
+ }
+ }
+ // Move cursor at the end of the replaced word
+ final int newCursorPosition = spanEnd + lengthDelta;
+ mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
+ }
+
+ private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
+ new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
+ return true;
+ }
+ return mTextView.onTextContextMenuItem(item.getItemId());
+ }
+ };
+
+ /**
+ * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
+ * pop-up should be displayed.
+ * Also monitors {@link Selection} to call back to the attached input method.
+ */
+ private class SpanController implements SpanWatcher {
+
+ private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
+
+ private EasyEditPopupWindow mPopupWindow;
+
+ private Runnable mHidePopup;
+
+ // This function is pure but inner classes can't have static functions
+ private boolean isNonIntermediateSelectionSpan(final Spannable text,
+ final Object span) {
+ return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
+ && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
+ }
+
+ @Override
+ public void onSpanAdded(Spannable text, Object span, int start, int end) {
+ if (isNonIntermediateSelectionSpan(text, span)) {
+ sendUpdateSelection();
+ } else if (span instanceof EasyEditSpan) {
+ if (mPopupWindow == null) {
+ mPopupWindow = new EasyEditPopupWindow();
+ mHidePopup = new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ };
+ }
+
+ // Make sure there is only at most one EasyEditSpan in the text
+ if (mPopupWindow.mEasyEditSpan != null) {
+ mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
+ }
+
+ mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
+ mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
+ @Override
+ public void onDeleteClick(EasyEditSpan span) {
+ Editable editable = (Editable) mTextView.getText();
+ int start = editable.getSpanStart(span);
+ int end = editable.getSpanEnd(span);
+ if (start >= 0 && end >= 0) {
+ sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
+ mTextView.deleteText_internal(start, end);
+ }
+ editable.removeSpan(span);
+ }
+ });
+
+ if (mTextView.getWindowVisibility() != View.VISIBLE) {
+ // The window is not visible yet, ignore the text change.
+ return;
+ }
+
+ if (mTextView.getLayout() == null) {
+ // The view has not been laid out yet, ignore the text change
+ return;
+ }
+
+ if (extractedTextModeWillBeStarted()) {
+ // The input is in extract mode. Do not handle the easy edit in
+ // the original TextView, as the ExtractEditText will do
+ return;
+ }
+
+ mPopupWindow.show();
+ mTextView.removeCallbacks(mHidePopup);
+ mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
+ }
+ }
+
+ @Override
+ public void onSpanRemoved(Spannable text, Object span, int start, int end) {
+ if (isNonIntermediateSelectionSpan(text, span)) {
+ sendUpdateSelection();
+ } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
+ hide();
+ }
+ }
+
+ @Override
+ public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
+ int newStart, int newEnd) {
+ if (isNonIntermediateSelectionSpan(text, span)) {
+ sendUpdateSelection();
+ } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
+ EasyEditSpan easyEditSpan = (EasyEditSpan) span;
+ sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
+ text.removeSpan(easyEditSpan);
+ }
+ }
+
+ public void hide() {
+ if (mPopupWindow != null) {
+ mPopupWindow.hide();
+ mTextView.removeCallbacks(mHidePopup);
+ }
+ }
+
+ private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
+ try {
+ PendingIntent pendingIntent = span.getPendingIntent();
+ if (pendingIntent != null) {
+ Intent intent = new Intent();
+ intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
+ pendingIntent.send(mTextView.getContext(), 0, intent);
+ }
+ } catch (CanceledException e) {
+ // This should not happen, as we should try to send the intent only once.
+ Log.w(TAG, "PendingIntent for notification cannot be sent", e);
+ }
+ }
+ }
+
+ /**
+ * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
+ */
+ private interface EasyEditDeleteListener {
+
+ /**
+ * Clicks the delete pop-up.
+ */
+ void onDeleteClick(EasyEditSpan span);
+ }
+
+ /**
+ * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
+ * by {@link SpanController}.
+ */
+ private class EasyEditPopupWindow extends PinnedPopupWindow
+ implements OnClickListener {
+ private static final int POPUP_TEXT_LAYOUT =
+ com.android.internal.R.layout.text_edit_action_popup_text;
+ private TextView mDeleteTextView;
+ private EasyEditSpan mEasyEditSpan;
+ private EasyEditDeleteListener mOnDeleteListener;
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setClippingEnabled(true);
+ }
+
+ @Override
+ protected void initContentView() {
+ LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ mContentView = linearLayout;
+ mContentView.setBackgroundResource(
+ com.android.internal.R.drawable.text_edit_side_paste_window);
+
+ LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+ mDeleteTextView.setLayoutParams(wrapContent);
+ mDeleteTextView.setText(com.android.internal.R.string.delete);
+ mDeleteTextView.setOnClickListener(this);
+ mContentView.addView(mDeleteTextView);
+ }
+
+ public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
+ mEasyEditSpan = easyEditSpan;
+ }
+
+ private void setOnDeleteListener(EasyEditDeleteListener listener) {
+ mOnDeleteListener = listener;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mDeleteTextView
+ && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
+ && mOnDeleteListener != null) {
+ mOnDeleteListener.onDeleteClick(mEasyEditSpan);
+ }
+ }
+
+ @Override
+ public void hide() {
+ if (mEasyEditSpan != null) {
+ mEasyEditSpan.setDeleteEnabled(false);
+ }
+ mOnDeleteListener = null;
+ super.hide();
+ }
+
+ @Override
+ protected int getTextOffset() {
+ // Place the pop-up at the end of the span
+ Editable editable = (Editable) mTextView.getText();
+ return editable.getSpanEnd(mEasyEditSpan);
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ final Layout layout = mTextView.getLayout();
+ return layout.getLineBottomWithoutSpacing(line);
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ // As we display the pop-up below the span, no vertical clipping is required.
+ return positionY;
+ }
+ }
+
+ private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+ // 3 handles
+ // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
+ // 1 CursorAnchorInfoNotifier
+ private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
+ private TextViewPositionListener[] mPositionListeners =
+ new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mPositionHasChanged = true;
+ // Absolute position of the TextView with respect to its parent window
+ private int mPositionX, mPositionY;
+ private int mPositionXOnScreen, mPositionYOnScreen;
+ private int mNumberOfListeners;
+ private boolean mScrollHasChanged;
+ final int[] mTempCoords = new int[2];
+
+ public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
+ if (mNumberOfListeners == 0) {
+ updatePosition();
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.addOnPreDrawListener(this);
+ }
+
+ int emptySlotIndex = -1;
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ TextViewPositionListener listener = mPositionListeners[i];
+ if (listener == positionListener) {
+ return;
+ } else if (emptySlotIndex < 0 && listener == null) {
+ emptySlotIndex = i;
+ }
+ }
+
+ mPositionListeners[emptySlotIndex] = positionListener;
+ mCanMove[emptySlotIndex] = canMove;
+ mNumberOfListeners++;
+ }
+
+ public void removeSubscriber(TextViewPositionListener positionListener) {
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionListeners[i] == positionListener) {
+ mPositionListeners[i] = null;
+ mNumberOfListeners--;
+ break;
+ }
+ }
+
+ if (mNumberOfListeners == 0) {
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.removeOnPreDrawListener(this);
+ }
+ }
+
+ public int getPositionX() {
+ return mPositionX;
+ }
+
+ public int getPositionY() {
+ return mPositionY;
+ }
+
+ public int getPositionXOnScreen() {
+ return mPositionXOnScreen;
+ }
+
+ public int getPositionYOnScreen() {
+ return mPositionYOnScreen;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ updatePosition();
+
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
+ TextViewPositionListener positionListener = mPositionListeners[i];
+ if (positionListener != null) {
+ positionListener.updatePosition(mPositionX, mPositionY,
+ mPositionHasChanged, mScrollHasChanged);
+ }
+ }
+ }
+
+ mScrollHasChanged = false;
+ return true;
+ }
+
+ private void updatePosition() {
+ mTextView.getLocationInWindow(mTempCoords);
+
+ mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+ mPositionX = mTempCoords[0];
+ mPositionY = mTempCoords[1];
+
+ mTextView.getLocationOnScreen(mTempCoords);
+
+ mPositionXOnScreen = mTempCoords[0];
+ mPositionYOnScreen = mTempCoords[1];
+ }
+
+ public void onScrollChanged() {
+ mScrollHasChanged = true;
+ }
+ }
+
+ private abstract class PinnedPopupWindow implements TextViewPositionListener {
+ protected PopupWindow mPopupWindow;
+ protected ViewGroup mContentView;
+ int mPositionX, mPositionY;
+ int mClippingLimitLeft, mClippingLimitRight;
+
+ protected abstract void createPopupWindow();
+ protected abstract void initContentView();
+ protected abstract int getTextOffset();
+ protected abstract int getVerticalLocalPosition(int line);
+ protected abstract int clipVertically(int positionY);
+ protected void setUp() {
+ }
+
+ public PinnedPopupWindow() {
+ // Due to calling subclass methods in base constructor, subclass constructor is not
+ // called before subclass methods, e.g. createPopupWindow or initContentView. To give
+ // a chance to initialize subclasses, call setUp() method here.
+ // TODO: It is good to extract non trivial initialization code from constructor.
+ setUp();
+
+ createPopupWindow();
+
+ mPopupWindow.setWindowLayoutType(
+ WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+ mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ initContentView();
+
+ LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContentView.setLayoutParams(wrapContent);
+
+ mPopupWindow.setContentView(mContentView);
+ }
+
+ public void show() {
+ getPositionListener().addSubscriber(this, false /* offset is fixed */);
+
+ computeLocalPosition();
+
+ final PositionListener positionListener = getPositionListener();
+ updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+ }
+
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
+ View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
+ View.MeasureSpec.AT_MOST));
+ }
+
+ /* The popup window will be horizontally centered on the getTextOffset() and vertically
+ * positioned according to viewportToContentHorizontalOffset.
+ *
+ * This method assumes that mContentView has properly been measured from its content. */
+ private void computeLocalPosition() {
+ measureContent();
+ final int width = mContentView.getMeasuredWidth();
+ final int offset = getTextOffset();
+ mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+
+ final int line = mTextView.getLayout().getLineForOffset(offset);
+ mPositionY = getVerticalLocalPosition(line);
+ mPositionY += mTextView.viewportToContentVerticalOffset();
+ }
+
+ private void updatePosition(int parentPositionX, int parentPositionY) {
+ int positionX = parentPositionX + mPositionX;
+ int positionY = parentPositionY + mPositionY;
+
+ positionY = clipVertically(positionY);
+
+ // Horizontal clipping
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ final int width = mContentView.getMeasuredWidth();
+ positionX = Math.min(
+ displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
+ positionX = Math.max(-mClippingLimitLeft, positionX);
+
+ if (isShowing()) {
+ mPopupWindow.update(positionX, positionY, -1, -1);
+ } else {
+ mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+ positionX, positionY);
+ }
+ }
+
+ public void hide() {
+ if (!isShowing()) {
+ return;
+ }
+ mPopupWindow.dismiss();
+ getPositionListener().removeSubscriber(this);
+ }
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ // Either parentPositionChanged or parentScrolled is true, check if still visible
+ if (isShowing() && isOffsetVisible(getTextOffset())) {
+ if (parentScrolled) computeLocalPosition();
+ updatePosition(parentPositionX, parentPositionY);
+ } else {
+ hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return mPopupWindow.isShowing();
+ }
+ }
+
+ private static final class SuggestionInfo {
+ // Range of actual suggestion within mText
+ int mSuggestionStart, mSuggestionEnd;
+
+ // The SuggestionSpan that this TextView represents
+ final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
+
+ // The index of this suggestion inside suggestionSpan
+ int mSuggestionIndex;
+
+ final SpannableStringBuilder mText = new SpannableStringBuilder();
+
+ void clear() {
+ mSuggestionSpanInfo.clear();
+ mText.clear();
+ }
+
+ // Utility method to set attributes about a SuggestionSpan.
+ void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
+ mSuggestionSpanInfo.mSuggestionSpan = span;
+ mSuggestionSpanInfo.mSpanStart = spanStart;
+ mSuggestionSpanInfo.mSpanEnd = spanEnd;
+ }
+ }
+
+ private static final class SuggestionSpanInfo {
+ // The SuggestionSpan;
+ @Nullable
+ SuggestionSpan mSuggestionSpan;
+
+ // The SuggestionSpan start position
+ int mSpanStart;
+
+ // The SuggestionSpan end position
+ int mSpanEnd;
+
+ void clear() {
+ mSuggestionSpan = null;
+ }
+ }
+
+ private class SuggestionHelper {
+ private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
+ new SuggestionSpanComparator();
+ private final HashMap<SuggestionSpan, Integer> mSpansLengths =
+ new HashMap<SuggestionSpan, Integer>();
+
+ private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
+ public int compare(SuggestionSpan span1, SuggestionSpan span2) {
+ final int flag1 = span1.getFlags();
+ final int flag2 = span2.getFlags();
+ if (flag1 != flag2) {
+ // The order here should match what is used in updateDrawState
+ final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+ final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+ final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+ final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+ if (easy1 && !misspelled1) return -1;
+ if (easy2 && !misspelled2) return 1;
+ if (misspelled1) return -1;
+ if (misspelled2) return 1;
+ }
+
+ return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
+ }
+ }
+
+ /**
+ * Returns the suggestion spans that cover the current cursor position. The suggestion
+ * spans are sorted according to the length of text that they are attached to.
+ */
+ private SuggestionSpan[] getSortedSuggestionSpans() {
+ int pos = mTextView.getSelectionStart();
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
+
+ mSpansLengths.clear();
+ for (SuggestionSpan suggestionSpan : suggestionSpans) {
+ int start = spannable.getSpanStart(suggestionSpan);
+ int end = spannable.getSpanEnd(suggestionSpan);
+ mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
+ }
+
+ // The suggestions are sorted according to their types (easy correction first, then
+ // misspelled) and to the length of the text that they cover (shorter first).
+ Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
+ mSpansLengths.clear();
+
+ return suggestionSpans;
+ }
+
+ /**
+ * Gets the SuggestionInfo list that contains suggestion information at the current cursor
+ * position.
+ *
+ * @param suggestionInfos SuggestionInfo array the results will be set.
+ * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
+ * @return the number of suggestions actually fetched.
+ */
+ public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
+ @Nullable SuggestionSpanInfo misspelledSpanInfo) {
+ final Spannable spannable = (Spannable) mTextView.getText();
+ final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
+ final int nbSpans = suggestionSpans.length;
+ if (nbSpans == 0) return 0;
+
+ int numberOfSuggestions = 0;
+ for (final SuggestionSpan suggestionSpan : suggestionSpans) {
+ final int spanStart = spannable.getSpanStart(suggestionSpan);
+ final int spanEnd = spannable.getSpanEnd(suggestionSpan);
+
+ if (misspelledSpanInfo != null
+ && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+ misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
+ misspelledSpanInfo.mSpanStart = spanStart;
+ misspelledSpanInfo.mSpanEnd = spanEnd;
+ }
+
+ final String[] suggestions = suggestionSpan.getSuggestions();
+ final int nbSuggestions = suggestions.length;
+ suggestionLoop:
+ for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
+ final String suggestion = suggestions[suggestionIndex];
+ for (int i = 0; i < numberOfSuggestions; i++) {
+ final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
+ if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
+ final int otherSpanStart =
+ otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
+ final int otherSpanEnd =
+ otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
+ if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
+ continue suggestionLoop;
+ }
+ }
+ }
+
+ SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
+ suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
+ suggestionInfo.mSuggestionIndex = suggestionIndex;
+ suggestionInfo.mSuggestionStart = 0;
+ suggestionInfo.mSuggestionEnd = suggestion.length();
+ suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
+ numberOfSuggestions++;
+ if (numberOfSuggestions >= suggestionInfos.length) {
+ return numberOfSuggestions;
+ }
+ }
+ }
+ return numberOfSuggestions;
+ }
+ }
+
+ @VisibleForTesting
+ public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
+ private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
+
+ // Key of intent extras for inserting new word into user dictionary.
+ private static final String USER_DICTIONARY_EXTRA_WORD = "word";
+ private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
+
+ private SuggestionInfo[] mSuggestionInfos;
+ private int mNumberOfSuggestions;
+ private boolean mCursorWasVisibleBeforeSuggestions;
+ private boolean mIsShowingUp = false;
+ private SuggestionAdapter mSuggestionsAdapter;
+ private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
+ private TextView mAddToDictionaryButton;
+ private TextView mDeleteButton;
+ private ListView mSuggestionListView;
+ private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
+ private int mContainerMarginWidth;
+ private int mContainerMarginTop;
+ private LinearLayout mContainerView;
+ private Context mContext; // TODO: Make mContext final.
+
+ private class CustomPopupWindow extends PopupWindow {
+
+ @Override
+ public void dismiss() {
+ if (!isShowing()) {
+ return;
+ }
+ super.dismiss();
+ getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
+
+ // Safe cast since show() checks that mTextView.getText() is an Editable
+ ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
+
+ mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
+ if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
+ getInsertionController().show();
+ }
+ }
+ }
+
+ public SuggestionsPopupWindow() {
+ mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+ }
+
+ @Override
+ protected void setUp() {
+ mContext = applyDefaultTheme(mTextView.getContext());
+ mHighlightSpan = new TextAppearanceSpan(mContext,
+ mTextView.mTextEditSuggestionHighlightStyle);
+ }
+
+ private Context applyDefaultTheme(Context originalContext) {
+ TypedArray a = originalContext.obtainStyledAttributes(
+ new int[]{com.android.internal.R.attr.isLightTheme});
+ boolean isLightTheme = a.getBoolean(0, true);
+ int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
+ : R.style.ThemeOverlay_Material_Dark;
+ a.recycle();
+ return new ContextThemeWrapper(originalContext, themeId);
+ }
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new CustomPopupWindow();
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ mPopupWindow.setFocusable(true);
+ mPopupWindow.setClippingEnabled(false);
+ }
+
+ @Override
+ protected void initContentView() {
+ final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mContentView = (ViewGroup) inflater.inflate(
+ mTextView.mTextEditSuggestionContainerLayout, null);
+
+ mContainerView = (LinearLayout) mContentView.findViewById(
+ com.android.internal.R.id.suggestionWindowContainer);
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
+ mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
+ mContainerMarginTop = lp.topMargin;
+ mClippingLimitLeft = lp.leftMargin;
+ mClippingLimitRight = lp.rightMargin;
+
+ mSuggestionListView = (ListView) mContentView.findViewById(
+ com.android.internal.R.id.suggestionContainer);
+
+ mSuggestionsAdapter = new SuggestionAdapter();
+ mSuggestionListView.setAdapter(mSuggestionsAdapter);
+ mSuggestionListView.setOnItemClickListener(this);
+
+ // Inflate the suggestion items once and for all.
+ mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
+ for (int i = 0; i < mSuggestionInfos.length; i++) {
+ mSuggestionInfos[i] = new SuggestionInfo();
+ }
+
+ mAddToDictionaryButton = (TextView) mContentView.findViewById(
+ com.android.internal.R.id.addToDictionaryButton);
+ mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ final SuggestionSpan misspelledSpan =
+ findEquivalentSuggestionSpan(mMisspelledSpanInfo);
+ if (misspelledSpan == null) {
+ // Span has been removed.
+ return;
+ }
+ final Editable editable = (Editable) mTextView.getText();
+ final int spanStart = editable.getSpanStart(misspelledSpan);
+ final int spanEnd = editable.getSpanEnd(misspelledSpan);
+ if (spanStart < 0 || spanEnd <= spanStart) {
+ return;
+ }
+ final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
+
+ final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
+ intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
+ intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
+ mTextView.getTextServicesLocale().toString());
+ intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+ mTextView.getContext().startActivity(intent);
+ // There is no way to know if the word was indeed added. Re-check.
+ // TODO The ExtractEditText should remove the span in the original text instead
+ editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
+ Selection.setSelection(editable, spanEnd);
+ updateSpellCheckSpans(spanStart, spanEnd, false);
+ hideWithCleanUp();
+ }
+ });
+
+ mDeleteButton = (TextView) mContentView.findViewById(
+ com.android.internal.R.id.deleteButton);
+ mDeleteButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ final Editable editable = (Editable) mTextView.getText();
+
+ final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
+ int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
+ if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
+ // Do not leave two adjacent spaces after deletion, or one at beginning of
+ // text
+ if (spanUnionEnd < editable.length()
+ && Character.isSpaceChar(editable.charAt(spanUnionEnd))
+ && (spanUnionStart == 0
+ || Character.isSpaceChar(
+ editable.charAt(spanUnionStart - 1)))) {
+ spanUnionEnd = spanUnionEnd + 1;
+ }
+ mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
+ }
+ hideWithCleanUp();
+ }
+ });
+
+ }
+
+ public boolean isShowingUp() {
+ return mIsShowingUp;
+ }
+
+ public void onParentLostFocus() {
+ mIsShowingUp = false;
+ }
+
+ private class SuggestionAdapter extends BaseAdapter {
+ private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ @Override
+ public int getCount() {
+ return mNumberOfSuggestions;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mSuggestionInfos[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView textView = (TextView) convertView;
+
+ if (textView == null) {
+ textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
+ parent, false);
+ }
+
+ final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+ textView.setText(suggestionInfo.mText);
+ return textView;
+ }
+ }
+
+ @VisibleForTesting
+ public ViewGroup getContentViewForTesting() {
+ return mContentView;
+ }
+
+ @Override
+ public void show() {
+ if (!(mTextView.getText() instanceof Editable)) return;
+ if (extractedTextModeWillBeStarted()) {
+ return;
+ }
+
+ if (updateSuggestions()) {
+ mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+ mTextView.setCursorVisible(false);
+ mIsShowingUp = true;
+ super.show();
+ }
+ }
+
+ @Override
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
+ final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
+
+ int width = 0;
+ View view = null;
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ view = mSuggestionsAdapter.getView(i, view, mContentView);
+ view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+ view.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, view.getMeasuredWidth());
+ }
+
+ if (mAddToDictionaryButton.getVisibility() != View.GONE) {
+ mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
+ }
+
+ mDeleteButton.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, mDeleteButton.getMeasuredWidth());
+
+ width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
+ + mContainerMarginWidth;
+
+ // Enforce the width based on actual text widths
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ verticalMeasure);
+
+ Drawable popupBackground = mPopupWindow.getBackground();
+ if (popupBackground != null) {
+ if (mTempRect == null) mTempRect = new Rect();
+ popupBackground.getPadding(mTempRect);
+ width += mTempRect.left + mTempRect.right;
+ }
+ mPopupWindow.setWidth(width);
+ }
+
+ @Override
+ protected int getTextOffset() {
+ return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ final Layout layout = mTextView.getLayout();
+ return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ final int height = mContentView.getMeasuredHeight();
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ return Math.min(positionY, displayMetrics.heightPixels - height);
+ }
+
+ private void hideWithCleanUp() {
+ for (final SuggestionInfo info : mSuggestionInfos) {
+ info.clear();
+ }
+ mMisspelledSpanInfo.clear();
+ hide();
+ }
+
+ private boolean updateSuggestions() {
+ Spannable spannable = (Spannable) mTextView.getText();
+ mNumberOfSuggestions =
+ mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
+ if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
+ return false;
+ }
+
+ int spanUnionStart = mTextView.getText().length();
+ int spanUnionEnd = 0;
+
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
+ spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
+ spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
+ }
+ if (mMisspelledSpanInfo.mSuggestionSpan != null) {
+ spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
+ spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
+ }
+
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
+ }
+
+ // Make "Add to dictionary" item visible if there is a span with the misspelled flag
+ int addToDictionaryButtonVisibility = View.GONE;
+ if (mMisspelledSpanInfo.mSuggestionSpan != null) {
+ if (mMisspelledSpanInfo.mSpanStart >= 0
+ && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
+ addToDictionaryButtonVisibility = View.VISIBLE;
+ }
+ }
+ mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
+
+ if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
+ final int underlineColor;
+ if (mNumberOfSuggestions != 0) {
+ underlineColor =
+ mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
+ } else {
+ underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
+ }
+
+ if (underlineColor == 0) {
+ // Fallback on the default highlight color when the first span does not provide one
+ mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
+ } else {
+ final float BACKGROUND_TRANSPARENCY = 0.4f;
+ final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
+ mSuggestionRangeSpan.setBackgroundColor(
+ (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
+ }
+ spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mSuggestionsAdapter.notifyDataSetChanged();
+ return true;
+ }
+
+ private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
+ int unionEnd) {
+ final Spannable text = (Spannable) mTextView.getText();
+ final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
+ final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
+
+ // Adjust the start/end of the suggestion span
+ suggestionInfo.mSuggestionStart = spanStart - unionStart;
+ suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
+ + suggestionInfo.mText.length();
+
+ suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ // Add the text before and after the span.
+ final String textAsString = text.toString();
+ suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
+ suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+ replaceWithSuggestion(suggestionInfo);
+ hideWithCleanUp();
+ }
+ }
+
+ /**
+ * An ActionMode Callback class that is used to provide actions while in text insertion or
+ * selection mode.
+ *
+ * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
+ * actions, depending on which of these this TextView supports and the current selection.
+ */
+ private class TextActionModeCallback extends ActionMode.Callback2 {
+ private final Path mSelectionPath = new Path();
+ private final RectF mSelectionBounds = new RectF();
+ private final boolean mHasSelection;
+ private final int mHandleHeight;
+
+ public TextActionModeCallback(boolean hasSelection) {
+ mHasSelection = hasSelection;
+ if (mHasSelection) {
+ SelectionModifierCursorController selectionController = getSelectionController();
+ if (selectionController.mStartHandle == null) {
+ // As these are for initializing selectionController, hide() must be called.
+ selectionController.initDrawables();
+ selectionController.initHandles();
+ selectionController.hide();
+ }
+ mHandleHeight = Math.max(
+ mSelectHandleLeft.getMinimumHeight(),
+ mSelectHandleRight.getMinimumHeight());
+ } else {
+ InsertionPointCursorController insertionController = getInsertionController();
+ if (insertionController != null) {
+ insertionController.getHandle();
+ mHandleHeight = mSelectHandleCenter.getMinimumHeight();
+ } else {
+ mHandleHeight = 0;
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.setTitle(null);
+ mode.setSubtitle(null);
+ mode.setTitleOptionalHint(true);
+ populateMenuWithItems(menu);
+
+ Callback customCallback = getCustomCallback();
+ if (customCallback != null) {
+ if (!customCallback.onCreateActionMode(mode, menu)) {
+ // The custom mode can choose to cancel the action mode, dismiss selection.
+ Selection.setSelection((Spannable) mTextView.getText(),
+ mTextView.getSelectionEnd());
+ return false;
+ }
+ }
+
+ if (mTextView.canProcessText()) {
+ mProcessTextIntentActionsHandler.onInitializeMenu(menu);
+ }
+
+ if (menu.hasVisibleItems() || mode.getCustomView() != null) {
+ if (mHasSelection && !mTextView.hasTransientState()) {
+ mTextView.setHasTransientState(true);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private Callback getCustomCallback() {
+ return mHasSelection
+ ? mCustomSelectionActionModeCallback
+ : mCustomInsertionActionModeCallback;
+ }
+
+ private void populateMenuWithItems(Menu menu) {
+ if (mTextView.canCut()) {
+ menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+ com.android.internal.R.string.cut)
+ .setAlphabeticShortcut('x')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+
+ if (mTextView.canCopy()) {
+ menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+ com.android.internal.R.string.copy)
+ .setAlphabeticShortcut('c')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+
+ if (mTextView.canPaste()) {
+ menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+ com.android.internal.R.string.paste)
+ .setAlphabeticShortcut('v')
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+
+ if (mTextView.canShare()) {
+ menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+ com.android.internal.R.string.share)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+
+ if (mTextView.canRequestAutofill()) {
+ final String selected = mTextView.getSelectedText();
+ if (selected == null || selected.isEmpty()) {
+ menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+ com.android.internal.R.string.autofill)
+ .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
+ }
+ }
+
+ if (mTextView.canPasteAsPlainText()) {
+ menu.add(
+ Menu.NONE,
+ TextView.ID_PASTE_AS_PLAIN_TEXT,
+ MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ com.android.internal.R.string.paste_as_plain_text)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+
+ updateSelectAllItem(menu);
+ updateReplaceItem(menu);
+ updateAssistMenuItem(menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ updateSelectAllItem(menu);
+ updateReplaceItem(menu);
+ updateAssistMenuItem(menu);
+
+ Callback customCallback = getCustomCallback();
+ if (customCallback != null) {
+ return customCallback.onPrepareActionMode(mode, menu);
+ }
+ return true;
+ }
+
+ private void updateSelectAllItem(Menu menu) {
+ boolean canSelectAll = mTextView.canSelectAllText();
+ boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
+ if (canSelectAll && !selectAllItemExists) {
+ menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+ com.android.internal.R.string.selectAll)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ } else if (!canSelectAll && selectAllItemExists) {
+ menu.removeItem(TextView.ID_SELECT_ALL);
+ }
+ }
+
+ private void updateReplaceItem(Menu menu) {
+ boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
+ boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
+ if (canReplace && !replaceItemExists) {
+ menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
+ com.android.internal.R.string.replace)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ } else if (!canReplace && replaceItemExists) {
+ menu.removeItem(TextView.ID_REPLACE);
+ }
+ }
+
+ private void updateAssistMenuItem(Menu menu) {
+ menu.removeItem(TextView.ID_ASSIST);
+ final TextClassification textClassification =
+ getSelectionActionModeHelper().getTextClassification();
+ if (canAssist()) {
+ menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
+ textClassification.getLabel())
+ .setIcon(textClassification.getIcon())
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+ }
+
+ private boolean canAssist() {
+ final TextClassification textClassification =
+ getSelectionActionModeHelper().getTextClassification();
+ return mTextView.isDeviceProvisioned()
+ && textClassification != null
+ && (textClassification.getIcon() != null
+ || !TextUtils.isEmpty(textClassification.getLabel()))
+ && (textClassification.getOnClickListener() != null
+ || (textClassification.getIntent() != null
+ && mTextView.getContext().canStartActivityForResult()));
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ getSelectionActionModeHelper().onSelectionAction(item.getItemId());
+
+ if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
+ return true;
+ }
+ Callback customCallback = getCustomCallback();
+ if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
+ return true;
+ }
+ final TextClassification textClassification =
+ getSelectionActionModeHelper().getTextClassification();
+ if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
+ final OnClickListener onClickListener =
+ textClassification.getOnClickListener();
+ if (onClickListener != null) {
+ onClickListener.onClick(mTextView);
+ } else {
+ final Intent intent = textClassification.getIntent();
+ if (intent != null) {
+ TextClassification.createStartActivityOnClickListener(
+ mTextView.getContext(), intent)
+ .onClick(mTextView);
+ }
+ }
+ stopTextActionMode();
+ return true;
+ }
+ return mTextView.onTextContextMenuItem(item.getItemId());
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
+ getSelectionActionModeHelper().onDestroyActionMode();
+ mTextActionMode = null;
+ Callback customCallback = getCustomCallback();
+ if (customCallback != null) {
+ customCallback.onDestroyActionMode(mode);
+ }
+
+ if (!mPreserveSelection) {
+ /*
+ * Leave current selection when we tentatively destroy action mode for the
+ * selection. If we're detaching from a window, we'll bring back the selection
+ * mode when (if) we get reattached.
+ */
+ Selection.setSelection((Spannable) mTextView.getText(),
+ mTextView.getSelectionEnd());
+ }
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
+ }
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ if (!view.equals(mTextView) || mTextView.getLayout() == null) {
+ super.onGetContentRect(mode, view, outRect);
+ return;
+ }
+ if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
+ // We have a selection.
+ mSelectionPath.reset();
+ mTextView.getLayout().getSelectionPath(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
+ mSelectionPath.computeBounds(mSelectionBounds, true);
+ mSelectionBounds.bottom += mHandleHeight;
+ } else {
+ // We have a cursor.
+ Layout layout = mTextView.getLayout();
+ int line = layout.getLineForOffset(mTextView.getSelectionStart());
+ float primaryHorizontal = clampHorizontalPosition(null,
+ layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
+ mSelectionBounds.set(
+ primaryHorizontal,
+ layout.getLineTop(line),
+ primaryHorizontal,
+ layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight);
+ }
+ // Take TextView's padding and scroll into account.
+ int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
+ int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
+ outRect.set(
+ (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
+ (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
+ (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
+ (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
+ }
+ }
+
+ /**
+ * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
+ * while the input method is requesting the cursor/anchor position. Does nothing as long as
+ * {@link InputMethodManager#isWatchingCursor(View)} returns false.
+ */
+ private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
+ final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
+ final int[] mTmpIntOffset = new int[2];
+ final Matrix mViewToScreenMatrix = new Matrix();
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ final InputMethodState ims = mInputMethodState;
+ if (ims == null || ims.mBatchEditNesting > 0) {
+ return;
+ }
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ if (null == imm) {
+ return;
+ }
+ if (!imm.isActive(mTextView)) {
+ return;
+ }
+ // Skip if the IME has not requested the cursor/anchor position.
+ if (!imm.isCursorAnchorInfoEnabled()) {
+ return;
+ }
+ Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ return;
+ }
+
+ final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
+ builder.reset();
+
+ final int selectionStart = mTextView.getSelectionStart();
+ builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
+
+ // Construct transformation matrix from view local coordinates to screen coordinates.
+ mViewToScreenMatrix.set(mTextView.getMatrix());
+ mTextView.getLocationOnScreen(mTmpIntOffset);
+ mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
+ builder.setMatrix(mViewToScreenMatrix);
+
+ final float viewportToContentHorizontalOffset =
+ mTextView.viewportToContentHorizontalOffset();
+ final float viewportToContentVerticalOffset =
+ mTextView.viewportToContentVerticalOffset();
+
+ final CharSequence text = mTextView.getText();
+ if (text instanceof Spannable) {
+ final Spannable sp = (Spannable) text;
+ int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
+ int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ if (composingTextEnd < composingTextStart) {
+ final int temp = composingTextEnd;
+ composingTextEnd = composingTextStart;
+ composingTextStart = temp;
+ }
+ final boolean hasComposingText =
+ (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
+ if (hasComposingText) {
+ final CharSequence composingText = text.subSequence(composingTextStart,
+ composingTextEnd);
+ builder.setComposingText(composingTextStart, composingText);
+ mTextView.populateCharacterBounds(builder, composingTextStart,
+ composingTextEnd, viewportToContentHorizontalOffset,
+ viewportToContentVerticalOffset);
+ }
+ }
+
+ // Treat selectionStart as the insertion point.
+ if (0 <= selectionStart) {
+ final int offset = selectionStart;
+ final int line = layout.getLineForOffset(offset);
+ final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
+ + viewportToContentHorizontalOffset;
+ final float insertionMarkerTop = layout.getLineTop(line)
+ + viewportToContentVerticalOffset;
+ final float insertionMarkerBaseline = layout.getLineBaseline(line)
+ + viewportToContentVerticalOffset;
+ final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
+ + viewportToContentVerticalOffset;
+ final boolean isTopVisible = mTextView
+ .isPositionVisible(insertionMarkerX, insertionMarkerTop);
+ final boolean isBottomVisible = mTextView
+ .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
+ int insertionMarkerFlags = 0;
+ if (isTopVisible || isBottomVisible) {
+ insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
+ }
+ if (!isTopVisible || !isBottomVisible) {
+ insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
+ }
+ if (layout.isRtlCharAt(offset)) {
+ insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
+ }
+ builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
+ insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
+ }
+
+ imm.updateCursorAnchorInfo(mTextView, builder.build());
+ }
+ }
+
+ @VisibleForTesting
+ public abstract class HandleView extends View implements TextViewPositionListener {
+ protected Drawable mDrawable;
+ protected Drawable mDrawableLtr;
+ protected Drawable mDrawableRtl;
+ private final PopupWindow mContainer;
+ // Position with respect to the parent TextView
+ private int mPositionX, mPositionY;
+ private boolean mIsDragging;
+ // Offset from touch position to mPosition
+ private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
+ protected int mHotspotX;
+ protected int mHorizontalGravity;
+ // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
+ private float mTouchOffsetY;
+ // Where the touch position should be on the handle to ensure a maximum cursor visibility
+ private float mIdealVerticalOffset;
+ // Parent's (TextView) previous position in window
+ private int mLastParentX, mLastParentY;
+ // Parent's (TextView) previous position on screen
+ private int mLastParentXOnScreen, mLastParentYOnScreen;
+ // Previous text character offset
+ protected int mPreviousOffset = -1;
+ // Previous text character offset
+ private boolean mPositionHasChanged = true;
+ // Minimum touch target size for handles
+ private int mMinSize;
+ // Indicates the line of text that the handle is on.
+ protected int mPrevLine = UNSET_LINE;
+ // Indicates the line of text that the user was touching. This can differ from mPrevLine
+ // when selecting text when the handles jump to the end / start of words which may be on
+ // a different line.
+ protected int mPreviousLineTouched = UNSET_LINE;
+
+ private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
+ super(mTextView.getContext());
+ setId(id);
+ mContainer = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mContainer.setSplitTouchEnabled(true);
+ mContainer.setClippingEnabled(false);
+ mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContainer.setContentView(this);
+
+ mDrawableLtr = drawableLtr;
+ mDrawableRtl = drawableRtl;
+ mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.text_handle_min_size);
+
+ updateDrawable();
+
+ final int handleHeight = getPreferredHeight();
+ mTouchOffsetY = -0.3f * handleHeight;
+ mIdealVerticalOffset = 0.7f * handleHeight;
+ }
+
+ public float getIdealVerticalOffset() {
+ return mIdealVerticalOffset;
+ }
+
+ protected void updateDrawable() {
+ if (mIsDragging) {
+ // Don't update drawable during dragging.
+ return;
+ }
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ return;
+ }
+ final int offset = getCurrentCursorOffset();
+ final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
+ final Drawable oldDrawable = mDrawable;
+ mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
+ mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
+ mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
+ if (oldDrawable != mDrawable && isShowing()) {
+ // Update popup window position.
+ mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
+ - getHorizontalOffset() + getCursorOffset();
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+ mPositionHasChanged = true;
+ updatePosition(mLastParentX, mLastParentY, false, false);
+ postInvalidate();
+ }
+ }
+
+ protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
+ protected abstract int getHorizontalGravity(boolean isRtlRun);
+
+ // Touch-up filter: number of previous positions remembered
+ private static final int HISTORY_SIZE = 5;
+ private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
+ private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
+ private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
+ private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
+ private int mPreviousOffsetIndex = 0;
+ private int mNumberPreviousOffsets = 0;
+
+ private void startTouchUpFilter(int offset) {
+ mNumberPreviousOffsets = 0;
+ addPositionToTouchUpFilter(offset);
+ }
+
+ private void addPositionToTouchUpFilter(int offset) {
+ mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
+ mPreviousOffsets[mPreviousOffsetIndex] = offset;
+ mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
+ mNumberPreviousOffsets++;
+ }
+
+ private void filterOnTouchUp(boolean fromTouchScreen) {
+ final long now = SystemClock.uptimeMillis();
+ int i = 0;
+ int index = mPreviousOffsetIndex;
+ final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
+ while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
+ i++;
+ index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
+ }
+
+ if (i > 0 && i < iMax
+ && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
+ positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
+ }
+ }
+
+ public boolean offsetHasBeenChanged() {
+ return mNumberPreviousOffsets > 1;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
+ }
+
+ @Override
+ public void invalidate() {
+ super.invalidate();
+ if (isShowing()) {
+ positionAtCursorOffset(getCurrentCursorOffset(), true, false);
+ }
+ };
+
+ private int getPreferredWidth() {
+ return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
+ }
+
+ private int getPreferredHeight() {
+ return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
+ }
+
+ public void show() {
+ if (isShowing()) return;
+
+ getPositionListener().addSubscriber(this, true /* local position may change */);
+
+ // Make sure the offset is always considered new, even when focusing at same position
+ mPreviousOffset = -1;
+ positionAtCursorOffset(getCurrentCursorOffset(), false, false);
+ }
+
+ protected void dismiss() {
+ mIsDragging = false;
+ mContainer.dismiss();
+ onDetached();
+ }
+
+ public void hide() {
+ dismiss();
+
+ getPositionListener().removeSubscriber(this);
+ }
+
+ public boolean isShowing() {
+ return mContainer.isShowing();
+ }
+
+ private boolean isVisible() {
+ // Always show a dragging handle.
+ if (mIsDragging) {
+ return true;
+ }
+
+ if (mTextView.isInBatchEditMode()) {
+ return false;
+ }
+
+ return mTextView.isPositionVisible(
+ mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
+ }
+
+ public abstract int getCurrentCursorOffset();
+
+ protected abstract void updateSelection(int offset);
+
+ protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
+
+ protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
+ return layout.isRtlCharAt(offset);
+ }
+
+ @VisibleForTesting
+ public float getHorizontal(@NonNull Layout layout, int offset) {
+ return layout.getPrimaryHorizontal(offset);
+ }
+
+ protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
+ return mTextView.getOffsetAtCoordinate(line, x);
+ }
+
+ /**
+ * @param offset Cursor offset. Must be in [-1, length].
+ * @param forceUpdatePosition whether to force update the position. This should be true
+ * when If the parent has been scrolled, for example.
+ * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
+ * touch screen.
+ */
+ protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+ boolean fromTouchScreen) {
+ // A HandleView relies on the layout, which may be nulled by external methods
+ Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ // Will update controllers' state, hiding them and stopping selection mode if needed
+ prepareCursorControllers();
+ return;
+ }
+ layout = mTextView.getLayout();
+
+ boolean offsetChanged = offset != mPreviousOffset;
+ if (offsetChanged || forceUpdatePosition) {
+ if (offsetChanged) {
+ updateSelection(offset);
+ if (fromTouchScreen && mHapticTextHandleEnabled) {
+ mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+ }
+ addPositionToTouchUpFilter(offset);
+ }
+ final int line = layout.getLineForOffset(offset);
+ mPrevLine = line;
+
+ mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
+ - getHorizontalOffset() + getCursorOffset();
+ mPositionY = layout.getLineBottomWithoutSpacing(line);
+
+ // Take TextView's padding and scroll into account.
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+ mPositionY += mTextView.viewportToContentVerticalOffset();
+
+ mPreviousOffset = offset;
+ mPositionHasChanged = true;
+ }
+ }
+
+ /**
+ * Return the clamped horizontal position for the cursor.
+ *
+ * @param layout Text layout.
+ * @param offset Character offset for the cursor.
+ * @return The clamped horizontal position for the cursor.
+ */
+ int getCursorHorizontalPosition(Layout layout, int offset) {
+ return (int) (getHorizontal(layout, offset) - 0.5f);
+ }
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
+ if (parentPositionChanged || mPositionHasChanged) {
+ if (mIsDragging) {
+ // Update touchToWindow offset in case of parent scrolling while dragging
+ if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
+ mTouchToWindowOffsetX += parentPositionX - mLastParentX;
+ mTouchToWindowOffsetY += parentPositionY - mLastParentY;
+ mLastParentX = parentPositionX;
+ mLastParentY = parentPositionY;
+ }
+
+ onHandleMoved();
+ }
+
+ if (isVisible()) {
+ // Transform to the window coordinates to follow the view tranformation.
+ final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
+ mTextView.transformFromViewToWindowSpace(pts);
+ pts[0] -= mHotspotX + getHorizontalOffset();
+
+ if (isShowing()) {
+ mContainer.update(pts[0], pts[1], -1, -1);
+ } else {
+ mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
+ }
+ } else {
+ if (isShowing()) {
+ dismiss();
+ }
+ }
+
+ mPositionHasChanged = false;
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ final int drawWidth = mDrawable.getIntrinsicWidth();
+ final int left = getHorizontalOffset();
+
+ mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
+ mDrawable.draw(c);
+ }
+
+ private int getHorizontalOffset() {
+ final int width = getPreferredWidth();
+ final int drawWidth = mDrawable.getIntrinsicWidth();
+ final int left;
+ switch (mHorizontalGravity) {
+ case Gravity.LEFT:
+ left = 0;
+ break;
+ default:
+ case Gravity.CENTER:
+ left = (width - drawWidth) / 2;
+ break;
+ case Gravity.RIGHT:
+ left = width - drawWidth;
+ break;
+ }
+ return left;
+ }
+
+ protected int getCursorOffset() {
+ return 0;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ updateFloatingToolbarVisibility(ev);
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ startTouchUpFilter(getCurrentCursorOffset());
+
+ final PositionListener positionListener = getPositionListener();
+ mLastParentX = positionListener.getPositionX();
+ mLastParentY = positionListener.getPositionY();
+ mLastParentXOnScreen = positionListener.getPositionXOnScreen();
+ mLastParentYOnScreen = positionListener.getPositionYOnScreen();
+
+ final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
+ final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
+ mTouchToWindowOffsetX = xInWindow - mPositionX;
+ mTouchToWindowOffsetY = yInWindow - mPositionY;
+
+ mIsDragging = true;
+ mPreviousLineTouched = UNSET_LINE;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
+ final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
+
+ // Vertical hysteresis: vertical down movement tends to snap to ideal offset
+ final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
+ final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
+ float newVerticalOffset;
+ if (previousVerticalOffset < mIdealVerticalOffset) {
+ newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
+ newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
+ } else {
+ newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
+ newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
+ }
+ mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
+
+ final float newPosX =
+ xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
+ final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
+
+ updatePosition(newPosX, newPosY,
+ ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+ break;
+ }
+
+ case MotionEvent.ACTION_UP:
+ filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+ mIsDragging = false;
+ updateDrawable();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mIsDragging = false;
+ updateDrawable();
+ break;
+ }
+ return true;
+ }
+
+ public boolean isDragging() {
+ return mIsDragging;
+ }
+
+ void onHandleMoved() {}
+
+ public void onDetached() {}
+ }
+
+ private class InsertionHandleView extends HandleView {
+ private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
+ private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
+
+ // Used to detect taps on the insertion handle, which will affect the insertion action mode
+ private float mDownPositionX, mDownPositionY;
+ private Runnable mHider;
+
+ public InsertionHandleView(Drawable drawable) {
+ super(drawable, drawable, com.android.internal.R.id.insertion_handle);
+ }
+
+ @Override
+ public void show() {
+ super.show();
+
+ final long durationSinceCutOrCopy =
+ SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
+
+ // Cancel the single tap delayed runnable.
+ if (mInsertionActionModeRunnable != null
+ && ((mTapState == TAP_STATE_DOUBLE_TAP)
+ || (mTapState == TAP_STATE_TRIPLE_CLICK)
+ || isCursorInsideEasyCorrectionSpan())) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+
+ // Prepare and schedule the single tap runnable to run exactly after the double tap
+ // timeout has passed.
+ if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
+ && !isCursorInsideEasyCorrectionSpan()
+ && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
+ if (mTextActionMode == null) {
+ if (mInsertionActionModeRunnable == null) {
+ mInsertionActionModeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ startInsertionActionMode();
+ }
+ };
+ }
+ mTextView.postDelayed(
+ mInsertionActionModeRunnable,
+ ViewConfiguration.getDoubleTapTimeout() + 1);
+ }
+
+ }
+
+ hideAfterDelay();
+ }
+
+ private void hideAfterDelay() {
+ if (mHider == null) {
+ mHider = new Runnable() {
+ public void run() {
+ hide();
+ }
+ };
+ } else {
+ removeHiderCallback();
+ }
+ mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
+ }
+
+ private void removeHiderCallback() {
+ if (mHider != null) {
+ mTextView.removeCallbacks(mHider);
+ }
+ }
+
+ @Override
+ protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+ return drawable.getIntrinsicWidth() / 2;
+ }
+
+ @Override
+ protected int getHorizontalGravity(boolean isRtlRun) {
+ return Gravity.CENTER_HORIZONTAL;
+ }
+
+ @Override
+ protected int getCursorOffset() {
+ int offset = super.getCursorOffset();
+ if (mCursorDrawable != null) {
+ mCursorDrawable.getPadding(mTempRect);
+ offset += (mCursorDrawable.getIntrinsicWidth()
+ - mTempRect.left - mTempRect.right) / 2;
+ }
+ return offset;
+ }
+
+ @Override
+ int getCursorHorizontalPosition(Layout layout, int offset) {
+ if (mCursorDrawable != null) {
+ final float horizontal = getHorizontal(layout, offset);
+ return clampHorizontalPosition(mCursorDrawable, horizontal) + mTempRect.left;
+ }
+ return super.getCursorHorizontalPosition(layout, offset);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final boolean result = super.onTouchEvent(ev);
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownPositionX = ev.getRawX();
+ mDownPositionY = ev.getRawY();
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (!offsetHasBeenChanged()) {
+ final float deltaX = mDownPositionX - ev.getRawX();
+ final float deltaY = mDownPositionY - ev.getRawY();
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ mTextView.getContext());
+ final int touchSlop = viewConfiguration.getScaledTouchSlop();
+
+ if (distanceSquared < touchSlop * touchSlop) {
+ // Tapping on the handle toggles the insertion action mode.
+ if (mTextActionMode != null) {
+ stopTextActionMode();
+ } else {
+ startInsertionActionMode();
+ }
+ }
+ } else {
+ if (mTextActionMode != null) {
+ mTextActionMode.invalidateContentRect();
+ }
+ }
+ hideAfterDelay();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ hideAfterDelay();
+ break;
+
+ default:
+ break;
+ }
+
+ return result;
+ }
+
+ @Override
+ public int getCurrentCursorOffset() {
+ return mTextView.getSelectionStart();
+ }
+
+ @Override
+ public void updateSelection(int offset) {
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ }
+
+ @Override
+ protected void updatePosition(float x, float y, boolean fromTouchScreen) {
+ Layout layout = mTextView.getLayout();
+ int offset;
+ if (layout != null) {
+ if (mPreviousLineTouched == UNSET_LINE) {
+ mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
+ }
+ int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
+ offset = getOffsetAtCoordinate(layout, currLine, x);
+ mPreviousLineTouched = currLine;
+ } else {
+ offset = -1;
+ }
+ positionAtCursorOffset(offset, false, fromTouchScreen);
+ if (mTextActionMode != null) {
+ invalidateActionMode();
+ }
+ }
+
+ @Override
+ void onHandleMoved() {
+ super.onHandleMoved();
+ removeHiderCallback();
+ }
+
+ @Override
+ public void onDetached() {
+ super.onDetached();
+ removeHiderCallback();
+ }
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
+ public @interface HandleType {}
+ public static final int HANDLE_TYPE_SELECTION_START = 0;
+ public static final int HANDLE_TYPE_SELECTION_END = 1;
+
+ /** For selection handles */
+ @VisibleForTesting
+ public final class SelectionHandleView extends HandleView {
+ // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
+ // end (HANDLE_TYPE_SELECTION_END).
+ @HandleType
+ private final int mHandleType;
+ // Indicates whether the cursor is making adjustments within a word.
+ private boolean mInWord = false;
+ // Difference between touch position and word boundary position.
+ private float mTouchWordDelta;
+ // X value of the previous updatePosition call.
+ private float mPrevX;
+ // Indicates if the handle has moved a boundary between LTR and RTL text.
+ private boolean mLanguageDirectionChanged = false;
+ // Distance from edge of horizontally scrolling text view
+ // to use to switch to character mode.
+ private final float mTextViewEdgeSlop;
+ // Used to save text view location.
+ private final int[] mTextViewLocation = new int[2];
+
+ public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
+ @HandleType int handleType) {
+ super(drawableLtr, drawableRtl, id);
+ mHandleType = handleType;
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
+ mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
+ }
+
+ private boolean isStartHandle() {
+ return mHandleType == HANDLE_TYPE_SELECTION_START;
+ }
+
+ @Override
+ protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+ if (isRtlRun == isStartHandle()) {
+ return drawable.getIntrinsicWidth() / 4;
+ } else {
+ return (drawable.getIntrinsicWidth() * 3) / 4;
+ }
+ }
+
+ @Override
+ protected int getHorizontalGravity(boolean isRtlRun) {
+ return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
+ }
+
+ @Override
+ public int getCurrentCursorOffset() {
+ return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
+ }
+
+ @Override
+ protected void updateSelection(int offset) {
+ if (isStartHandle()) {
+ Selection.setSelection((Spannable) mTextView.getText(), offset,
+ mTextView.getSelectionEnd());
+ } else {
+ Selection.setSelection((Spannable) mTextView.getText(),
+ mTextView.getSelectionStart(), offset);
+ }
+ updateDrawable();
+ if (mTextActionMode != null) {
+ invalidateActionMode();
+ }
+ }
+
+ @Override
+ protected void updatePosition(float x, float y, boolean fromTouchScreen) {
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) {
+ // HandleView will deal appropriately in positionAtCursorOffset when
+ // layout is null.
+ positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
+ fromTouchScreen);
+ return;
+ }
+
+ if (mPreviousLineTouched == UNSET_LINE) {
+ mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
+ }
+
+ boolean positionCursor = false;
+ final int anotherHandleOffset =
+ isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
+ int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
+ int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
+
+ if (isStartHandle() && initialOffset >= anotherHandleOffset
+ || !isStartHandle() && initialOffset <= anotherHandleOffset) {
+ // Handles have crossed, bound it to the first selected line and
+ // adjust by word / char as normal.
+ currLine = layout.getLineForOffset(anotherHandleOffset);
+ initialOffset = getOffsetAtCoordinate(layout, currLine, x);
+ }
+
+ int offset = initialOffset;
+ final int wordEnd = getWordEnd(offset);
+ final int wordStart = getWordStart(offset);
+
+ if (mPrevX == UNSET_X_VALUE) {
+ mPrevX = x;
+ }
+
+ final int currentOffset = getCurrentCursorOffset();
+ final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
+ final boolean atRtl = isAtRtlRun(layout, offset);
+ final boolean isLvlBoundary = layout.isLevelBoundary(offset);
+
+ // We can't determine if the user is expanding or shrinking the selection if they're
+ // on a bi-di boundary, so until they've moved past the boundary we'll just place
+ // the cursor at the current position.
+ if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
+ // We're on a boundary or this is the first direction change -- just update
+ // to the current position.
+ mLanguageDirectionChanged = true;
+ mTouchWordDelta = 0.0f;
+ positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+ return;
+ } else if (mLanguageDirectionChanged && !isLvlBoundary) {
+ // We've just moved past the boundary so update the position. After this we can
+ // figure out if the user is expanding or shrinking to go by word or character.
+ positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+ mTouchWordDelta = 0.0f;
+ mLanguageDirectionChanged = false;
+ return;
+ }
+
+ boolean isExpanding;
+ final float xDiff = x - mPrevX;
+ if (isStartHandle()) {
+ isExpanding = currLine < mPreviousLineTouched;
+ } else {
+ isExpanding = currLine > mPreviousLineTouched;
+ }
+ if (atRtl == isStartHandle()) {
+ isExpanding |= xDiff > 0;
+ } else {
+ isExpanding |= xDiff < 0;
+ }
+
+ if (mTextView.getHorizontallyScrolling()) {
+ if (positionNearEdgeOfScrollingView(x, atRtl)
+ && ((isStartHandle() && mTextView.getScrollX() != 0)
+ || (!isStartHandle()
+ && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
+ && ((isExpanding && ((isStartHandle() && offset < currentOffset)
+ || (!isStartHandle() && offset > currentOffset)))
+ || !isExpanding)) {
+ // If we're expanding ensure that the offset is actually expanding compared to
+ // the current offset, if the handle snapped to the word, the finger position
+ // may be out of sync and we don't want the selection to jump back.
+ mTouchWordDelta = 0.0f;
+ final int nextOffset = (atRtl == isStartHandle())
+ ? layout.getOffsetToRightOf(mPreviousOffset)
+ : layout.getOffsetToLeftOf(mPreviousOffset);
+ positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
+ return;
+ }
+ }
+
+ if (isExpanding) {
+ // User is increasing the selection.
+ int wordBoundary = isStartHandle() ? wordStart : wordEnd;
+ final boolean snapToWord = (!mInWord
+ || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
+ && atRtl == isAtRtlRun(layout, wordBoundary);
+ if (snapToWord) {
+ // Sometimes words can be broken across lines (Chinese, hyphenation).
+ // We still snap to the word boundary but we only use the letters on the
+ // current line to determine if the user is far enough into the word to snap.
+ if (layout.getLineForOffset(wordBoundary) != currLine) {
+ wordBoundary = isStartHandle()
+ ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
+ }
+ final int offsetThresholdToSnap = isStartHandle()
+ ? wordEnd - ((wordEnd - wordBoundary) / 2)
+ : wordStart + ((wordBoundary - wordStart) / 2);
+ if (isStartHandle()
+ && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
+ // User is far enough into the word or on a different line so we expand by
+ // word.
+ offset = wordStart;
+ } else if (!isStartHandle()
+ && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
+ // User is far enough into the word or on a different line so we expand by
+ // word.
+ offset = wordEnd;
+ } else {
+ offset = mPreviousOffset;
+ }
+ }
+ if ((isStartHandle() && offset < initialOffset)
+ || (!isStartHandle() && offset > initialOffset)) {
+ final float adjustedX = getHorizontal(layout, offset);
+ mTouchWordDelta =
+ mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
+ } else {
+ mTouchWordDelta = 0.0f;
+ }
+ positionCursor = true;
+ } else {
+ final int adjustedOffset =
+ getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
+ final boolean shrinking = isStartHandle()
+ ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
+ : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
+ if (shrinking) {
+ // User is shrinking the selection.
+ if (currLine != mPrevLine) {
+ // We're on a different line, so we'll snap to word boundaries.
+ offset = isStartHandle() ? wordStart : wordEnd;
+ if ((isStartHandle() && offset < initialOffset)
+ || (!isStartHandle() && offset > initialOffset)) {
+ final float adjustedX = getHorizontal(layout, offset);
+ mTouchWordDelta =
+ mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
+ } else {
+ mTouchWordDelta = 0.0f;
+ }
+ } else {
+ offset = adjustedOffset;
+ }
+ positionCursor = true;
+ } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
+ || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
+ // Handle has jumped to the word boundary, and the user is moving
+ // their finger towards the handle, the delta should be updated.
+ mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
+ - getHorizontal(layout, mPreviousOffset);
+ }
+ }
+
+ if (positionCursor) {
+ mPreviousLineTouched = currLine;
+ positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+ }
+ mPrevX = x;
+ }
+
+ @Override
+ protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+ boolean fromTouchScreen) {
+ super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
+ mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean superResult = super.onTouchEvent(event);
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ // Reset the touch word offset and x value when the user
+ // re-engages the handle.
+ mTouchWordDelta = 0.0f;
+ mPrevX = UNSET_X_VALUE;
+ }
+ return superResult;
+ }
+
+ private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
+ final int anotherHandleOffset =
+ isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
+ if ((isStartHandle() && offset >= anotherHandleOffset)
+ || (!isStartHandle() && offset <= anotherHandleOffset)) {
+ mTouchWordDelta = 0.0f;
+ final Layout layout = mTextView.getLayout();
+ if (layout != null && offset != anotherHandleOffset) {
+ final float horiz = getHorizontal(layout, offset);
+ final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
+ !isStartHandle());
+ final float currentHoriz = getHorizontal(layout, mPreviousOffset);
+ if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
+ || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
+ // This handle passes another one as it crossed a direction boundary.
+ // Don't minimize the selection, but keep the handle at the run boundary.
+ final int currentOffset = getCurrentCursorOffset();
+ final int offsetToGetRunRange = isStartHandle()
+ ? currentOffset : Math.max(currentOffset - 1, 0);
+ final long range = layout.getRunRange(offsetToGetRunRange);
+ if (isStartHandle()) {
+ offset = TextUtils.unpackRangeStartFromLong(range);
+ } else {
+ offset = TextUtils.unpackRangeEndFromLong(range);
+ }
+ positionAtCursorOffset(offset, false, fromTouchScreen);
+ return;
+ }
+ }
+ // Handles can not cross and selection is at least one character.
+ offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
+ }
+ positionAtCursorOffset(offset, false, fromTouchScreen);
+ }
+
+ private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
+ mTextView.getLocationOnScreen(mTextViewLocation);
+ boolean nearEdge;
+ if (atRtl == isStartHandle()) {
+ int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
+ - mTextView.getPaddingRight();
+ nearEdge = x > rightEdge - mTextViewEdgeSlop;
+ } else {
+ int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
+ nearEdge = x < leftEdge + mTextViewEdgeSlop;
+ }
+ return nearEdge;
+ }
+
+ @Override
+ protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
+ final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
+ return layout.isRtlCharAt(offsetToCheck);
+ }
+
+ @Override
+ public float getHorizontal(@NonNull Layout layout, int offset) {
+ return getHorizontal(layout, offset, isStartHandle());
+ }
+
+ private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
+ final int line = layout.getLineForOffset(offset);
+ final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
+ final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
+ final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
+ return (isRtlChar == isRtlParagraph)
+ ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
+ }
+
+ @Override
+ protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
+ final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
+ final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
+ if (!layout.isLevelBoundary(primaryOffset)) {
+ return primaryOffset;
+ }
+ final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
+ final int currentOffset = getCurrentCursorOffset();
+ final int primaryDiff = Math.abs(primaryOffset - currentOffset);
+ final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
+ if (primaryDiff < secondaryDiff) {
+ return primaryOffset;
+ } else if (primaryDiff > secondaryDiff) {
+ return secondaryOffset;
+ } else {
+ final int offsetToCheck = isStartHandle()
+ ? currentOffset : Math.max(currentOffset - 1, 0);
+ final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
+ final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
+ return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
+ }
+ }
+ }
+
+ private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
+ final int trueLine = mTextView.getLineAtCoordinate(y);
+ if (layout == null || prevLine > layout.getLineCount()
+ || layout.getLineCount() <= 0 || prevLine < 0) {
+ // Invalid parameters, just return whatever line is at y.
+ return trueLine;
+ }
+
+ if (Math.abs(trueLine - prevLine) >= 2) {
+ // Only stick to lines if we're within a line of the previous selection.
+ return trueLine;
+ }
+
+ final float verticalOffset = mTextView.viewportToContentVerticalOffset();
+ final int lineCount = layout.getLineCount();
+ final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
+
+ final float firstLineTop = layout.getLineTop(0) + verticalOffset;
+ final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
+ final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
+
+ final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
+ final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
+ final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
+
+ // Determine if we've moved lines based on y position and previous line.
+ int currLine;
+ if (y <= yTopBound) {
+ currLine = Math.max(prevLine - 1, 0);
+ } else if (y >= yBottomBound) {
+ currLine = Math.min(prevLine + 1, lineCount - 1);
+ } else {
+ currLine = prevLine;
+ }
+ return currLine;
+ }
+
+ /**
+ * A CursorController instance can be used to control a cursor in the text.
+ */
+ private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
+ /**
+ * Makes the cursor controller visible on screen.
+ * See also {@link #hide()}.
+ */
+ public void show();
+
+ /**
+ * Hide the cursor controller from screen.
+ * See also {@link #show()}.
+ */
+ public void hide();
+
+ /**
+ * Called when the view is detached from window. Perform house keeping task, such as
+ * stopping Runnable thread that would otherwise keep a reference on the context, thus
+ * preventing the activity from being recycled.
+ */
+ public void onDetached();
+
+ public boolean isCursorBeingModified();
+
+ public boolean isActive();
+ }
+
+ private class InsertionPointCursorController implements CursorController {
+ private InsertionHandleView mHandle;
+
+ public void show() {
+ getHandle().show();
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
+ }
+
+ public void hide() {
+ if (mHandle != null) {
+ mHandle.hide();
+ }
+ }
+
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ hide();
+ }
+ }
+
+ private InsertionHandleView getHandle() {
+ if (mSelectHandleCenter == null) {
+ mSelectHandleCenter = mTextView.getContext().getDrawable(
+ mTextView.mTextSelectHandleRes);
+ }
+ if (mHandle == null) {
+ mHandle = new InsertionHandleView(mSelectHandleCenter);
+ }
+ return mHandle;
+ }
+
+ @Override
+ public void onDetached() {
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.removeOnTouchModeChangeListener(this);
+
+ if (mHandle != null) mHandle.onDetached();
+ }
+
+ @Override
+ public boolean isCursorBeingModified() {
+ return mHandle != null && mHandle.isDragging();
+ }
+
+ @Override
+ public boolean isActive() {
+ return mHandle != null && mHandle.isShowing();
+ }
+
+ public void invalidateHandle() {
+ if (mHandle != null) {
+ mHandle.invalidate();
+ }
+ }
+ }
+
+ class SelectionModifierCursorController implements CursorController {
+ // The cursor controller handles, lazily created when shown.
+ private SelectionHandleView mStartHandle;
+ private SelectionHandleView mEndHandle;
+ // The offsets of that last touch down event. Remembered to start selection there.
+ private int mMinTouchOffset, mMaxTouchOffset;
+
+ private float mDownPositionX, mDownPositionY;
+ private boolean mGestureStayedInTapRegion;
+
+ // Where the user first starts the drag motion.
+ private int mStartOffset = -1;
+
+ private boolean mHaventMovedEnoughToStartDrag;
+ // The line that a selection happened most recently with the drag accelerator.
+ private int mLineSelectionIsOn = -1;
+ // Whether the drag accelerator has selected past the initial line.
+ private boolean mSwitchedLines = false;
+
+ // Indicates the drag accelerator mode that the user is currently using.
+ private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
+ // Drag accelerator is inactive.
+ private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
+ // Character based selection by dragging. Only for mouse.
+ private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
+ // Word based selection by dragging. Enabled after long pressing or double tapping.
+ private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
+ // Paragraph based selection by dragging. Enabled after mouse triple click.
+ private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
+
+ SelectionModifierCursorController() {
+ resetTouchOffsets();
+ }
+
+ public void show() {
+ if (mTextView.isInBatchEditMode()) {
+ return;
+ }
+ initDrawables();
+ initHandles();
+ }
+
+ private void initDrawables() {
+ if (mSelectHandleLeft == null) {
+ mSelectHandleLeft = mTextView.getContext().getDrawable(
+ mTextView.mTextSelectHandleLeftRes);
+ }
+ if (mSelectHandleRight == null) {
+ mSelectHandleRight = mTextView.getContext().getDrawable(
+ mTextView.mTextSelectHandleRightRes);
+ }
+ }
+
+ private void initHandles() {
+ // Lazy object creation has to be done before updatePosition() is called.
+ if (mStartHandle == null) {
+ mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
+ com.android.internal.R.id.selection_start_handle,
+ HANDLE_TYPE_SELECTION_START);
+ }
+ if (mEndHandle == null) {
+ mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
+ com.android.internal.R.id.selection_end_handle,
+ HANDLE_TYPE_SELECTION_END);
+ }
+
+ mStartHandle.show();
+ mEndHandle.show();
+
+ hideInsertionPointCursorController();
+ }
+
+ public void hide() {
+ if (mStartHandle != null) mStartHandle.hide();
+ if (mEndHandle != null) mEndHandle.hide();
+ }
+
+ public void enterDrag(int dragAcceleratorMode) {
+ // Just need to init the handles / hide insertion cursor.
+ show();
+ mDragAcceleratorMode = dragAcceleratorMode;
+ // Start location of selection.
+ mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
+ mLastDownPositionY);
+ mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
+ // Don't show the handles until user has lifted finger.
+ hide();
+
+ // This stops scrolling parents from intercepting the touch event, allowing
+ // the user to continue dragging across the screen to select text; TextView will
+ // scroll as necessary.
+ mTextView.getParent().requestDisallowInterceptTouchEvent(true);
+ mTextView.cancelLongPress();
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ // This is done even when the View does not have focus, so that long presses can start
+ // selection and tap can move cursor from this tap position.
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (extractedTextModeWillBeStarted()) {
+ // Prevent duplicating the selection handles until the mode starts.
+ hide();
+ } else {
+ // Remember finger down position, to be able to start selection from there.
+ mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
+ eventX, eventY);
+
+ // Double tap detection
+ if (mGestureStayedInTapRegion) {
+ if (mTapState == TAP_STATE_DOUBLE_TAP
+ || mTapState == TAP_STATE_TRIPLE_CLICK) {
+ final float deltaX = eventX - mDownPositionX;
+ final float deltaY = eventY - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ mTextView.getContext());
+ int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+ boolean stayedInArea =
+ distanceSquared < doubleTapSlop * doubleTapSlop;
+
+ if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
+ if (mTapState == TAP_STATE_DOUBLE_TAP) {
+ selectCurrentWordAndStartDrag();
+ } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
+ selectCurrentParagraphAndStartDrag();
+ }
+ mDiscardNextActionUp = true;
+ }
+ }
+ }
+
+ mDownPositionX = eventX;
+ mDownPositionY = eventY;
+ mGestureStayedInTapRegion = true;
+ mHaventMovedEnoughToStartDrag = true;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_POINTER_UP:
+ // Handle multi-point gestures. Keep min and max offset positions.
+ // Only activated for devices that correctly handle multi-touch.
+ if (mTextView.getContext().getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ updateMinAndMaxOffsets(event);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final ViewConfiguration viewConfig = ViewConfiguration.get(
+ mTextView.getContext());
+ final int touchSlop = viewConfig.getScaledTouchSlop();
+
+ if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
+ final float deltaX = eventX - mDownPositionX;
+ final float deltaY = eventY - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ if (mGestureStayedInTapRegion) {
+ int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
+ mGestureStayedInTapRegion =
+ distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
+ }
+ if (mHaventMovedEnoughToStartDrag) {
+ // We don't start dragging until the user has moved enough.
+ mHaventMovedEnoughToStartDrag =
+ distanceSquared <= touchSlop * touchSlop;
+ }
+ }
+
+ if (isMouse && !isDragAcceleratorActive()) {
+ final int offset = mTextView.getOffsetForPosition(eventX, eventY);
+ if (mTextView.hasSelection()
+ && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
+ && offset >= mTextView.getSelectionStart()
+ && offset <= mTextView.getSelectionEnd()) {
+ startDragAndDrop();
+ break;
+ }
+
+ if (mStartOffset != offset) {
+ // Start character based drag accelerator.
+ stopTextActionMode();
+ enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
+ mDiscardNextActionUp = true;
+ mHaventMovedEnoughToStartDrag = false;
+ }
+ }
+
+ if (mStartHandle != null && mStartHandle.isShowing()) {
+ // Don't do the drag if the handles are showing already.
+ break;
+ }
+
+ updateSelection(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (!isDragAcceleratorActive()) {
+ break;
+ }
+ updateSelection(event);
+
+ // No longer dragging to select text, let the parent intercept events.
+ mTextView.getParent().requestDisallowInterceptTouchEvent(false);
+
+ // No longer the first dragging motion, reset.
+ resetDragAcceleratorState();
+
+ if (mTextView.hasSelection()) {
+ // Drag selection should not be adjusted by the text classifier.
+ startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
+ }
+ break;
+ }
+ }
+
+ private void updateSelection(MotionEvent event) {
+ if (mTextView.getLayout() != null) {
+ switch (mDragAcceleratorMode) {
+ case DRAG_ACCELERATOR_MODE_CHARACTER:
+ updateCharacterBasedSelection(event);
+ break;
+ case DRAG_ACCELERATOR_MODE_WORD:
+ updateWordBasedSelection(event);
+ break;
+ case DRAG_ACCELERATOR_MODE_PARAGRAPH:
+ updateParagraphBasedSelection(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * If the TextView allows text selection, selects the current paragraph and starts a drag.
+ *
+ * @return true if the drag was started.
+ */
+ private boolean selectCurrentParagraphAndStartDrag() {
+ if (mInsertionActionModeRunnable != null) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+ stopTextActionMode();
+ if (!selectCurrentParagraph()) {
+ return false;
+ }
+ enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
+ return true;
+ }
+
+ private void updateCharacterBasedSelection(MotionEvent event) {
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+ updateSelectionInternal(mStartOffset, offset,
+ event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+ }
+
+ private void updateWordBasedSelection(MotionEvent event) {
+ if (mHaventMovedEnoughToStartDrag) {
+ return;
+ }
+ final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+ final ViewConfiguration viewConfig = ViewConfiguration.get(
+ mTextView.getContext());
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ final int currLine;
+ if (isMouse) {
+ // No need to offset the y coordinate for mouse input.
+ currLine = mTextView.getLineAtCoordinate(eventY);
+ } else {
+ float y = eventY;
+ if (mSwitchedLines) {
+ // Offset the finger by the same vertical offset as the handles.
+ // This improves visibility of the content being selected by
+ // shifting the finger below the content, this is applied once
+ // the user has switched lines.
+ final int touchSlop = viewConfig.getScaledTouchSlop();
+ final float fingerOffset = (mStartHandle != null)
+ ? mStartHandle.getIdealVerticalOffset()
+ : touchSlop;
+ y = eventY - fingerOffset;
+ }
+
+ currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
+ y);
+ if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
+ // Break early here, we want to offset the finger position from
+ // the selection highlight, once the user moved their finger
+ // to a different line we should apply the offset and *not* switch
+ // lines until recomputing the position with the finger offset.
+ mSwitchedLines = true;
+ return;
+ }
+ }
+
+ int startOffset;
+ int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
+ // Snap to word boundaries.
+ if (mStartOffset < offset) {
+ // Expanding with end handle.
+ offset = getWordEnd(offset);
+ startOffset = getWordStart(mStartOffset);
+ } else {
+ // Expanding with start handle.
+ offset = getWordStart(offset);
+ startOffset = getWordEnd(mStartOffset);
+ if (startOffset == offset) {
+ offset = getNextCursorOffset(offset, false);
+ }
+ }
+ mLineSelectionIsOn = currLine;
+ updateSelectionInternal(startOffset, offset,
+ event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+ }
+
+ private void updateParagraphBasedSelection(MotionEvent event) {
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+
+ final int start = Math.min(offset, mStartOffset);
+ final int end = Math.max(offset, mStartOffset);
+ final long paragraphsRange = getParagraphsRange(start, end);
+ final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+ final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+ updateSelectionInternal(selectionStart, selectionEnd,
+ event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+ }
+
+ private void updateSelectionInternal(int selectionStart, int selectionEnd,
+ boolean fromTouchScreen) {
+ final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
+ && ((mTextView.getSelectionStart() != selectionStart)
+ || (mTextView.getSelectionEnd() != selectionEnd));
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ if (performHapticFeedback) {
+ mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+ }
+ }
+
+ /**
+ * @param event
+ */
+ private void updateMinAndMaxOffsets(MotionEvent event) {
+ int pointerCount = event.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
+ if (offset < mMinTouchOffset) mMinTouchOffset = offset;
+ if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
+ }
+ }
+
+ public int getMinTouchOffset() {
+ return mMinTouchOffset;
+ }
+
+ public int getMaxTouchOffset() {
+ return mMaxTouchOffset;
+ }
+
+ public void resetTouchOffsets() {
+ mMinTouchOffset = mMaxTouchOffset = -1;
+ resetDragAcceleratorState();
+ }
+
+ private void resetDragAcceleratorState() {
+ mStartOffset = -1;
+ mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
+ mSwitchedLines = false;
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+ if (selectionStart > selectionEnd) {
+ Selection.setSelection((Spannable) mTextView.getText(),
+ selectionEnd, selectionStart);
+ }
+ }
+
+ /**
+ * @return true iff this controller is currently used to move the selection start.
+ */
+ public boolean isSelectionStartDragged() {
+ return mStartHandle != null && mStartHandle.isDragging();
+ }
+
+ @Override
+ public boolean isCursorBeingModified() {
+ return isDragAcceleratorActive() || isSelectionStartDragged()
+ || (mEndHandle != null && mEndHandle.isDragging());
+ }
+
+ /**
+ * @return true if the user is selecting text using the drag accelerator.
+ */
+ public boolean isDragAcceleratorActive() {
+ return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
+ }
+
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ hide();
+ }
+ }
+
+ @Override
+ public void onDetached() {
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.removeOnTouchModeChangeListener(this);
+
+ if (mStartHandle != null) mStartHandle.onDetached();
+ if (mEndHandle != null) mEndHandle.onDetached();
+ }
+
+ @Override
+ public boolean isActive() {
+ return mStartHandle != null && mStartHandle.isShowing();
+ }
+
+ public void invalidateHandles() {
+ if (mStartHandle != null) {
+ mStartHandle.invalidate();
+ }
+ if (mEndHandle != null) {
+ mEndHandle.invalidate();
+ }
+ }
+ }
+
+ private class CorrectionHighlighter {
+ private final Path mPath = new Path();
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private int mStart, mEnd;
+ private long mFadingStartTime;
+ private RectF mTempRectF;
+ private static final int FADE_OUT_DURATION = 400;
+
+ public CorrectionHighlighter() {
+ mPaint.setCompatibilityScaling(
+ mTextView.getResources().getCompatibilityInfo().applicationScale);
+ mPaint.setStyle(Paint.Style.FILL);
+ }
+
+ public void highlight(CorrectionInfo info) {
+ mStart = info.getOffset();
+ mEnd = mStart + info.getNewText().length();
+ mFadingStartTime = SystemClock.uptimeMillis();
+
+ if (mStart < 0 || mEnd < 0) {
+ stopAnimation();
+ }
+ }
+
+ public void draw(Canvas canvas, int cursorOffsetVertical) {
+ if (updatePath() && updatePaint()) {
+ if (cursorOffsetVertical != 0) {
+ canvas.translate(0, cursorOffsetVertical);
+ }
+
+ canvas.drawPath(mPath, mPaint);
+
+ if (cursorOffsetVertical != 0) {
+ canvas.translate(0, -cursorOffsetVertical);
+ }
+ invalidate(true); // TODO invalidate cursor region only
+ } else {
+ stopAnimation();
+ invalidate(false); // TODO invalidate cursor region only
+ }
+ }
+
+ private boolean updatePaint() {
+ final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
+ if (duration > FADE_OUT_DURATION) return false;
+
+ final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
+ final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
+ final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
+ + ((int) (highlightColorAlpha * coef) << 24);
+ mPaint.setColor(color);
+ return true;
+ }
+
+ private boolean updatePath() {
+ final Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ // Update in case text is edited while the animation is run
+ final int length = mTextView.getText().length();
+ int start = Math.min(length, mStart);
+ int end = Math.min(length, mEnd);
+
+ mPath.reset();
+ layout.getSelectionPath(start, end, mPath);
+ return true;
+ }
+
+ private void invalidate(boolean delayed) {
+ if (mTextView.getLayout() == null) return;
+
+ if (mTempRectF == null) mTempRectF = new RectF();
+ mPath.computeBounds(mTempRectF, false);
+
+ int left = mTextView.getCompoundPaddingLeft();
+ int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
+
+ if (delayed) {
+ mTextView.postInvalidateOnAnimation(
+ left + (int) mTempRectF.left, top + (int) mTempRectF.top,
+ left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
+ } else {
+ mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
+ (int) mTempRectF.right, (int) mTempRectF.bottom);
+ }
+ }
+
+ private void stopAnimation() {
+ Editor.this.mCorrectionHighlighter = null;
+ }
+ }
+
+ private static class ErrorPopup extends PopupWindow {
+ private boolean mAbove = false;
+ private final TextView mView;
+ private int mPopupInlineErrorBackgroundId = 0;
+ private int mPopupInlineErrorAboveBackgroundId = 0;
+
+ ErrorPopup(TextView v, int width, int height) {
+ super(v, width, height);
+ mView = v;
+ // Make sure the TextView has a background set as it will be used the first time it is
+ // shown and positioned. Initialized with below background, which should have
+ // dimensions identical to the above version for this to work (and is more likely).
+ mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageBackground);
+ mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
+ }
+
+ void fixDirection(boolean above) {
+ mAbove = above;
+
+ if (above) {
+ mPopupInlineErrorAboveBackgroundId =
+ getResourceId(mPopupInlineErrorAboveBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
+ } else {
+ mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+ com.android.internal.R.styleable.Theme_errorMessageBackground);
+ }
+
+ mView.setBackgroundResource(
+ above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
+ }
+
+ private int getResourceId(int currentId, int index) {
+ if (currentId == 0) {
+ TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
+ R.styleable.Theme);
+ currentId = styledAttributes.getResourceId(index, 0);
+ styledAttributes.recycle();
+ }
+ return currentId;
+ }
+
+ @Override
+ public void update(int x, int y, int w, int h, boolean force) {
+ super.update(x, y, w, h, force);
+
+ boolean above = isAboveAnchor();
+ if (above != mAbove) {
+ fixDirection(above);
+ }
+ }
+ }
+
+ static class InputContentType {
+ int imeOptions = EditorInfo.IME_NULL;
+ String privateImeOptions;
+ CharSequence imeActionLabel;
+ int imeActionId;
+ Bundle extras;
+ OnEditorActionListener onEditorActionListener;
+ boolean enterDown;
+ LocaleList imeHintLocales;
+ }
+
+ static class InputMethodState {
+ ExtractedTextRequest mExtractedTextRequest;
+ final ExtractedText mExtractedText = new ExtractedText();
+ int mBatchEditNesting;
+ boolean mCursorChanged;
+ boolean mSelectionModeChanged;
+ boolean mContentChanged;
+ int mChangedStart, mChangedEnd, mChangedDelta;
+ }
+
+ /**
+ * @return True iff (start, end) is a valid range within the text.
+ */
+ private static boolean isValidRange(CharSequence text, int start, int end) {
+ return 0 <= start && start <= end && end <= text.length();
+ }
+
+ @VisibleForTesting
+ public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
+ return mSuggestionsPopupWindow;
+ }
+
+ /**
+ * An InputFilter that monitors text input to maintain undo history. It does not modify the
+ * text being typed (and hence always returns null from the filter() method).
+ *
+ * TODO: Make this span aware.
+ */
+ public static class UndoInputFilter implements InputFilter {
+ private final Editor mEditor;
+
+ // Whether the current filter pass is directly caused by an end-user text edit.
+ private boolean mIsUserEdit;
+
+ // Whether the text field is handling an IME composition. Must be parceled in case the user
+ // rotates the screen during composition.
+ private boolean mHasComposition;
+
+ // Whether the user is expanding or shortening the text
+ private boolean mExpanding;
+
+ // Whether the previous edit operation was in the current batch edit.
+ private boolean mPreviousOperationWasInSameBatchEdit;
+
+ public UndoInputFilter(Editor editor) {
+ mEditor = editor;
+ }
+
+ public void saveInstanceState(Parcel parcel) {
+ parcel.writeInt(mIsUserEdit ? 1 : 0);
+ parcel.writeInt(mHasComposition ? 1 : 0);
+ parcel.writeInt(mExpanding ? 1 : 0);
+ parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
+ }
+
+ public void restoreInstanceState(Parcel parcel) {
+ mIsUserEdit = parcel.readInt() != 0;
+ mHasComposition = parcel.readInt() != 0;
+ mExpanding = parcel.readInt() != 0;
+ mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
+ }
+
+ /**
+ * Signals that a user-triggered edit is starting.
+ */
+ public void beginBatchEdit() {
+ if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
+ mIsUserEdit = true;
+ }
+
+ public void endBatchEdit() {
+ if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
+ mIsUserEdit = false;
+ mPreviousOperationWasInSameBatchEdit = false;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ if (DEBUG_UNDO) {
+ Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
+ + "dest=" + dest + " (" + dstart + "-" + dend + ")");
+ }
+
+ // Check to see if this edit should be tracked for undo.
+ if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
+ return null;
+ }
+
+ final boolean hadComposition = mHasComposition;
+ mHasComposition = isComposition(source);
+ final boolean wasExpanding = mExpanding;
+ boolean shouldCreateSeparateState = false;
+ if ((end - start) != (dend - dstart)) {
+ mExpanding = (end - start) > (dend - dstart);
+ if (hadComposition && mExpanding != wasExpanding) {
+ shouldCreateSeparateState = true;
+ }
+ }
+
+ // Handle edit.
+ handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
+ return null;
+ }
+
+ void freezeLastEdit() {
+ mEditor.mUndoManager.beginUpdate("Edit text");
+ EditOperation lastEdit = getLastEdit();
+ if (lastEdit != null) {
+ lastEdit.mFrozen = true;
+ }
+ mEditor.mUndoManager.endUpdate();
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
+ private @interface MergeMode {}
+ private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
+ private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
+ /** Use {@link EditOperation#mergeWith} to merge */
+ private static final int MERGE_EDIT_MODE_NORMAL = 2;
+
+ private void handleEdit(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
+ // An application may install a TextWatcher to provide additional modifications after
+ // the initial input filters run (e.g. a credit card formatter that adds spaces to a
+ // string). This results in multiple filter() calls for what the user considers to be
+ // a single operation. Always undo the whole set of changes in one step.
+ @MergeMode
+ final int mergeMode;
+ if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
+ mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
+ } else if (shouldCreateSeparateState) {
+ mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
+ } else {
+ mergeMode = MERGE_EDIT_MODE_NORMAL;
+ }
+ // Build a new operation with all the information from this edit.
+ String newText = TextUtils.substring(source, start, end);
+ String oldText = TextUtils.substring(dest, dstart, dend);
+ EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
+ mHasComposition);
+ if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
+ return;
+ }
+ recordEdit(edit, mergeMode);
+ }
+
+ private EditOperation getLastEdit() {
+ final UndoManager um = mEditor.mUndoManager;
+ return um.getLastOperation(
+ EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
+ }
+ /**
+ * Fetches the last undo operation and checks to see if a new edit should be merged into it.
+ * If forceMerge is true then the new edit is always merged.
+ */
+ private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
+ // Fetch the last edit operation and attempt to merge in the new edit.
+ final UndoManager um = mEditor.mUndoManager;
+ um.beginUpdate("Edit text");
+ EditOperation lastEdit = getLastEdit();
+ if (lastEdit == null) {
+ // Add this as the first edit.
+ if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
+ um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+ } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
+ // Forced merges take priority because they could be the result of a non-user-edit
+ // change and this case should not create a new undo operation.
+ if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
+ lastEdit.forceMergeWith(edit);
+ } else if (!mIsUserEdit) {
+ // An application directly modified the Editable outside of a text edit. Treat this
+ // as a new change and don't attempt to merge.
+ if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
+ um.commitState(mEditor.mUndoOwner);
+ um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+ } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
+ // Merge succeeded, nothing else to do.
+ if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
+ } else {
+ // Could not merge with the last edit, so commit the last edit and add this edit.
+ if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
+ um.commitState(mEditor.mUndoOwner);
+ um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+ }
+ mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
+ um.endUpdate();
+ }
+
+ private boolean canUndoEdit(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ if (!mEditor.mAllowUndo) {
+ if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
+ return false;
+ }
+
+ if (mEditor.mUndoManager.isInUndo()) {
+ if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
+ return false;
+ }
+
+ // Text filters run before input operations are applied. However, some input operations
+ // are invalid and will throw exceptions when applied. This is common in tests. Don't
+ // attempt to undo invalid operations.
+ if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
+ if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
+ return false;
+ }
+
+ // Earlier filters can rewrite input to be a no-op, for example due to a length limit
+ // on an input field. Skip no-op changes.
+ if (start == end && dstart == dend) {
+ if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isComposition(CharSequence source) {
+ if (!(source instanceof Spannable)) {
+ return false;
+ }
+ // This is a composition edit if the source has a non-zero-length composing span.
+ Spannable text = (Spannable) source;
+ int composeBegin = EditableInputConnection.getComposingSpanStart(text);
+ int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
+ return composeBegin < composeEnd;
+ }
+
+ private boolean isInTextWatcher() {
+ CharSequence text = mEditor.mTextView.getText();
+ return (text instanceof SpannableStringBuilder)
+ && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
+ }
+ }
+
+ /**
+ * An operation to undo a single "edit" to a text view.
+ */
+ public static class EditOperation extends UndoOperation<Editor> {
+ private static final int TYPE_INSERT = 0;
+ private static final int TYPE_DELETE = 1;
+ private static final int TYPE_REPLACE = 2;
+
+ private int mType;
+ private String mOldText;
+ private String mNewText;
+ private int mStart;
+
+ private int mOldCursorPos;
+ private int mNewCursorPos;
+ private boolean mFrozen;
+ private boolean mIsComposition;
+
+ /**
+ * Constructs an edit operation from a text input operation on editor that replaces the
+ * oldText starting at dstart with newText.
+ */
+ public EditOperation(Editor editor, String oldText, int dstart, String newText,
+ boolean isComposition) {
+ super(editor.mUndoOwner);
+ mOldText = oldText;
+ mNewText = newText;
+
+ // Determine the type of the edit.
+ if (mNewText.length() > 0 && mOldText.length() == 0) {
+ mType = TYPE_INSERT;
+ } else if (mNewText.length() == 0 && mOldText.length() > 0) {
+ mType = TYPE_DELETE;
+ } else {
+ mType = TYPE_REPLACE;
+ }
+
+ mStart = dstart;
+ // Store cursor data.
+ mOldCursorPos = editor.mTextView.getSelectionStart();
+ mNewCursorPos = dstart + mNewText.length();
+ mIsComposition = isComposition;
+ }
+
+ public EditOperation(Parcel src, ClassLoader loader) {
+ super(src, loader);
+ mType = src.readInt();
+ mOldText = src.readString();
+ mNewText = src.readString();
+ mStart = src.readInt();
+ mOldCursorPos = src.readInt();
+ mNewCursorPos = src.readInt();
+ mFrozen = src.readInt() == 1;
+ mIsComposition = src.readInt() == 1;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeString(mOldText);
+ dest.writeString(mNewText);
+ dest.writeInt(mStart);
+ dest.writeInt(mOldCursorPos);
+ dest.writeInt(mNewCursorPos);
+ dest.writeInt(mFrozen ? 1 : 0);
+ dest.writeInt(mIsComposition ? 1 : 0);
+ }
+
+ private int getNewTextEnd() {
+ return mStart + mNewText.length();
+ }
+
+ private int getOldTextEnd() {
+ return mStart + mOldText.length();
+ }
+
+ @Override
+ public void commit() {
+ }
+
+ @Override
+ public void undo() {
+ if (DEBUG_UNDO) Log.d(TAG, "undo");
+ // Remove the new text and insert the old.
+ Editor editor = getOwnerData();
+ Editable text = (Editable) editor.mTextView.getText();
+ modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
+ }
+
+ @Override
+ public void redo() {
+ if (DEBUG_UNDO) Log.d(TAG, "redo");
+ // Remove the old text and insert the new.
+ Editor editor = getOwnerData();
+ Editable text = (Editable) editor.mTextView.getText();
+ modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
+ }
+
+ /**
+ * Attempts to merge this existing operation with a new edit.
+ * @param edit The new edit operation.
+ * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
+ * object unchanged.
+ */
+ private boolean mergeWith(EditOperation edit) {
+ if (DEBUG_UNDO) {
+ Log.d(TAG, "mergeWith old " + this);
+ Log.d(TAG, "mergeWith new " + edit);
+ }
+
+ if (mFrozen) {
+ return false;
+ }
+
+ switch (mType) {
+ case TYPE_INSERT:
+ return mergeInsertWith(edit);
+ case TYPE_DELETE:
+ return mergeDeleteWith(edit);
+ case TYPE_REPLACE:
+ return mergeReplaceWith(edit);
+ default:
+ return false;
+ }
+ }
+
+ private boolean mergeInsertWith(EditOperation edit) {
+ if (edit.mType == TYPE_INSERT) {
+ // Merge insertions that are contiguous even when it's frozen.
+ if (getNewTextEnd() != edit.mStart) {
+ return false;
+ }
+ mNewText += edit.mNewText;
+ mNewCursorPos = edit.mNewCursorPos;
+ mFrozen = edit.mFrozen;
+ mIsComposition = edit.mIsComposition;
+ return true;
+ }
+ if (mIsComposition && edit.mType == TYPE_REPLACE
+ && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
+ // Merge insertion with replace as they can be single insertion.
+ mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
+ + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
+ mNewCursorPos = edit.mNewCursorPos;
+ mIsComposition = edit.mIsComposition;
+ return true;
+ }
+ return false;
+ }
+
+ // TODO: Support forward delete.
+ private boolean mergeDeleteWith(EditOperation edit) {
+ // Only merge continuous deletes.
+ if (edit.mType != TYPE_DELETE) {
+ return false;
+ }
+ // Only merge deletions that are contiguous.
+ if (mStart != edit.getOldTextEnd()) {
+ return false;
+ }
+ mStart = edit.mStart;
+ mOldText = edit.mOldText + mOldText;
+ mNewCursorPos = edit.mNewCursorPos;
+ mIsComposition = edit.mIsComposition;
+ return true;
+ }
+
+ private boolean mergeReplaceWith(EditOperation edit) {
+ if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
+ // Merge with adjacent insert.
+ mNewText += edit.mNewText;
+ mNewCursorPos = edit.mNewCursorPos;
+ return true;
+ }
+ if (!mIsComposition) {
+ return false;
+ }
+ if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
+ && getNewTextEnd() >= edit.getOldTextEnd()) {
+ // Merge with delete as they can be single operation.
+ mNewText = mNewText.substring(0, edit.mStart - mStart)
+ + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
+ if (mNewText.isEmpty()) {
+ mType = TYPE_DELETE;
+ }
+ mNewCursorPos = edit.mNewCursorPos;
+ mIsComposition = edit.mIsComposition;
+ return true;
+ }
+ if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
+ && TextUtils.equals(mNewText, edit.mOldText)) {
+ // Merge with the replace that replaces the same region.
+ mNewText = edit.mNewText;
+ mNewCursorPos = edit.mNewCursorPos;
+ mIsComposition = edit.mIsComposition;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Forcibly creates a single merged edit operation by simulating the entire text
+ * contents being replaced.
+ */
+ public void forceMergeWith(EditOperation edit) {
+ if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
+ if (mergeWith(edit)) {
+ return;
+ }
+ Editor editor = getOwnerData();
+
+ // Copy the text of the current field.
+ // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
+ // but would require two parallel implementations of modifyText() because Editable and
+ // StringBuilder do not share an interface for replace/delete/insert.
+ Editable editable = (Editable) editor.mTextView.getText();
+ Editable originalText = new SpannableStringBuilder(editable.toString());
+
+ // Roll back the last operation.
+ modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
+
+ // Clone the text again and apply the new operation.
+ Editable finalText = new SpannableStringBuilder(editable.toString());
+ modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
+ edit.mNewText, edit.mStart, edit.mNewCursorPos);
+
+ // Convert this operation into a replace operation.
+ mType = TYPE_REPLACE;
+ mNewText = finalText.toString();
+ mOldText = originalText.toString();
+ mStart = 0;
+ mNewCursorPos = edit.mNewCursorPos;
+ mIsComposition = edit.mIsComposition;
+ // mOldCursorPos is unchanged.
+ }
+
+ private static void modifyText(Editable text, int deleteFrom, int deleteTo,
+ CharSequence newText, int newTextInsertAt, int newCursorPos) {
+ // Apply the edit if it is still valid.
+ if (isValidRange(text, deleteFrom, deleteTo)
+ && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
+ if (deleteFrom != deleteTo) {
+ text.delete(deleteFrom, deleteTo);
+ }
+ if (newText.length() != 0) {
+ text.insert(newTextInsertAt, newText);
+ }
+ }
+ // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
+ // don't explicitly set it and rely on SpannableStringBuilder to position it.
+ // TODO: Select all the text that was undone.
+ if (0 <= newCursorPos && newCursorPos <= text.length()) {
+ Selection.setSelection(text, newCursorPos);
+ }
+ }
+
+ private String getTypeString() {
+ switch (mType) {
+ case TYPE_INSERT:
+ return "insert";
+ case TYPE_DELETE:
+ return "delete";
+ case TYPE_REPLACE:
+ return "replace";
+ default:
+ return "";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[mType=" + getTypeString() + ", "
+ + "mOldText=" + mOldText + ", "
+ + "mNewText=" + mNewText + ", "
+ + "mStart=" + mStart + ", "
+ + "mOldCursorPos=" + mOldCursorPos + ", "
+ + "mNewCursorPos=" + mNewCursorPos + ", "
+ + "mFrozen=" + mFrozen + ", "
+ + "mIsComposition=" + mIsComposition + "]";
+ }
+
+ public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
+ new Parcelable.ClassLoaderCreator<EditOperation>() {
+ @Override
+ public EditOperation createFromParcel(Parcel in) {
+ return new EditOperation(in, null);
+ }
+
+ @Override
+ public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
+ return new EditOperation(in, loader);
+ }
+
+ @Override
+ public EditOperation[] newArray(int size) {
+ return new EditOperation[size];
+ }
+ };
+ }
+
+ /**
+ * A helper for enabling and handling "PROCESS_TEXT" menu actions.
+ * These allow external applications to plug into currently selected text.
+ */
+ static final class ProcessTextIntentActionsHandler {
+
+ private final Editor mEditor;
+ private final TextView mTextView;
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+ private final String mPackageName;
+ private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
+ private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
+ new SparseArray<>();
+ private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
+
+ private ProcessTextIntentActionsHandler(Editor editor) {
+ mEditor = Preconditions.checkNotNull(editor);
+ mTextView = Preconditions.checkNotNull(mEditor.mTextView);
+ mContext = Preconditions.checkNotNull(mTextView.getContext());
+ mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
+ mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
+ }
+
+ /**
+ * Adds "PROCESS_TEXT" menu items to the specified menu.
+ */
+ public void onInitializeMenu(Menu menu) {
+ final int size = mSupportedActivities.size();
+ loadSupportedActivities();
+ for (int i = 0; i < size; i++) {
+ final ResolveInfo resolveInfo = mSupportedActivities.get(i);
+ menu.add(Menu.NONE, Menu.NONE,
+ Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
+ getLabel(resolveInfo))
+ .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ }
+
+ /**
+ * Performs a "PROCESS_TEXT" action if there is one associated with the specified
+ * menu item.
+ *
+ * @return True if the action was performed, false otherwise.
+ */
+ public boolean performMenuItemAction(MenuItem item) {
+ return fireIntent(item.getIntent());
+ }
+
+ /**
+ * Initializes and caches "PROCESS_TEXT" accessibility actions.
+ */
+ public void initializeAccessibilityActions() {
+ mAccessibilityIntents.clear();
+ mAccessibilityActions.clear();
+ int i = 0;
+ loadSupportedActivities();
+ for (ResolveInfo resolveInfo : mSupportedActivities) {
+ int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
+ mAccessibilityActions.put(
+ actionId,
+ new AccessibilityNodeInfo.AccessibilityAction(
+ actionId, getLabel(resolveInfo)));
+ mAccessibilityIntents.put(
+ actionId, createProcessTextIntentForResolveInfo(resolveInfo));
+ }
+ }
+
+ /**
+ * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
+ * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
+ * latest accessibility actions available for this call.
+ */
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
+ for (int i = 0; i < mAccessibilityActions.size(); i++) {
+ nodeInfo.addAction(mAccessibilityActions.valueAt(i));
+ }
+ }
+
+ /**
+ * Performs a "PROCESS_TEXT" action if there is one associated with the specified
+ * accessibility action id.
+ *
+ * @return True if the action was performed, false otherwise.
+ */
+ public boolean performAccessibilityAction(int actionId) {
+ return fireIntent(mAccessibilityIntents.get(actionId));
+ }
+
+ private boolean fireIntent(Intent intent) {
+ if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
+ String selectedText = mTextView.getSelectedText();
+ selectedText = TextUtils.trimToParcelableSize(selectedText);
+ intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
+ mEditor.mPreserveSelection = true;
+ mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
+ return true;
+ }
+ return false;
+ }
+
+ private void loadSupportedActivities() {
+ mSupportedActivities.clear();
+ PackageManager packageManager = mTextView.getContext().getPackageManager();
+ List<ResolveInfo> unfiltered =
+ packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ for (ResolveInfo info : unfiltered) {
+ if (isSupportedActivity(info)) {
+ mSupportedActivities.add(info);
+ }
+ }
+ }
+
+ private boolean isSupportedActivity(ResolveInfo info) {
+ return mPackageName.equals(info.activityInfo.packageName)
+ || info.activityInfo.exported
+ && (info.activityInfo.permission == null
+ || mContext.checkSelfPermission(info.activityInfo.permission)
+ == PackageManager.PERMISSION_GRANTED);
+ }
+
+ private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
+ return createProcessTextIntent()
+ .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+
+ private Intent createProcessTextIntent() {
+ return new Intent()
+ .setAction(Intent.ACTION_PROCESS_TEXT)
+ .setType("text/plain");
+ }
+
+ private CharSequence getLabel(ResolveInfo resolveInfo) {
+ return resolveInfo.loadLabel(mPackageManager);
+ }
+ }
+}
diff --git a/android/widget/ExpandableListAdapter.java b/android/widget/ExpandableListAdapter.java
new file mode 100644
index 0000000..7f6781b
--- /dev/null
+++ b/android/widget/ExpandableListAdapter.java
@@ -0,0 +1,209 @@
+/*
+ * 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.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
+ * {@link #getChildView(int, int, boolean, View, ViewGroup)}.
+ *
+ * @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/android/widget/ExpandableListConnector.java b/android/widget/ExpandableListConnector.java
new file mode 100644
index 0000000..bda64ba
--- /dev/null
+++ b/android/widget/ExpandableListConnector.java
@@ -0,0 +1,1035 @@
+/*
+ * 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.os.SystemClock;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/*
+ * 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 final 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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 in 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 PositionMetadata.obtain(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., or null.
+ */
+ 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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 PositionMetadata.obtain(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 PositionMetadata metadata = getUnflattenedPos(flatListPos);
+ final ExpandableListPosition pos = metadata.position;
+
+ boolean retValue;
+ if (pos.type == ExpandableListPosition.CHILD) {
+ retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
+ } else {
+ // Groups are always selectable
+ retValue = true;
+ }
+
+ metadata.recycle();
+
+ return retValue;
+ }
+
+ 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);
+
+ Object retValue;
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ retValue = mExpandableListAdapter
+ .getGroup(posMetadata.position.groupPos);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
+ posMetadata.position.childPos);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+
+ posMetadata.recycle();
+
+ return retValue;
+ }
+
+ public long getItemId(int flatListPos) {
+ final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+ final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
+
+ long retValue;
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
+ posMetadata.position.childPos);
+ retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+
+ posMetadata.recycle();
+
+ return retValue;
+ }
+
+ public View getView(int flatListPos, View convertView, ViewGroup parent) {
+ final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+
+ View retValue;
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
+ posMetadata.isExpanded(), convertView, parent);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
+
+ retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
+ posMetadata.position.childPos, isLastChild, convertView, parent);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+
+ posMetadata.recycle();
+
+ return retValue;
+ }
+
+ @Override
+ public int getItemViewType(int flatListPos) {
+ final PositionMetadata metadata = getUnflattenedPos(flatListPos);
+ final ExpandableListPosition pos = metadata.position;
+
+ int retValue;
+ if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
+ HeterogeneousExpandableList adapter =
+ (HeterogeneousExpandableList) mExpandableListAdapter;
+ if (pos.type == ExpandableListPosition.GROUP) {
+ retValue = adapter.getGroupType(pos.groupPos);
+ } else {
+ final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
+ retValue = adapter.getGroupTypeCount() + childType;
+ }
+ } else {
+ if (pos.type == ExpandableListPosition.GROUP) {
+ retValue = 0;
+ } else {
+ retValue = 1;
+ }
+ }
+
+ metadata.recycle();
+
+ return retValue;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
+ HeterogeneousExpandableList adapter =
+ (HeterogeneousExpandableList) mExpandableListAdapter;
+ return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
+ } else {
+ 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.
+ * @param syncGroupPositions Whether to search for the group positions
+ * based on the group IDs. This should only be needed when calling
+ * this from an onChanged callback.
+ */
+ @SuppressWarnings("unchecked")
+ private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
+ boolean syncGroupPositions) {
+ final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+ int egmlSize = egml.size();
+ int curFlPos = 0;
+
+ /* Update child count as we go through */
+ mTotalExpChildrenCount = 0;
+
+ if (syncGroupPositions) {
+ // We need to check whether any groups have moved positions
+ boolean positionsChanged = false;
+
+ for (int i = egmlSize - 1; i >= 0; i--) {
+ GroupMetadata curGm = egml.get(i);
+ int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
+ if (newGPos != curGm.gPos) {
+ if (newGPos == AdapterView.INVALID_POSITION) {
+ // Doh, just remove it from the list of expanded groups
+ egml.remove(i);
+ egmlSize--;
+ }
+
+ curGm.gPos = newGPos;
+ if (!positionsChanged) positionsChanged = true;
+ }
+ }
+
+ if (positionsChanged) {
+ // At least one group changed positions, so re-sort
+ Collections.sort(egml);
+ }
+ }
+
+ int gChildrenCount;
+ int lastGPos = 0;
+ for (int i = 0; i < egmlSize; i++) {
+ /* Store in local variable since we'll access freq */
+ GroupMetadata 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) {
+ ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+ ExpandableListPosition.GROUP, groupPos, -1, -1);
+ PositionMetadata pm = getFlattenedPos(elGroupPos);
+ elGroupPos.recycle();
+ if (pm == null) return false;
+
+ boolean retValue = collapseGroup(pm);
+ pm.recycle();
+ return retValue;
+ }
+
+ 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, 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) {
+ ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+ ExpandableListPosition.GROUP, groupPos, -1, -1);
+ PositionMetadata pm = getFlattenedPos(elGroupPos);
+ elGroupPos.recycle();
+ boolean retValue = expandGroup(pm);
+ pm.recycle();
+ return retValue;
+ }
+
+ 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 expanded 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 = GroupMetadata.obtain(
+ GroupMetadata.REFRESH,
+ GroupMetadata.REFRESH,
+ posMetadata.position.groupPos,
+ mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
+
+ mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
+
+ // Refresh the metadata
+ refreshExpGroupMetadataList(false, 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, false);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ ExpandableListAdapter adapter = getAdapter();
+ return adapter != null ? adapter.isEmpty() : true;
+ }
+
+ /**
+ * Searches the expandable list adapter for a group position matching the
+ * given group ID. The search starts at the given seed position 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 the given row ID, or
+ * {@link AdapterView#INVALID_POSITION} if it can't be found
+ * @see AdapterView#findSyncPosition()
+ */
+ int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
+ int count = mExpandableListAdapter.getGroupCount();
+
+ if (count == 0) {
+ return AdapterView.INVALID_POSITION;
+ }
+
+ // If there isn't a selection don't hunt for it
+ if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
+ return AdapterView.INVALID_POSITION;
+ }
+
+ // Pin seed to reasonable values
+ seedGroupPosition = Math.max(0, seedGroupPosition);
+ seedGroupPosition = Math.min(count - 1, seedGroupPosition);
+
+ long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
+
+ long rowId;
+
+ // first position scanned so far
+ int first = seedGroupPosition;
+
+ // last position scanned so far
+ int last = seedGroupPosition;
+
+ // 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
+ ExpandableListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return AdapterView.INVALID_POSITION;
+ }
+
+ while (SystemClock.uptimeMillis() <= endTime) {
+ rowId = adapter.getGroupId(seedGroupPosition);
+ if (rowId == groupIdToMatch) {
+ // Found it!
+ return seedGroupPosition;
+ }
+
+ 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++;
+ seedGroupPosition = 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--;
+ seedGroupPosition = first;
+ // Try going down next time
+ next = true;
+ }
+
+ }
+
+ return AdapterView.INVALID_POSITION;
+ }
+
+ protected class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ refreshExpGroupMetadataList(true, true);
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ refreshExpGroupMetadataList(true, 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, Comparable<GroupMetadata> {
+ 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;
+
+ /**
+ * This group's id
+ */
+ long gId;
+
+ private GroupMetadata() {
+ }
+
+ static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
+ GroupMetadata gm = new GroupMetadata();
+ gm.flPos = flPos;
+ gm.lastChildFlPos = lastChildFlPos;
+ gm.gPos = gPos;
+ gm.gId = gId;
+ return gm;
+ }
+
+ public int compareTo(GroupMetadata another) {
+ if (another == null) {
+ throw new IllegalArgumentException();
+ }
+
+ return gPos - another.gPos;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(flPos);
+ dest.writeInt(lastChildFlPos);
+ dest.writeInt(gPos);
+ dest.writeLong(gId);
+ }
+
+ public static final Parcelable.Creator<GroupMetadata> CREATOR =
+ new Parcelable.Creator<GroupMetadata>() {
+
+ public GroupMetadata createFromParcel(Parcel in) {
+ GroupMetadata gm = GroupMetadata.obtain(
+ in.readInt(),
+ in.readInt(),
+ in.readInt(),
+ in.readLong());
+ 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 {
+
+ private static final int MAX_POOL_SIZE = 5;
+ private static ArrayList<PositionMetadata> sPool =
+ new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
+
+ /** 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;
+
+ private void resetState() {
+ if (position != null) {
+ position.recycle();
+ position = null;
+ }
+ groupMetadata = null;
+ groupInsertIndex = 0;
+ }
+
+ /**
+ * Use {@link #obtain(int, int, int, int, GroupMetadata, int)}
+ */
+ private PositionMetadata() {
+ }
+
+ static PositionMetadata obtain(int flatListPos, int type, int groupPos,
+ int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
+ PositionMetadata pm = getRecycledOrCreate();
+ pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos);
+ pm.groupMetadata = groupMetadata;
+ pm.groupInsertIndex = groupInsertIndex;
+ return pm;
+ }
+
+ private static PositionMetadata getRecycledOrCreate() {
+ PositionMetadata pm;
+ synchronized (sPool) {
+ if (sPool.size() > 0) {
+ pm = sPool.remove(0);
+ } else {
+ return new PositionMetadata();
+ }
+ }
+ pm.resetState();
+ return pm;
+ }
+
+ public void recycle() {
+ resetState();
+ synchronized (sPool) {
+ if (sPool.size() < MAX_POOL_SIZE) {
+ sPool.add(this);
+ }
+ }
+ }
+
+ /**
+ * 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/android/widget/ExpandableListPosition.java b/android/widget/ExpandableListPosition.java
new file mode 100644
index 0000000..bb68da6
--- /dev/null
+++ b/android/widget/ExpandableListPosition.java
@@ -0,0 +1,139 @@
+/*
+ * 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 java.util.ArrayList;
+
+/**
+ * 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 {
+
+ private static final int MAX_POOL_SIZE = 5;
+ private static ArrayList<ExpandableListPosition> sPool =
+ new ArrayList<ExpandableListPosition>(MAX_POOL_SIZE);
+
+ /**
+ * 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;
+
+ private void resetState() {
+ groupPos = 0;
+ childPos = 0;
+ flatListPos = 0;
+ type = 0;
+ }
+
+ private ExpandableListPosition() {
+ }
+
+ long getPackedPosition() {
+ if (type == CHILD) return ExpandableListView.getPackedPositionForChild(groupPos, childPos);
+ else return ExpandableListView.getPackedPositionForGroup(groupPos);
+ }
+
+ static ExpandableListPosition obtainGroupPosition(int groupPosition) {
+ return obtain(GROUP, groupPosition, 0, 0);
+ }
+
+ static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) {
+ return obtain(CHILD, groupPosition, childPosition, 0);
+ }
+
+ static ExpandableListPosition obtainPosition(long packedPosition) {
+ if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) {
+ return null;
+ }
+
+ ExpandableListPosition elp = getRecycledOrCreate();
+ elp.groupPos = ExpandableListView.getPackedPositionGroup(packedPosition);
+ if (ExpandableListView.getPackedPositionType(packedPosition) ==
+ ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+ elp.type = CHILD;
+ elp.childPos = ExpandableListView.getPackedPositionChild(packedPosition);
+ } else {
+ elp.type = GROUP;
+ }
+ return elp;
+ }
+
+ static ExpandableListPosition obtain(int type, int groupPos, int childPos, int flatListPos) {
+ ExpandableListPosition elp = getRecycledOrCreate();
+ elp.type = type;
+ elp.groupPos = groupPos;
+ elp.childPos = childPos;
+ elp.flatListPos = flatListPos;
+ return elp;
+ }
+
+ private static ExpandableListPosition getRecycledOrCreate() {
+ ExpandableListPosition elp;
+ synchronized (sPool) {
+ if (sPool.size() > 0) {
+ elp = sPool.remove(0);
+ } else {
+ return new ExpandableListPosition();
+ }
+ }
+ elp.resetState();
+ return elp;
+ }
+
+ /**
+ * Do not call this unless you obtained this via ExpandableListPosition.obtain().
+ * PositionMetadata will handle recycling its own children.
+ */
+ public void recycle() {
+ synchronized (sPool) {
+ if (sPool.size() < MAX_POOL_SIZE) {
+ sPool.add(this);
+ }
+ }
+ }
+}
diff --git a/android/widget/ExpandableListView.java b/android/widget/ExpandableListView.java
new file mode 100644
index 0000000..8d9848d
--- /dev/null
+++ b/android/widget/ExpandableListView.java
@@ -0,0 +1,1346 @@
+/*
+ * 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 static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+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.ContextMenu.ContextMenuInfo;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.widget.ExpandableListConnector.PositionMetadata;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * 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
+ * @attr ref android.R.styleable#ExpandableListView_indicatorStart
+ * @attr ref android.R.styleable#ExpandableListView_indicatorEnd
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorStart
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorEnd
+ */
+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;
+
+ /** Start bound for drawing the indicator. */
+ private int mIndicatorStart;
+
+ /** End bound for drawing the indicator. */
+ private int mIndicatorEnd;
+
+ /**
+ * 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;
+
+ /**
+ * Start bound for drawing the indicator of a child. Value of
+ * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorStart.
+ */
+ private int mChildIndicatorStart;
+
+ /**
+ * End bound for drawing the indicator of a child. Value of
+ * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorEnd.
+ */
+ private int mChildIndicatorEnd;
+
+ /**
+ * Denotes when a child indicator should inherit this bound from the generic
+ * indicator bounds
+ */
+ public static final int CHILD_INDICATOR_INHERIT = -1;
+
+ /**
+ * Denotes an undefined value for an indicator
+ */
+ private static final int INDICATOR_UNDEFINED = -2;
+
+ /** 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;
+
+ // Bounds of the indicator to be drawn
+ private final Rect mIndicatorRect = new Rect();
+
+ 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ExpandableListView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes);
+
+ 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);
+ if (mIndicatorRight == 0 && mGroupIndicator != null) {
+ mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+ }
+ 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);
+
+ if (!isRtlCompatibilityMode()) {
+ mIndicatorStart = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_indicatorStart,
+ INDICATOR_UNDEFINED);
+ mIndicatorEnd = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_indicatorEnd,
+ INDICATOR_UNDEFINED);
+
+ mChildIndicatorStart = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_childIndicatorStart,
+ CHILD_INDICATOR_INHERIT);
+ mChildIndicatorEnd = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_childIndicatorEnd,
+ CHILD_INDICATOR_INHERIT);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or
+ * RTL not supported)
+ */
+ private boolean isRtlCompatibilityMode() {
+ final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+ return targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport();
+ }
+
+ /**
+ * Return true if the application tag in the AndroidManifest has set "supportRtl" to true
+ */
+ private boolean hasRtlSupport() {
+ return mContext.getApplicationInfo().hasRtlSupport();
+ }
+
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ resolveIndicator();
+ resolveChildIndicator();
+ }
+
+ /**
+ * Resolve start/end indicator. start/end indicator always takes precedence over left/right
+ * indicator when defined.
+ */
+ private void resolveIndicator() {
+ final boolean isLayoutRtl = isLayoutRtl();
+ if (isLayoutRtl) {
+ if (mIndicatorStart >= 0) {
+ mIndicatorRight = mIndicatorStart;
+ }
+ if (mIndicatorEnd >= 0) {
+ mIndicatorLeft = mIndicatorEnd;
+ }
+ } else {
+ if (mIndicatorStart >= 0) {
+ mIndicatorLeft = mIndicatorStart;
+ }
+ if (mIndicatorEnd >= 0) {
+ mIndicatorRight = mIndicatorEnd;
+ }
+ }
+ if (mIndicatorRight == 0 && mGroupIndicator != null) {
+ mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+ }
+ }
+
+ /**
+ * Resolve start/end child indicator. start/end child indicator always takes precedence over
+ * left/right child indicator when defined.
+ */
+ private void resolveChildIndicator() {
+ final boolean isLayoutRtl = isLayoutRtl();
+ if (isLayoutRtl) {
+ if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
+ mChildIndicatorRight = mChildIndicatorStart;
+ }
+ if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
+ mChildIndicatorLeft = mChildIndicatorEnd;
+ }
+ } else {
+ if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
+ mChildIndicatorLeft = mChildIndicatorStart;
+ }
+ if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
+ mChildIndicatorRight = mChildIndicatorEnd;
+ }
+ }
+ }
+
+ @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);
+
+ final Rect indicatorRect = mIndicatorRect;
+
+ // 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);
+
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int width = getWidth();
+
+ // 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;
+ }
+
+ if (isLayoutRtl) {
+ final int temp = indicatorRect.left;
+ indicatorRect.left = width - indicatorRect.right;
+ indicatorRect.right = width - temp;
+
+ indicatorRect.left -= mPaddingRight;
+ indicatorRect.right -= mPaddingRight;
+ } else {
+ indicatorRect.left += mPaddingLeft;
+ indicatorRect.right += mPaddingLeft;
+ }
+
+ lastItemType = pos.position.type;
+ }
+
+ if (indicatorRect.left != indicatorRect.right) {
+ // 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) {
+ // Draw the indicator
+ indicator.setBounds(indicatorRect);
+ indicator.draw(canvas);
+ }
+ }
+ pos.recycle();
+ }
+
+ 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) {
+ final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+ PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition);
+ // 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
+ final Drawable divider = mChildDivider;
+ divider.setBounds(bounds);
+ divider.draw(canvas);
+ pos.recycle();
+ return;
+ }
+ pos.recycle();
+ }
+
+ // 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;
+ }
+
+ /**
+ * @param position An absolute (including header and footer) flat list position.
+ * @return true if the position corresponds to a header or a footer item.
+ */
+ private boolean isHeaderOrFooterPosition(int position) {
+ final int footerViewsStart = mItemCount - getFooterViewsCount();
+ return (position < getHeaderViewsCount() || position >= footerViewsStart);
+ }
+
+ /**
+ * Converts an absolute item flat position into a group/child flat position, shifting according
+ * to the number of header items.
+ *
+ * @param flatListPosition The absolute flat position
+ * @return A group/child flat position as expected by the connector.
+ */
+ private int getFlatPositionForConnector(int flatListPosition) {
+ return flatListPosition - getHeaderViewsCount();
+ }
+
+ /**
+ * Converts a group/child flat position into an absolute flat position, that takes into account
+ * the possible headers.
+ *
+ * @param flatListPosition The child/group flat position
+ * @return An absolute flat position.
+ */
+ private int getAbsoluteFlatPosition(int flatListPosition) {
+ return flatListPosition + getHeaderViewsCount();
+ }
+
+ @Override
+ public boolean performItemClick(View v, int position, long id) {
+ // Ignore clicks in header/footers
+ if (isHeaderOrFooterPosition(position)) {
+ // Clicked on a header/footer, so ignore pass it on to super
+ return super.performItemClick(v, position, id);
+ }
+
+ // Internally handle the item click
+ final int adjustedPosition = getFlatPositionForConnector(position);
+ return handleItemClick(v, adjustedPosition, 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);
+
+ boolean returnValue;
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ /* It's a group, so handle collapsing/expanding */
+
+ /* It's a group click, so pass on event */
+ if (mOnGroupClickListener != null) {
+ if (mOnGroupClickListener.onGroupClick(this, v,
+ posMetadata.position.groupPos, id)) {
+ posMetadata.recycle();
+ return true;
+ }
+ }
+
+ if (posMetadata.isExpanded()) {
+ /* Collapse it */
+ mConnector.collapseGroup(posMetadata);
+
+ playSoundEffect(SoundEffectConstants.CLICK);
+
+ if (mOnGroupCollapseListener != null) {
+ mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
+ }
+ } else {
+ /* Expand it */
+ mConnector.expandGroup(posMetadata);
+
+ playSoundEffect(SoundEffectConstants.CLICK);
+
+ if (mOnGroupExpandListener != null) {
+ mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
+ }
+
+ final int groupPos = posMetadata.position.groupPos;
+ final int groupFlatPos = posMetadata.position.flatListPos;
+
+ final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
+ smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
+ shiftedGroupPosition);
+ }
+
+ returnValue = 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);
+ }
+
+ returnValue = false;
+ }
+
+ posMetadata.recycle();
+
+ return returnValue;
+ }
+
+ /**
+ * 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) {
+ return expandGroup(groupPos, false);
+ }
+
+ /**
+ * Expand a group in the grouped list view
+ *
+ * @param groupPos the group to be expanded
+ * @param animate true if the expanding group should be animated in
+ * @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 animate) {
+ ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+ ExpandableListPosition.GROUP, groupPos, -1, -1);
+ PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
+ elGroupPos.recycle();
+ boolean retValue = mConnector.expandGroup(pm);
+
+ if (mOnGroupExpandListener != null) {
+ mOnGroupExpandListener.onGroupExpand(groupPos);
+ }
+
+ if (animate) {
+ final int groupFlatPos = pm.position.flatListPos;
+
+ final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
+ smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
+ shiftedGroupPosition);
+ }
+ pm.recycle();
+
+ 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 a 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. #PACKED_POSITION_VALUE_NULL if
+ * the position corresponds to a header or a footer item.
+ */
+ public long getExpandableListPosition(int flatListPosition) {
+ if (isHeaderOrFooterPosition(flatListPosition)) {
+ return PACKED_POSITION_VALUE_NULL;
+ }
+
+ final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+ PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
+ long packedPos = pm.position.getPackedPosition();
+ pm.recycle();
+ return packedPos;
+ }
+
+ /**
+ * 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) {
+ ExpandableListPosition elPackedPos = ExpandableListPosition
+ .obtainPosition(packedPosition);
+ PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos);
+ elPackedPos.recycle();
+ final int flatListPosition = pm.position.flatListPos;
+ pm.recycle();
+ return getAbsoluteFlatPosition(flatListPosition);
+ }
+
+ /**
+ * 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
+ * or if selection is on a header or a footer item.
+ */
+ public long getSelectedPosition() {
+ final int selectedPos = getSelectedItemPosition();
+
+ // The case where there is no selection (selectedPos == -1) is also handled here.
+ 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);
+ PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
+ elGroupPos.recycle();
+ final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos);
+ super.setSelection(absoluteFlatPosition);
+ pm.recycle();
+ }
+
+ /**
+ * 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");
+ }
+ }
+
+ int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos);
+ super.setSelection(absoluteFlatPosition);
+
+ elChildPos.recycle();
+ flatChildPos.recycle();
+
+ 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) {
+ if (isHeaderOrFooterPosition(flatListPosition)) {
+ // Return normal info for header/footer view context menus
+ return new AdapterContextMenuInfo(view, flatListPosition, id);
+ }
+
+ final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+ PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
+ ExpandableListPosition pos = pm.position;
+
+ id = getChildOrGroupId(pos);
+ long packedPosition = pos.getPackedPosition();
+
+ pm.recycle();
+
+ return new ExpandableListContextMenuInfo(view, packedPosition, 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;
+ resolveChildIndicator();
+ }
+
+ /**
+ * Sets the relative 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 start The start position (relative to the start bounds of this View)
+ * to start drawing the indicator.
+ * @param end The end position (relative to the end bounds of this
+ * View) to end the drawing of the indicator.
+ */
+ public void setChildIndicatorBoundsRelative(int start, int end) {
+ mChildIndicatorStart = start;
+ mChildIndicatorEnd = end;
+ resolveChildIndicator();
+ }
+
+ /**
+ * 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;
+ if (mIndicatorRight == 0 && mGroupIndicator != null) {
+ mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+ }
+ }
+
+ /**
+ * 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;
+ resolveIndicator();
+ }
+
+ /**
+ * Sets the relative 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 start The start position (relative to the start bounds of this View)
+ * to start drawing the indicator.
+ * @param end The end position (relative to the end bounds of this
+ * View) to end the drawing of the indicator.
+ */
+ public void setIndicatorBoundsRelative(int start, int end) {
+ mIndicatorStart = start;
+ mIndicatorEnd = end;
+ resolveIndicator();
+ }
+
+ /**
+ * 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) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (mConnector != null && ss.expandedGroupMetadataList != null) {
+ mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ExpandableListView.class.getName();
+ }
+}
diff --git a/android/widget/FastScroller.java b/android/widget/FastScroller.java
new file mode 100644
index 0000000..198bf27
--- /dev/null
+++ b/android/widget/FastScroller.java
@@ -0,0 +1,1681 @@
+/*
+ * 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.IntProperty;
+import android.util.MathUtils;
+import android.util.Property;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroupOverlay;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.ImageView.ScaleType;
+
+import com.android.internal.R;
+
+/**
+ * Helper class for AbsListView to draw and control the Fast Scroll thumb
+ */
+class FastScroller {
+ /** Duration of fade-out animation. */
+ private static final int DURATION_FADE_OUT = 300;
+
+ /** Duration of fade-in animation. */
+ private static final int DURATION_FADE_IN = 150;
+
+ /** Duration of transition cross-fade animation. */
+ private static final int DURATION_CROSS_FADE = 50;
+
+ /** Duration of transition resize animation. */
+ private static final int DURATION_RESIZE = 100;
+
+ /** Inactivity timeout before fading controls. */
+ private static final long FADE_TIMEOUT = 1500;
+
+ /** Minimum number of pages to justify showing a fast scroll thumb. */
+ private static final int MIN_PAGES = 4;
+
+ /** Scroll thumb and preview not showing. */
+ private static final int STATE_NONE = 0;
+
+ /** Scroll thumb visible and moving along with the scrollbar. */
+ private static final int STATE_VISIBLE = 1;
+
+ /** Scroll thumb and preview being dragged by user. */
+ private static final int STATE_DRAGGING = 2;
+
+ // Positions for preview image and text.
+ private static final int OVERLAY_FLOATING = 0;
+ private static final int OVERLAY_AT_THUMB = 1;
+ private static final int OVERLAY_ABOVE_THUMB = 2;
+
+ // Positions for thumb in relation to track.
+ private static final int THUMB_POSITION_MIDPOINT = 0;
+ private static final int THUMB_POSITION_INSIDE = 1;
+
+ // Indices for mPreviewResId.
+ private static final int PREVIEW_LEFT = 0;
+ private static final int PREVIEW_RIGHT = 1;
+
+ /** Delay before considering a tap in the thumb area to be a drag. */
+ private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+
+ private final Rect mTempBounds = new Rect();
+ private final Rect mTempMargins = new Rect();
+ private final Rect mContainerRect = new Rect();
+
+ private final AbsListView mList;
+ private final ViewGroupOverlay mOverlay;
+ private final TextView mPrimaryText;
+ private final TextView mSecondaryText;
+ private final ImageView mThumbImage;
+ private final ImageView mTrackImage;
+ private final View mPreviewImage;
+ /**
+ * Preview image resource IDs for left- and right-aligned layouts. See
+ * {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}.
+ */
+ private final int[] mPreviewResId = new int[2];
+
+ /** The minimum touch target size in pixels. */
+ private final int mMinimumTouchTarget;
+
+ /**
+ * Padding in pixels around the preview text. Applied as layout margins to
+ * the preview text and padding to the preview image.
+ */
+ private int mPreviewPadding;
+
+ private int mPreviewMinWidth;
+ private int mPreviewMinHeight;
+ private int mThumbMinWidth;
+ private int mThumbMinHeight;
+
+ /** Theme-specified text size. Used only if text appearance is not set. */
+ private float mTextSize;
+
+ /** Theme-specified text color. Used only if text appearance is not set. */
+ private ColorStateList mTextColor;
+
+ private Drawable mThumbDrawable;
+ private Drawable mTrackDrawable;
+ private int mTextAppearance;
+ private int mThumbPosition;
+
+ // Used to convert between y-coordinate and thumb position within track.
+ private float mThumbOffset;
+ private float mThumbRange;
+
+ /** Total width of decorations. */
+ private int mWidth;
+
+ /** Set containing decoration transition animations. */
+ private AnimatorSet mDecorAnimation;
+
+ /** Set containing preview text transition animations. */
+ private AnimatorSet mPreviewAnimation;
+
+ /** Whether the primary text is showing. */
+ private boolean mShowingPrimary;
+
+ /** Whether we're waiting for completion of scrollTo(). */
+ private boolean mScrollCompleted;
+
+ /** The position of the first visible item in the list. */
+ private int mFirstVisibleItem;
+
+ /** The number of headers at the top of the view. */
+ private int mHeaderCount;
+
+ /** The index of the current section. */
+ private int mCurrentSection = -1;
+
+ /** The current scrollbar position. */
+ private int mScrollbarPosition = -1;
+
+ /** Whether the list is long enough to need a fast scroller. */
+ private boolean mLongList;
+
+ private Object[] mSections;
+
+ /** Whether this view is currently performing layout. */
+ private boolean mUpdatingLayout;
+
+ /**
+ * Current decoration state, one of:
+ * <ul>
+ * <li>{@link #STATE_NONE}, nothing visible
+ * <li>{@link #STATE_VISIBLE}, showing track and thumb
+ * <li>{@link #STATE_DRAGGING}, visible and showing preview
+ * </ul>
+ */
+ private int mState;
+
+ /** Whether the preview image is visible. */
+ private boolean mShowingPreview;
+
+ private Adapter mListAdapter;
+ private SectionIndexer mSectionIndexer;
+
+ /** Whether decorations should be laid out from right to left. */
+ private boolean mLayoutFromRight;
+
+ /** Whether the fast scroller is enabled. */
+ private boolean mEnabled;
+
+ /** Whether the scrollbar and decorations should always be shown. */
+ private boolean mAlwaysShow;
+
+ /**
+ * Position for the preview image and text. One of:
+ * <ul>
+ * <li>{@link #OVERLAY_FLOATING}
+ * <li>{@link #OVERLAY_AT_THUMB}
+ * <li>{@link #OVERLAY_ABOVE_THUMB}
+ * </ul>
+ */
+ private int mOverlayPosition;
+
+ /** Current scrollbar style, including inset and overlay properties. */
+ private int mScrollBarStyle;
+
+ /** Whether to precisely match the thumb position to the list. */
+ private boolean mMatchDragPosition;
+
+ private float mInitialTouchY;
+ private long mPendingDrag = -1;
+ private int mScaledTouchSlop;
+
+ private int mOldItemCount;
+ private int mOldChildCount;
+
+ /**
+ * Used to delay hiding fast scroll decorations.
+ */
+ private final Runnable mDeferHide = new Runnable() {
+ @Override
+ public void run() {
+ setState(STATE_NONE);
+ }
+ };
+
+ /**
+ * Used to effect a transition from primary to secondary text.
+ */
+ private final AnimatorListener mSwitchPrimaryListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mShowingPrimary = !mShowingPrimary;
+ }
+ };
+
+ public FastScroller(AbsListView listView, int styleResId) {
+ mList = listView;
+ mOldItemCount = listView.getCount();
+ mOldChildCount = listView.getChildCount();
+
+ final Context context = listView.getContext();
+ mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mScrollBarStyle = listView.getScrollBarStyle();
+
+ mScrollCompleted = true;
+ mState = STATE_VISIBLE;
+ mMatchDragPosition =
+ context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
+
+ mTrackImage = new ImageView(context);
+ mTrackImage.setScaleType(ScaleType.FIT_XY);
+ mThumbImage = new ImageView(context);
+ mThumbImage.setScaleType(ScaleType.FIT_XY);
+ mPreviewImage = new View(context);
+ mPreviewImage.setAlpha(0f);
+
+ mPrimaryText = createPreviewTextView(context);
+ mSecondaryText = createPreviewTextView(context);
+
+ mMinimumTouchTarget = listView.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.fast_scroller_minimum_touch_target);
+
+ setStyle(styleResId);
+
+ final ViewGroupOverlay overlay = listView.getOverlay();
+ mOverlay = overlay;
+ overlay.add(mTrackImage);
+ overlay.add(mThumbImage);
+ overlay.add(mPreviewImage);
+ overlay.add(mPrimaryText);
+ overlay.add(mSecondaryText);
+
+ getSectionsFromIndexer();
+ updateLongList(mOldChildCount, mOldItemCount);
+ setScrollbarPosition(listView.getVerticalScrollbarPosition());
+ postAutoHide();
+ }
+
+ private void updateAppearance() {
+ int width = 0;
+
+ // Add track to overlay if it has an image.
+ mTrackImage.setImageDrawable(mTrackDrawable);
+ if (mTrackDrawable != null) {
+ width = Math.max(width, mTrackDrawable.getIntrinsicWidth());
+ }
+
+ // Add thumb to overlay if it has an image.
+ mThumbImage.setImageDrawable(mThumbDrawable);
+ mThumbImage.setMinimumWidth(mThumbMinWidth);
+ mThumbImage.setMinimumHeight(mThumbMinHeight);
+ if (mThumbDrawable != null) {
+ width = Math.max(width, mThumbDrawable.getIntrinsicWidth());
+ }
+
+ // Account for minimum thumb width.
+ mWidth = Math.max(width, mThumbMinWidth);
+
+ if (mTextAppearance != 0) {
+ mPrimaryText.setTextAppearance(mTextAppearance);
+ mSecondaryText.setTextAppearance(mTextAppearance);
+ }
+
+ if (mTextColor != null) {
+ mPrimaryText.setTextColor(mTextColor);
+ mSecondaryText.setTextColor(mTextColor);
+ }
+
+ if (mTextSize > 0) {
+ mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ }
+
+ final int padding = mPreviewPadding;
+ mPrimaryText.setIncludeFontPadding(false);
+ mPrimaryText.setPadding(padding, padding, padding, padding);
+ mSecondaryText.setIncludeFontPadding(false);
+ mSecondaryText.setPadding(padding, padding, padding, padding);
+
+ refreshDrawablePressedState();
+ }
+
+ public void setStyle(@StyleRes int resId) {
+ final Context context = mList.getContext();
+ final TypedArray ta = context.obtainStyledAttributes(null,
+ R.styleable.FastScroll, R.attr.fastScrollStyle, resId);
+ final int N = ta.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ final int index = ta.getIndex(i);
+ switch (index) {
+ case R.styleable.FastScroll_position:
+ mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING);
+ break;
+ case R.styleable.FastScroll_backgroundLeft:
+ mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0);
+ break;
+ case R.styleable.FastScroll_backgroundRight:
+ mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0);
+ break;
+ case R.styleable.FastScroll_thumbDrawable:
+ mThumbDrawable = ta.getDrawable(index);
+ break;
+ case R.styleable.FastScroll_trackDrawable:
+ mTrackDrawable = ta.getDrawable(index);
+ break;
+ case R.styleable.FastScroll_textAppearance:
+ mTextAppearance = ta.getResourceId(index, 0);
+ break;
+ case R.styleable.FastScroll_textColor:
+ mTextColor = ta.getColorStateList(index);
+ break;
+ case R.styleable.FastScroll_textSize:
+ mTextSize = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_minWidth:
+ mPreviewMinWidth = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_minHeight:
+ mPreviewMinHeight = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_thumbMinWidth:
+ mThumbMinWidth = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_thumbMinHeight:
+ mThumbMinHeight = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_padding:
+ mPreviewPadding = ta.getDimensionPixelSize(index, 0);
+ break;
+ case R.styleable.FastScroll_thumbPosition:
+ mThumbPosition = ta.getInt(index, THUMB_POSITION_MIDPOINT);
+ break;
+ }
+ }
+ ta.recycle();
+
+ updateAppearance();
+ }
+
+ /**
+ * Removes this FastScroller overlay from the host view.
+ */
+ public void remove() {
+ mOverlay.remove(mTrackImage);
+ mOverlay.remove(mThumbImage);
+ mOverlay.remove(mPreviewImage);
+ mOverlay.remove(mPrimaryText);
+ mOverlay.remove(mSecondaryText);
+ }
+
+ /**
+ * @param enabled Whether the fast scroll thumb is enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+
+ onStateDependencyChanged(true);
+ }
+ }
+
+ /**
+ * @return Whether the fast scroll thumb is enabled.
+ */
+ public boolean isEnabled() {
+ return mEnabled && (mLongList || mAlwaysShow);
+ }
+
+ /**
+ * @param alwaysShow Whether the fast scroll thumb should always be shown
+ */
+ public void setAlwaysShow(boolean alwaysShow) {
+ if (mAlwaysShow != alwaysShow) {
+ mAlwaysShow = alwaysShow;
+
+ onStateDependencyChanged(false);
+ }
+ }
+
+ /**
+ * @return Whether the fast scroll thumb will always be shown
+ * @see #setAlwaysShow(boolean)
+ */
+ public boolean isAlwaysShowEnabled() {
+ return mAlwaysShow;
+ }
+
+ /**
+ * Called when one of the variables affecting enabled state changes.
+ *
+ * @param peekIfEnabled whether the thumb should peek, if enabled
+ */
+ private void onStateDependencyChanged(boolean peekIfEnabled) {
+ if (isEnabled()) {
+ if (isAlwaysShowEnabled()) {
+ setState(STATE_VISIBLE);
+ } else if (mState == STATE_VISIBLE) {
+ postAutoHide();
+ } else if (peekIfEnabled) {
+ setState(STATE_VISIBLE);
+ postAutoHide();
+ }
+ } else {
+ stop();
+ }
+
+ mList.resolvePadding();
+ }
+
+ public void setScrollBarStyle(int style) {
+ if (mScrollBarStyle != style) {
+ mScrollBarStyle = style;
+
+ updateLayout();
+ }
+ }
+
+ /**
+ * Immediately transitions the fast scroller decorations to a hidden state.
+ */
+ public void stop() {
+ setState(STATE_NONE);
+ }
+
+ public void setScrollbarPosition(int position) {
+ if (position == View.SCROLLBAR_POSITION_DEFAULT) {
+ position = mList.isLayoutRtl() ?
+ View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
+ }
+
+ if (mScrollbarPosition != position) {
+ mScrollbarPosition = position;
+ mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
+
+ final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
+ mPreviewImage.setBackgroundResource(previewResId);
+
+ // Propagate padding to text min width/height.
+ final int textMinWidth = Math.max(0, mPreviewMinWidth - mPreviewImage.getPaddingLeft()
+ - mPreviewImage.getPaddingRight());
+ mPrimaryText.setMinimumWidth(textMinWidth);
+ mSecondaryText.setMinimumWidth(textMinWidth);
+
+ final int textMinHeight = Math.max(0, mPreviewMinHeight - mPreviewImage.getPaddingTop()
+ - mPreviewImage.getPaddingBottom());
+ mPrimaryText.setMinimumHeight(textMinHeight);
+ mSecondaryText.setMinimumHeight(textMinHeight);
+
+ // Requires re-layout.
+ updateLayout();
+ }
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateLayout();
+ }
+
+ public void onItemCountChanged(int childCount, int itemCount) {
+ if (mOldItemCount != itemCount || mOldChildCount != childCount) {
+ mOldItemCount = itemCount;
+ mOldChildCount = childCount;
+
+ final boolean hasMoreItems = itemCount - childCount > 0;
+ if (hasMoreItems && mState != STATE_DRAGGING) {
+ final int firstVisibleItem = mList.getFirstVisiblePosition();
+ setThumbPos(getPosFromItemCount(firstVisibleItem, childCount, itemCount));
+ }
+
+ updateLongList(childCount, itemCount);
+ }
+ }
+
+ private void updateLongList(int childCount, int itemCount) {
+ final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
+ if (mLongList != longList) {
+ mLongList = longList;
+
+ onStateDependencyChanged(false);
+ }
+ }
+
+ /**
+ * Creates a view into which preview text can be placed.
+ */
+ private TextView createPreviewTextView(Context context) {
+ final LayoutParams params = new LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ final TextView textView = new TextView(context);
+ textView.setLayoutParams(params);
+ textView.setSingleLine(true);
+ textView.setEllipsize(TruncateAt.MIDDLE);
+ textView.setGravity(Gravity.CENTER);
+ textView.setAlpha(0f);
+
+ // Manually propagate inherited layout direction.
+ textView.setLayoutDirection(mList.getLayoutDirection());
+
+ return textView;
+ }
+
+ /**
+ * Measures and layouts the scrollbar and decorations.
+ */
+ public void updateLayout() {
+ // Prevent re-entry when RTL properties change as a side-effect of
+ // resolving padding.
+ if (mUpdatingLayout) {
+ return;
+ }
+
+ mUpdatingLayout = true;
+
+ updateContainerRect();
+
+ layoutThumb();
+ layoutTrack();
+
+ updateOffsetAndRange();
+
+ final Rect bounds = mTempBounds;
+ measurePreview(mPrimaryText, bounds);
+ applyLayout(mPrimaryText, bounds);
+ measurePreview(mSecondaryText, bounds);
+ applyLayout(mSecondaryText, bounds);
+
+ if (mPreviewImage != null) {
+ // Apply preview image padding.
+ bounds.left -= mPreviewImage.getPaddingLeft();
+ bounds.top -= mPreviewImage.getPaddingTop();
+ bounds.right += mPreviewImage.getPaddingRight();
+ bounds.bottom += mPreviewImage.getPaddingBottom();
+ applyLayout(mPreviewImage, bounds);
+ }
+
+ mUpdatingLayout = false;
+ }
+
+ /**
+ * Layouts a view within the specified bounds and pins the pivot point to
+ * the appropriate edge.
+ *
+ * @param view The view to layout.
+ * @param bounds Bounds at which to layout the view.
+ */
+ private void applyLayout(View view, Rect bounds) {
+ view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0);
+ }
+
+ /**
+ * Measures the preview text bounds, taking preview image padding into
+ * account. This method should only be called after {@link #layoutThumb()}
+ * and {@link #layoutTrack()} have both been called at least once.
+ *
+ * @param v The preview text view to measure.
+ * @param out Rectangle into which measured bounds are placed.
+ */
+ private void measurePreview(View v, Rect out) {
+ // Apply the preview image's padding as layout margins.
+ final Rect margins = mTempMargins;
+ margins.left = mPreviewImage.getPaddingLeft();
+ margins.top = mPreviewImage.getPaddingTop();
+ margins.right = mPreviewImage.getPaddingRight();
+ margins.bottom = mPreviewImage.getPaddingBottom();
+
+ if (mOverlayPosition == OVERLAY_FLOATING) {
+ measureFloating(v, margins, out);
+ } else {
+ measureViewToSide(v, mThumbImage, margins, out);
+ }
+ }
+
+ /**
+ * Measures the bounds for a view that should be laid out against the edge
+ * of an adjacent view. If no adjacent view is provided, lays out against
+ * the list edge.
+ *
+ * @param view The view to measure for layout.
+ * @param adjacent (Optional) The adjacent view, may be null to align to the
+ * list edge.
+ * @param margins Layout margins to apply to the view.
+ * @param out Rectangle into which measured bounds are placed.
+ */
+ private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) {
+ final int marginLeft;
+ final int marginTop;
+ final int marginRight;
+ if (margins == null) {
+ marginLeft = 0;
+ marginTop = 0;
+ marginRight = 0;
+ } else {
+ marginLeft = margins.left;
+ marginTop = margins.top;
+ marginRight = margins.right;
+ }
+
+ final Rect container = mContainerRect;
+ final int containerWidth = container.width();
+ final int maxWidth;
+ if (adjacent == null) {
+ maxWidth = containerWidth;
+ } else if (mLayoutFromRight) {
+ maxWidth = adjacent.getLeft();
+ } else {
+ maxWidth = containerWidth - adjacent.getRight();
+ }
+
+ final int adjMaxHeight = Math.max(0, container.height());
+ final int adjMaxWidth = Math.max(0, maxWidth - marginLeft - marginRight);
+ final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
+ final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ adjMaxHeight, MeasureSpec.UNSPECIFIED);
+ view.measure(widthMeasureSpec, heightMeasureSpec);
+
+ // Align to the left or right.
+ final int width = Math.min(adjMaxWidth, view.getMeasuredWidth());
+ final int left;
+ final int right;
+ if (mLayoutFromRight) {
+ right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight;
+ left = right - width;
+ } else {
+ left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft;
+ right = left + width;
+ }
+
+ // Don't adjust the vertical position.
+ final int top = marginTop;
+ final int bottom = top + view.getMeasuredHeight();
+ out.set(left, top, right, bottom);
+ }
+
+ private void measureFloating(View preview, Rect margins, Rect out) {
+ final int marginLeft;
+ final int marginTop;
+ final int marginRight;
+ if (margins == null) {
+ marginLeft = 0;
+ marginTop = 0;
+ marginRight = 0;
+ } else {
+ marginLeft = margins.left;
+ marginTop = margins.top;
+ marginRight = margins.right;
+ }
+
+ final Rect container = mContainerRect;
+ final int containerWidth = container.width();
+ final int adjMaxHeight = Math.max(0, container.height());
+ final int adjMaxWidth = Math.max(0, containerWidth - marginLeft - marginRight);
+ final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
+ final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ adjMaxHeight, MeasureSpec.UNSPECIFIED);
+ preview.measure(widthMeasureSpec, heightMeasureSpec);
+
+ // Align at the vertical center, 10% from the top.
+ final int containerHeight = container.height();
+ final int width = preview.getMeasuredWidth();
+ final int top = containerHeight / 10 + marginTop + container.top;
+ final int bottom = top + preview.getMeasuredHeight();
+ final int left = (containerWidth - width) / 2 + container.left;
+ final int right = left + width;
+ out.set(left, top, right, bottom);
+ }
+
+ /**
+ * Updates the container rectangle used for layout.
+ */
+ private void updateContainerRect() {
+ final AbsListView list = mList;
+ list.resolvePadding();
+
+ final Rect container = mContainerRect;
+ container.left = 0;
+ container.top = 0;
+ container.right = list.getWidth();
+ container.bottom = list.getHeight();
+
+ final int scrollbarStyle = mScrollBarStyle;
+ if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET
+ || scrollbarStyle == View.SCROLLBARS_INSIDE_OVERLAY) {
+ container.left += list.getPaddingLeft();
+ container.top += list.getPaddingTop();
+ container.right -= list.getPaddingRight();
+ container.bottom -= list.getPaddingBottom();
+
+ // In inset mode, we need to adjust for padded scrollbar width.
+ if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET) {
+ final int width = getWidth();
+ if (mScrollbarPosition == View.SCROLLBAR_POSITION_RIGHT) {
+ container.right += width;
+ } else {
+ container.left -= width;
+ }
+ }
+ }
+ }
+
+ /**
+ * Lays out the thumb according to the current scrollbar position.
+ */
+ private void layoutThumb() {
+ final Rect bounds = mTempBounds;
+ measureViewToSide(mThumbImage, null, null, bounds);
+ applyLayout(mThumbImage, bounds);
+ }
+
+ /**
+ * Lays out the track centered on the thumb. Must be called after
+ * {@link #layoutThumb}.
+ */
+ private void layoutTrack() {
+ final View track = mTrackImage;
+ final View thumb = mThumbImage;
+ final Rect container = mContainerRect;
+ final int maxWidth = Math.max(0, container.width());
+ final int maxHeight = Math.max(0, container.height());
+ final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
+ final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ maxHeight, MeasureSpec.UNSPECIFIED);
+ track.measure(widthMeasureSpec, heightMeasureSpec);
+
+ final int top;
+ final int bottom;
+ if (mThumbPosition == THUMB_POSITION_INSIDE) {
+ top = container.top;
+ bottom = container.bottom;
+ } else {
+ final int thumbHalfHeight = thumb.getHeight() / 2;
+ top = container.top + thumbHalfHeight;
+ bottom = container.bottom - thumbHalfHeight;
+ }
+
+ final int trackWidth = track.getMeasuredWidth();
+ final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
+ final int right = left + trackWidth;
+ track.layout(left, top, right, bottom);
+ }
+
+ /**
+ * Updates the offset and range used to convert from absolute y-position to
+ * thumb position within the track.
+ */
+ private void updateOffsetAndRange() {
+ final View trackImage = mTrackImage;
+ final View thumbImage = mThumbImage;
+ final float min;
+ final float max;
+ if (mThumbPosition == THUMB_POSITION_INSIDE) {
+ final float halfThumbHeight = thumbImage.getHeight() / 2f;
+ min = trackImage.getTop() + halfThumbHeight;
+ max = trackImage.getBottom() - halfThumbHeight;
+ } else{
+ min = trackImage.getTop();
+ max = trackImage.getBottom();
+ }
+
+ mThumbOffset = min;
+ mThumbRange = max - min;
+ }
+
+ private void setState(int state) {
+ mList.removeCallbacks(mDeferHide);
+
+ if (mAlwaysShow && state == STATE_NONE) {
+ state = STATE_VISIBLE;
+ }
+
+ if (state == mState) {
+ return;
+ }
+
+ switch (state) {
+ case STATE_NONE:
+ transitionToHidden();
+ break;
+ case STATE_VISIBLE:
+ transitionToVisible();
+ break;
+ case STATE_DRAGGING:
+ if (transitionPreviewLayout(mCurrentSection)) {
+ transitionToDragging();
+ } else {
+ transitionToVisible();
+ }
+ break;
+ }
+
+ mState = state;
+
+ refreshDrawablePressedState();
+ }
+
+ private void refreshDrawablePressedState() {
+ final boolean isPressed = mState == STATE_DRAGGING;
+ mThumbImage.setPressed(isPressed);
+ mTrackImage.setPressed(isPressed);
+ }
+
+ /**
+ * Shows nothing.
+ */
+ private void transitionToHidden() {
+ if (mDecorAnimation != null) {
+ mDecorAnimation.cancel();
+ }
+
+ final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage,
+ mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT);
+
+ // Push the thumb and track outside the list bounds.
+ final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth();
+ final Animator slideOut = groupAnimatorOfFloat(
+ View.TRANSLATION_X, offset, mThumbImage, mTrackImage)
+ .setDuration(DURATION_FADE_OUT);
+
+ mDecorAnimation = new AnimatorSet();
+ mDecorAnimation.playTogether(fadeOut, slideOut);
+ mDecorAnimation.start();
+
+ mShowingPreview = false;
+ }
+
+ /**
+ * Shows the thumb and track.
+ */
+ private void transitionToVisible() {
+ if (mDecorAnimation != null) {
+ mDecorAnimation.cancel();
+ }
+
+ final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage)
+ .setDuration(DURATION_FADE_IN);
+ final Animator fadeOut = groupAnimatorOfFloat(
+ View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
+ .setDuration(DURATION_FADE_OUT);
+ final Animator slideIn = groupAnimatorOfFloat(
+ View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
+
+ mDecorAnimation = new AnimatorSet();
+ mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn);
+ mDecorAnimation.start();
+
+ mShowingPreview = false;
+ }
+
+ /**
+ * Shows the thumb, preview, and track.
+ */
+ private void transitionToDragging() {
+ if (mDecorAnimation != null) {
+ mDecorAnimation.cancel();
+ }
+
+ final Animator fadeIn = groupAnimatorOfFloat(
+ View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage)
+ .setDuration(DURATION_FADE_IN);
+ final Animator slideIn = groupAnimatorOfFloat(
+ View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
+
+ mDecorAnimation = new AnimatorSet();
+ mDecorAnimation.playTogether(fadeIn, slideIn);
+ mDecorAnimation.start();
+
+ mShowingPreview = true;
+ }
+
+ private void postAutoHide() {
+ mList.removeCallbacks(mDeferHide);
+ mList.postDelayed(mDeferHide, FADE_TIMEOUT);
+ }
+
+ public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (!isEnabled()) {
+ setState(STATE_NONE);
+ return;
+ }
+
+ final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
+ if (hasMoreItems && mState != STATE_DRAGGING) {
+ setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
+ }
+
+ mScrollCompleted = true;
+
+ if (mFirstVisibleItem != firstVisibleItem) {
+ mFirstVisibleItem = firstVisibleItem;
+
+ // Show the thumb, if necessary, and set up auto-fade.
+ if (mState != STATE_DRAGGING) {
+ setState(STATE_VISIBLE);
+ postAutoHide();
+ }
+ }
+ }
+
+ private void getSectionsFromIndexer() {
+ mSectionIndexer = null;
+
+ Adapter adapter = mList.getAdapter();
+ if (adapter instanceof HeaderViewListAdapter) {
+ mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
+ adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
+ }
+
+ if (adapter instanceof ExpandableListConnector) {
+ final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
+ .getAdapter();
+ if (expAdapter instanceof SectionIndexer) {
+ mSectionIndexer = (SectionIndexer) expAdapter;
+ mListAdapter = adapter;
+ mSections = mSectionIndexer.getSections();
+ }
+ } else if (adapter instanceof SectionIndexer) {
+ mListAdapter = adapter;
+ mSectionIndexer = (SectionIndexer) adapter;
+ mSections = mSectionIndexer.getSections();
+ } else {
+ mListAdapter = adapter;
+ mSections = null;
+ }
+ }
+
+ public void onSectionsChanged() {
+ mListAdapter = null;
+ }
+
+ /**
+ * Scrolls to a specific position within the section
+ * @param position
+ */
+ private void scrollTo(float position) {
+ mScrollCompleted = false;
+
+ final int count = mList.getCount();
+ final Object[] sections = mSections;
+ final int sectionCount = sections == null ? 0 : sections.length;
+ int sectionIndex;
+ if (sections != null && sectionCount > 1) {
+ final int exactSection = MathUtils.constrain(
+ (int) (position * sectionCount), 0, sectionCount - 1);
+ int targetSection = exactSection;
+ int targetIndex = mSectionIndexer.getPositionForSection(targetSection);
+ sectionIndex = targetSection;
+
+ // Given the expected section and index, the following code will
+ // try to account for missing sections (no names starting with..)
+ // It will compute the scroll space of surrounding empty sections
+ // and interpolate the currently visible letter's range across the
+ // available space, so that there is always some list movement while
+ // the user moves the thumb.
+ int nextIndex = count;
+ int prevIndex = targetIndex;
+ int prevSection = targetSection;
+ int nextSection = targetSection + 1;
+
+ // Assume the next section is unique
+ if (targetSection < sectionCount - 1) {
+ nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
+ }
+
+ // Find the previous index if we're slicing the previous section
+ if (nextIndex == targetIndex) {
+ // Non-existent letter
+ while (targetSection > 0) {
+ targetSection--;
+ prevIndex = mSectionIndexer.getPositionForSection(targetSection);
+ if (prevIndex != targetIndex) {
+ prevSection = targetSection;
+ sectionIndex = targetSection;
+ break;
+ } else if (targetSection == 0) {
+ // When section reaches 0 here, sectionIndex must follow it.
+ // Assuming mSectionIndexer.getPositionForSection(0) == 0.
+ sectionIndex = 0;
+ break;
+ }
+ }
+ }
+
+ // Find the next index, in case the assumed next index is not
+ // unique. For instance, if there is no P, then request for P's
+ // position actually returns Q's. So we need to look ahead to make
+ // sure that there is really a Q at Q's position. If not, move
+ // further down...
+ int nextNextSection = nextSection + 1;
+ while (nextNextSection < sectionCount &&
+ mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
+ nextNextSection++;
+ nextSection++;
+ }
+
+ // Compute the beginning and ending scroll range percentage of the
+ // currently visible section. This could be equal to or greater than
+ // (1 / nSections). If the target position is near the previous
+ // position, snap to the previous position.
+ final float prevPosition = (float) prevSection / sectionCount;
+ final float nextPosition = (float) nextSection / sectionCount;
+ final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count;
+ if (prevSection == exactSection && position - prevPosition < snapThreshold) {
+ targetIndex = prevIndex;
+ } else {
+ targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
+ / (nextPosition - prevPosition));
+ }
+
+ // Clamp to valid positions.
+ targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
+
+ if (mList instanceof ExpandableListView) {
+ final ExpandableListView expList = (ExpandableListView) mList;
+ expList.setSelectionFromTop(expList.getFlatListPosition(
+ ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
+ 0);
+ } else if (mList instanceof ListView) {
+ ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
+ } else {
+ mList.setSelection(targetIndex + mHeaderCount);
+ }
+ } else {
+ final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
+
+ if (mList instanceof ExpandableListView) {
+ ExpandableListView expList = (ExpandableListView) mList;
+ expList.setSelectionFromTop(expList.getFlatListPosition(
+ ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
+ } else if (mList instanceof ListView) {
+ ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
+ } else {
+ mList.setSelection(index + mHeaderCount);
+ }
+
+ sectionIndex = -1;
+ }
+
+ if (mCurrentSection != sectionIndex) {
+ mCurrentSection = sectionIndex;
+
+ final boolean hasPreview = transitionPreviewLayout(sectionIndex);
+ if (!mShowingPreview && hasPreview) {
+ transitionToDragging();
+ } else if (mShowingPreview && !hasPreview) {
+ transitionToVisible();
+ }
+ }
+ }
+
+ /**
+ * Transitions the preview text to a new section. Handles animation,
+ * measurement, and layout. If the new preview text is empty, returns false.
+ *
+ * @param sectionIndex The section index to which the preview should
+ * transition.
+ * @return False if the new preview text is empty.
+ */
+ private boolean transitionPreviewLayout(int sectionIndex) {
+ final Object[] sections = mSections;
+ String text = null;
+ if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) {
+ final Object section = sections[sectionIndex];
+ if (section != null) {
+ text = section.toString();
+ }
+ }
+
+ final Rect bounds = mTempBounds;
+ final View preview = mPreviewImage;
+ final TextView showing;
+ final TextView target;
+ if (mShowingPrimary) {
+ showing = mPrimaryText;
+ target = mSecondaryText;
+ } else {
+ showing = mSecondaryText;
+ target = mPrimaryText;
+ }
+
+ // Set and layout target immediately.
+ target.setText(text);
+ measurePreview(target, bounds);
+ applyLayout(target, bounds);
+
+ if (mPreviewAnimation != null) {
+ mPreviewAnimation.cancel();
+ }
+
+ // Cross-fade preview text.
+ final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
+ final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
+ hideShowing.addListener(mSwitchPrimaryListener);
+
+ // Apply preview image padding and animate bounds, if necessary.
+ bounds.left -= preview.getPaddingLeft();
+ bounds.top -= preview.getPaddingTop();
+ bounds.right += preview.getPaddingRight();
+ bounds.bottom += preview.getPaddingBottom();
+ final Animator resizePreview = animateBounds(preview, bounds);
+ resizePreview.setDuration(DURATION_RESIZE);
+
+ mPreviewAnimation = new AnimatorSet();
+ final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget);
+ builder.with(resizePreview);
+
+ // The current preview size is unaffected by hidden or showing. It's
+ // used to set starting scales for things that need to be scaled down.
+ final int previewWidth = preview.getWidth() - preview.getPaddingLeft()
+ - preview.getPaddingRight();
+
+ // If target is too large, shrink it immediately to fit and expand to
+ // target size. Otherwise, start at target size.
+ final int targetWidth = target.getWidth();
+ if (targetWidth > previewWidth) {
+ target.setScaleX((float) previewWidth / targetWidth);
+ final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE);
+ builder.with(scaleAnim);
+ } else {
+ target.setScaleX(1f);
+ }
+
+ // If showing is larger than target, shrink to target size.
+ final int showingWidth = showing.getWidth();
+ if (showingWidth > targetWidth) {
+ final float scale = (float) targetWidth / showingWidth;
+ final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE);
+ builder.with(scaleAnim);
+ }
+
+ mPreviewAnimation.start();
+
+ return !TextUtils.isEmpty(text);
+ }
+
+ /**
+ * Positions the thumb and preview widgets.
+ *
+ * @param position The position, between 0 and 1, along the track at which
+ * to place the thumb.
+ */
+ private void setThumbPos(float position) {
+ final float thumbMiddle = position * mThumbRange + mThumbOffset;
+ mThumbImage.setTranslationY(thumbMiddle - mThumbImage.getHeight() / 2f);
+
+ final View previewImage = mPreviewImage;
+ final float previewHalfHeight = previewImage.getHeight() / 2f;
+ final float previewPos;
+ switch (mOverlayPosition) {
+ case OVERLAY_AT_THUMB:
+ previewPos = thumbMiddle;
+ break;
+ case OVERLAY_ABOVE_THUMB:
+ previewPos = thumbMiddle - previewHalfHeight;
+ break;
+ case OVERLAY_FLOATING:
+ default:
+ previewPos = 0;
+ break;
+ }
+
+ // Center the preview on the thumb, constrained to the list bounds.
+ final Rect container = mContainerRect;
+ final int top = container.top;
+ final int bottom = container.bottom;
+ final float minP = top + previewHalfHeight;
+ final float maxP = bottom - previewHalfHeight;
+ final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP);
+ final float previewTop = previewMiddle - previewHalfHeight;
+ previewImage.setTranslationY(previewTop);
+
+ mPrimaryText.setTranslationY(previewTop);
+ mSecondaryText.setTranslationY(previewTop);
+ }
+
+ private float getPosFromMotionEvent(float y) {
+ // If the list is the same height as the thumbnail or shorter,
+ // effectively disable scrolling.
+ if (mThumbRange <= 0) {
+ return 0f;
+ }
+
+ return MathUtils.constrain((y - mThumbOffset) / mThumbRange, 0f, 1f);
+ }
+
+ /**
+ * Calculates the thumb position based on the visible items.
+ *
+ * @param firstVisibleItem First visible item, >= 0.
+ * @param visibleItemCount Number of visible items, >= 0.
+ * @param totalItemCount Total number of items, >= 0.
+ * @return
+ */
+ private float getPosFromItemCount(
+ int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ final SectionIndexer sectionIndexer = mSectionIndexer;
+ if (sectionIndexer == null || mListAdapter == null) {
+ getSectionsFromIndexer();
+ }
+
+ if (visibleItemCount == 0 || totalItemCount == 0) {
+ // No items are visible.
+ return 0;
+ }
+
+ final boolean hasSections = sectionIndexer != null && mSections != null
+ && mSections.length > 0;
+ if (!hasSections || !mMatchDragPosition) {
+ if (visibleItemCount == totalItemCount) {
+ // All items are visible.
+ return 0;
+ } else {
+ return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
+ }
+ }
+
+ // Ignore headers.
+ firstVisibleItem -= mHeaderCount;
+ if (firstVisibleItem < 0) {
+ return 0;
+ }
+ totalItemCount -= mHeaderCount;
+
+ // Hidden portion of the first visible row.
+ final View child = mList.getChildAt(0);
+ final float incrementalPos;
+ if (child == null || child.getHeight() == 0) {
+ incrementalPos = 0;
+ } else {
+ incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
+ }
+
+ // Number of rows in this section.
+ final int section = sectionIndexer.getSectionForPosition(firstVisibleItem);
+ final int sectionPos = sectionIndexer.getPositionForSection(section);
+ final int sectionCount = mSections.length;
+ final int positionsInSection;
+ if (section < sectionCount - 1) {
+ final int nextSectionPos;
+ if (section + 1 < sectionCount) {
+ nextSectionPos = sectionIndexer.getPositionForSection(section + 1);
+ } else {
+ nextSectionPos = totalItemCount - 1;
+ }
+ positionsInSection = nextSectionPos - sectionPos;
+ } else {
+ positionsInSection = totalItemCount - sectionPos;
+ }
+
+ // Position within this section.
+ final float posWithinSection;
+ if (positionsInSection == 0) {
+ posWithinSection = 0;
+ } else {
+ posWithinSection = (firstVisibleItem + incrementalPos - sectionPos)
+ / positionsInSection;
+ }
+
+ float result = (section + posWithinSection) / sectionCount;
+
+ // Fake out the scroll bar for the last item. Since the section indexer
+ // won't ever actually move the list in this end space, make scrolling
+ // across the last item account for whatever space is remaining.
+ if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
+ final View lastChild = mList.getChildAt(visibleItemCount - 1);
+ final int bottomPadding = mList.getPaddingBottom();
+ final int maxSize;
+ final int currentVisibleSize;
+ if (mList.getClipToPadding()) {
+ maxSize = lastChild.getHeight();
+ currentVisibleSize = mList.getHeight() - bottomPadding - lastChild.getTop();
+ } else {
+ maxSize = lastChild.getHeight() + bottomPadding;
+ currentVisibleSize = mList.getHeight() - lastChild.getTop();
+ }
+ if (currentVisibleSize > 0 && maxSize > 0) {
+ result += (1 - result) * ((float) currentVisibleSize / maxSize );
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Cancels an ongoing fling event by injecting a
+ * {@link MotionEvent#ACTION_CANCEL} into the host view.
+ */
+ private void cancelFling() {
+ final MotionEvent cancelFling = MotionEvent.obtain(
+ 0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mList.onTouchEvent(cancelFling);
+ cancelFling.recycle();
+ }
+
+ /**
+ * Cancels a pending drag.
+ *
+ * @see #startPendingDrag()
+ */
+ private void cancelPendingDrag() {
+ mPendingDrag = -1;
+ }
+
+ /**
+ * Delays dragging until after the framework has determined that the user is
+ * scrolling, rather than tapping.
+ */
+ private void startPendingDrag() {
+ mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT;
+ }
+
+ private void beginDrag() {
+ mPendingDrag = -1;
+
+ setState(STATE_DRAGGING);
+
+ if (mListAdapter == null && mList != null) {
+ getSectionsFromIndexer();
+ }
+
+ if (mList != null) {
+ mList.requestDisallowInterceptTouchEvent(true);
+ mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+
+ cancelFling();
+ }
+
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!isEnabled()) {
+ return false;
+ }
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (isPointInside(ev.getX(), ev.getY())) {
+ // If the parent has requested that its children delay
+ // pressed state (e.g. is a scrolling container) then we
+ // need to allow the parent time to decide whether it wants
+ // to intercept events. If it does, we will receive a CANCEL
+ // event.
+ if (!mList.isInScrollingContainer()) {
+ // This will get dispatched to onTouchEvent(). Start
+ // dragging there.
+ return true;
+ }
+
+ mInitialTouchY = ev.getY();
+ startPendingDrag();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!isPointInside(ev.getX(), ev.getY())) {
+ cancelPendingDrag();
+ } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) {
+ beginDrag();
+
+ final float pos = getPosFromMotionEvent(mInitialTouchY);
+ scrollTo(pos);
+
+ // This may get dispatched to onTouchEvent(), but it
+ // doesn't really matter since we'll already be in a drag.
+ return onTouchEvent(ev);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ cancelPendingDrag();
+ break;
+ }
+
+ return false;
+ }
+
+ public boolean onInterceptHoverEvent(MotionEvent ev) {
+ if (!isEnabled()) {
+ return false;
+ }
+
+ final int actionMasked = ev.getActionMasked();
+ if ((actionMasked == MotionEvent.ACTION_HOVER_ENTER
+ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) && mState == STATE_NONE
+ && isPointInside(ev.getX(), ev.getY())) {
+ setState(STATE_VISIBLE);
+ postAutoHide();
+ }
+
+ return false;
+ }
+
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (mState == STATE_DRAGGING || isPointInside(event.getX(), event.getY())) {
+ return PointerIcon.getSystemIcon(mList.getContext(), PointerIcon.TYPE_ARROW);
+ }
+ return null;
+ }
+
+ public boolean onTouchEvent(MotionEvent me) {
+ if (!isEnabled()) {
+ return false;
+ }
+
+ switch (me.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (isPointInside(me.getX(), me.getY())) {
+ if (!mList.isInScrollingContainer()) {
+ beginDrag();
+ return true;
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ if (mPendingDrag >= 0) {
+ // Allow a tap to scroll.
+ beginDrag();
+
+ final float pos = getPosFromMotionEvent(me.getY());
+ setThumbPos(pos);
+ scrollTo(pos);
+
+ // Will hit the STATE_DRAGGING check below
+ }
+
+ if (mState == STATE_DRAGGING) {
+ if (mList != null) {
+ // ViewGroup does the right thing already, but there might
+ // be other classes that don't properly reset on touch-up,
+ // so do this explicitly just in case.
+ mList.requestDisallowInterceptTouchEvent(false);
+ mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+
+ setState(STATE_VISIBLE);
+ postAutoHide();
+
+ return true;
+ }
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
+ beginDrag();
+
+ // Will hit the STATE_DRAGGING check below
+ }
+
+ if (mState == STATE_DRAGGING) {
+ // TODO: Ignore jitter.
+ final float pos = getPosFromMotionEvent(me.getY());
+ setThumbPos(pos);
+
+ // If the previous scrollTo is still pending
+ if (mScrollCompleted) {
+ scrollTo(pos);
+ }
+
+ return true;
+ }
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ cancelPendingDrag();
+ } break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether a coordinate is inside the scroller's activation area. If
+ * there is a track image, touching anywhere within the thumb-width of the
+ * track activates scrolling. Otherwise, the user has to touch inside thumb
+ * itself.
+ *
+ * @param x The x-coordinate.
+ * @param y The y-coordinate.
+ * @return Whether the coordinate is inside the scroller's activation area.
+ */
+ private boolean isPointInside(float x, float y) {
+ return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y));
+ }
+
+ private boolean isPointInsideX(float x) {
+ final float offset = mThumbImage.getTranslationX();
+ final float left = mThumbImage.getLeft() + offset;
+ final float right = mThumbImage.getRight() + offset;
+
+ // Apply the minimum touch target size.
+ final float targetSizeDiff = mMinimumTouchTarget - (right - left);
+ final float adjust = targetSizeDiff > 0 ? targetSizeDiff : 0;
+
+ if (mLayoutFromRight) {
+ return x >= mThumbImage.getLeft() - adjust;
+ } else {
+ return x <= mThumbImage.getRight() + adjust;
+ }
+ }
+
+ private boolean isPointInsideY(float y) {
+ final float offset = mThumbImage.getTranslationY();
+ final float top = mThumbImage.getTop() + offset;
+ final float bottom = mThumbImage.getBottom() + offset;
+
+ // Apply the minimum touch target size.
+ final float targetSizeDiff = mMinimumTouchTarget - (bottom - top);
+ final float adjust = targetSizeDiff > 0 ? targetSizeDiff / 2 : 0;
+
+ return y >= (top - adjust) && y <= (bottom + adjust);
+ }
+
+ /**
+ * Constructs an animator for the specified property on a group of views.
+ * See {@link ObjectAnimator#ofFloat(Object, String, float...)} for
+ * implementation details.
+ *
+ * @param property The property being animated.
+ * @param value The value to which that property should animate.
+ * @param views The target views to animate.
+ * @return An animator for all the specified views.
+ */
+ private static Animator groupAnimatorOfFloat(
+ Property<View, Float> property, float value, View... views) {
+ AnimatorSet animSet = new AnimatorSet();
+ AnimatorSet.Builder builder = null;
+
+ for (int i = views.length - 1; i >= 0; i--) {
+ final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
+ if (builder == null) {
+ builder = animSet.play(anim);
+ } else {
+ builder.with(anim);
+ }
+ }
+
+ return animSet;
+ }
+
+ /**
+ * Returns an animator for the view's scaleX value.
+ */
+ private static Animator animateScaleX(View v, float target) {
+ return ObjectAnimator.ofFloat(v, View.SCALE_X, target);
+ }
+
+ /**
+ * Returns an animator for the view's alpha value.
+ */
+ private static Animator animateAlpha(View v, float alpha) {
+ return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
+ }
+
+ /**
+ * A Property wrapper around the <code>left</code> functionality handled by the
+ * {@link View#setLeft(int)} and {@link View#getLeft()} methods.
+ */
+ private static Property<View, Integer> LEFT = new IntProperty<View>("left") {
+ @Override
+ public void setValue(View object, int value) {
+ object.setLeft(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return object.getLeft();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>top</code> functionality handled by the
+ * {@link View#setTop(int)} and {@link View#getTop()} methods.
+ */
+ private static Property<View, Integer> TOP = new IntProperty<View>("top") {
+ @Override
+ public void setValue(View object, int value) {
+ object.setTop(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return object.getTop();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>right</code> functionality handled by the
+ * {@link View#setRight(int)} and {@link View#getRight()} methods.
+ */
+ private static Property<View, Integer> RIGHT = new IntProperty<View>("right") {
+ @Override
+ public void setValue(View object, int value) {
+ object.setRight(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return object.getRight();
+ }
+ };
+
+ /**
+ * A Property wrapper around the <code>bottom</code> functionality handled by the
+ * {@link View#setBottom(int)} and {@link View#getBottom()} methods.
+ */
+ private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") {
+ @Override
+ public void setValue(View object, int value) {
+ object.setBottom(value);
+ }
+
+ @Override
+ public Integer get(View object) {
+ return object.getBottom();
+ }
+ };
+
+ /**
+ * Returns an animator for the view's bounds.
+ */
+ private static Animator animateBounds(View v, Rect bounds) {
+ final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left);
+ final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top);
+ final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right);
+ final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom);
+ return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom);
+ }
+}
diff --git a/android/widget/Filter.java b/android/widget/Filter.java
new file mode 100644
index 0000000..d901540
--- /dev/null
+++ b/android/widget/Filter.java
@@ -0,0 +1,323 @@
+/*
+ * 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;
+import android.util.Log;
+
+/**
+ * <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 LOG_TAG = "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;
+
+ private Delayer mDelayer;
+
+ private final Object mLock = new Object();
+
+ /**
+ * <p>Creates a new asynchronous filter.</p>
+ */
+ public Filter() {
+ mResultHandler = new ResultsHandler();
+ }
+
+ /**
+ * Provide an interface that decides how long to delay the message for a given query. Useful
+ * for heuristics such as posting a delay for the delete key to avoid doing any work while the
+ * user holds down the delete key.
+ *
+ * @param delayer The delayer.
+ * @hide
+ */
+ public void setDelayer(Delayer delayer) {
+ synchronized (mLock) {
+ mDelayer = delayer;
+ }
+ }
+
+ /**
+ * <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 (mLock) {
+ if (mThreadHandler == null) {
+ HandlerThread thread = new HandlerThread(
+ THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ thread.start();
+ mThreadHandler = new RequestHandler(thread.getLooper());
+ }
+
+ final long delay = (mDelayer == null) ? 0 : mDelayer.getPostingDelay(constraint);
+
+ Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
+
+ RequestArguments args = new RequestArguments();
+ // make sure we use an immutable copy of the constraint, so that
+ // it doesn't change while the filter operation is in progress
+ args.constraint = constraint != null ? constraint.toString() : null;
+ args.listener = listener;
+ message.obj = args;
+
+ mThreadHandler.removeMessages(FILTER_TOKEN);
+ mThreadHandler.removeMessages(FINISH_TOKEN);
+ mThreadHandler.sendMessageDelayed(message, delay);
+ }
+ }
+
+ /**
+ * <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);
+ } catch (Exception e) {
+ args.results = new FilterResults();
+ Log.w(LOG_TAG, "An exception occured during performFiltering()!", e);
+ } finally {
+ message = mResultHandler.obtainMessage(what);
+ message.obj = args;
+ message.sendToTarget();
+ }
+
+ synchronized (mLock) {
+ if (mThreadHandler != null) {
+ Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
+ mThreadHandler.sendMessageDelayed(finishMessage, 3000);
+ }
+ }
+ break;
+ case FINISH_TOKEN:
+ synchronized (mLock) {
+ 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;
+ }
+
+ /**
+ * @hide
+ */
+ public interface Delayer {
+
+ /**
+ * @param constraint The constraint passed to {@link Filter#filter(CharSequence)}
+ * @return The delay that should be used for
+ * {@link Handler#sendMessageDelayed(android.os.Message, long)}
+ */
+ long getPostingDelay(CharSequence constraint);
+ }
+}
diff --git a/android/widget/FilterQueryProvider.java b/android/widget/FilterQueryProvider.java
new file mode 100644
index 0000000..740d2f0
--- /dev/null
+++ b/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/android/widget/Filterable.java b/android/widget/Filterable.java
new file mode 100644
index 0000000..f7c8d59
--- /dev/null
+++ b/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/android/widget/ForwardingListener.java b/android/widget/ForwardingListener.java
new file mode 100644
index 0000000..8b82c06
--- /dev/null
+++ b/android/widget/ForwardingListener.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2015 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.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * Abstract class that forwards touch events to a {@link ShowableListMenu}.
+ *
+ * @hide
+ */
+public abstract class ForwardingListener
+ implements View.OnTouchListener, View.OnAttachStateChangeListener {
+
+ /** Scaled touch slop, used for detecting movement outside bounds. */
+ private final float mScaledTouchSlop;
+
+ /** Timeout before disallowing intercept on the source's parent. */
+ private final int mTapTimeout;
+
+ /** Timeout before accepting a long-press to start forwarding. */
+ private final int mLongPressTimeout;
+
+ /** Source view from which events are forwarded. */
+ private final View mSrc;
+
+ /** Runnable used to prevent conflicts with scrolling parents. */
+ private Runnable mDisallowIntercept;
+
+ /** Runnable used to trigger forwarding on long-press. */
+ private Runnable mTriggerLongPress;
+
+ /** Whether this listener is currently forwarding touch events. */
+ private boolean mForwarding;
+
+ /** The id of the first pointer down in the current event stream. */
+ private int mActivePointerId;
+
+ public ForwardingListener(View src) {
+ mSrc = src;
+ src.setLongClickable(true);
+ src.addOnAttachStateChangeListener(this);
+
+ mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+
+ // Use a medium-press timeout. Halfway between tap and long-press.
+ mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
+ }
+
+ /**
+ * Returns the popup to which this listener is forwarding events.
+ * <p>
+ * Override this to return the correct popup. If the popup is displayed
+ * asynchronously, you may also need to override
+ * {@link #onForwardingStopped} to prevent premature cancellation of
+ * forwarding.
+ *
+ * @return the popup to which this listener is forwarding events
+ */
+ public abstract ShowableListMenu getPopup();
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ final boolean wasForwarding = mForwarding;
+ final boolean forwarding;
+ if (wasForwarding) {
+ forwarding = onTouchForwarded(event) || !onForwardingStopped();
+ } else {
+ forwarding = onTouchObserved(event) && onForwardingStarted();
+
+ if (forwarding) {
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
+ 0.0f, 0.0f, 0);
+ mSrc.onTouchEvent(e);
+ e.recycle();
+ }
+ }
+
+ mForwarding = forwarding;
+ return forwarding || wasForwarding;
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mForwarding = false;
+ mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+ if (mDisallowIntercept != null) {
+ mSrc.removeCallbacks(mDisallowIntercept);
+ }
+ }
+
+ /**
+ * Called when forwarding would like to start.
+ * <p>
+ * By default, this will show the popup returned by {@link #getPopup()}.
+ * It may be overridden to perform another action, like clicking the
+ * source view or preparing the popup before showing it.
+ *
+ * @return true to start forwarding, false otherwise
+ */
+ protected boolean onForwardingStarted() {
+ final ShowableListMenu popup = getPopup();
+ if (popup != null && !popup.isShowing()) {
+ popup.show();
+ }
+ return true;
+ }
+
+ /**
+ * Called when forwarding would like to stop.
+ * <p>
+ * By default, this will dismiss the popup returned by
+ * {@link #getPopup()}. It may be overridden to perform some other
+ * action.
+ *
+ * @return true to stop forwarding, false otherwise
+ */
+ protected boolean onForwardingStopped() {
+ final ShowableListMenu popup = getPopup();
+ if (popup != null && popup.isShowing()) {
+ popup.dismiss();
+ }
+ return true;
+ }
+
+ /**
+ * Observes motion events and determines when to start forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to start forwarding motion events, false otherwise
+ */
+ private boolean onTouchObserved(MotionEvent srcEvent) {
+ final View src = mSrc;
+ if (!src.isEnabled()) {
+ return false;
+ }
+
+ final int actionMasked = srcEvent.getActionMasked();
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ mActivePointerId = srcEvent.getPointerId(0);
+
+ if (mDisallowIntercept == null) {
+ mDisallowIntercept = new DisallowIntercept();
+ }
+ src.postDelayed(mDisallowIntercept, mTapTimeout);
+
+ if (mTriggerLongPress == null) {
+ mTriggerLongPress = new TriggerLongPress();
+ }
+ src.postDelayed(mTriggerLongPress, mLongPressTimeout);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+ if (activePointerIndex >= 0) {
+ final float x = srcEvent.getX(activePointerIndex);
+ final float y = srcEvent.getY(activePointerIndex);
+
+ // Has the pointer moved outside of the view?
+ if (!src.pointInView(x, y, mScaledTouchSlop)) {
+ clearCallbacks();
+
+ // Don't let the parent intercept our events.
+ src.getParent().requestDisallowInterceptTouchEvent(true);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ clearCallbacks();
+ break;
+ }
+
+ return false;
+ }
+
+ private void clearCallbacks() {
+ if (mTriggerLongPress != null) {
+ mSrc.removeCallbacks(mTriggerLongPress);
+ }
+
+ if (mDisallowIntercept != null) {
+ mSrc.removeCallbacks(mDisallowIntercept);
+ }
+ }
+
+ private void onLongPress() {
+ clearCallbacks();
+
+ final View src = mSrc;
+ if (!src.isEnabled() || src.isLongClickable()) {
+ // Ignore long-press if the view is disabled or has its own
+ // handler.
+ return;
+ }
+
+ if (!onForwardingStarted()) {
+ return;
+ }
+
+ // Don't let the parent intercept our events.
+ src.getParent().requestDisallowInterceptTouchEvent(true);
+
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ src.onTouchEvent(e);
+ e.recycle();
+
+ mForwarding = true;
+ }
+
+ /**
+ * Handles forwarded motion events and determines when to stop
+ * forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to continue forwarding motion events, false to cancel
+ */
+ private boolean onTouchForwarded(MotionEvent srcEvent) {
+ final View src = mSrc;
+ final ShowableListMenu popup = getPopup();
+ if (popup == null || !popup.isShowing()) {
+ return false;
+ }
+
+ final DropDownListView dst = (DropDownListView) popup.getListView();
+ if (dst == null || !dst.isShown()) {
+ return false;
+ }
+
+ // Convert event to destination-local coordinates.
+ final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+ src.toGlobalMotionEvent(dstEvent);
+ dst.toLocalMotionEvent(dstEvent);
+
+ // Forward converted event to destination view, then recycle it.
+ final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
+ dstEvent.recycle();
+
+ // Always cancel forwarding when the touch stream ends.
+ final int action = srcEvent.getActionMasked();
+ final boolean keepForwarding = action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL;
+
+ return handled && keepForwarding;
+ }
+
+ private class DisallowIntercept implements Runnable {
+ @Override
+ public void run() {
+ final ViewParent parent = mSrc.getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+
+ private class TriggerLongPress implements Runnable {
+ @Override
+ public void run() {
+ onLongPress();
+ }
+ }
+}
diff --git a/android/widget/FrameLayout.java b/android/widget/FrameLayout.java
new file mode 100644
index 0000000..dc8ee01
--- /dev/null
+++ b/android/widget/FrameLayout.java
@@ -0,0 +1,487 @@
+/*
+ * 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.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * FrameLayout is designed to block out an area on the screen to display
+ * a single item. Generally, FrameLayout should be used to hold a single child view, because it can
+ * be difficult to organize child views in a way that's scalable to different screen sizes without
+ * the children overlapping each other. You can, however, add multiple children to a FrameLayout
+ * and control their position within the FrameLayout by assigning gravity to each child, using the
+ * <a href="FrameLayout.LayoutParams.html#attr_android:layout_gravity">{@code
+ * android:layout_gravity}</a> attribute.
+ * <p>Child views are drawn in a stack, with the most recently added child on top.
+ * The size of the FrameLayout is the size of its largest child (plus padding), visible
+ * or not (if the FrameLayout's parent permits). Views that are {@link android.view.View#GONE} are
+ * used for sizing
+ * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()}
+ * is set to true.
+ *
+ * @attr ref android.R.styleable#FrameLayout_measureAllChildren
+ */
+@RemoteView
+public class FrameLayout extends ViewGroup {
+ private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
+
+ @ViewDebug.ExportedProperty(category = "measurement")
+ boolean mMeasureAllChildren = false;
+
+ @ViewDebug.ExportedProperty(category = "padding")
+ private int mForegroundPaddingLeft = 0;
+
+ @ViewDebug.ExportedProperty(category = "padding")
+ private int mForegroundPaddingTop = 0;
+
+ @ViewDebug.ExportedProperty(category = "padding")
+ private int mForegroundPaddingRight = 0;
+
+ @ViewDebug.ExportedProperty(category = "padding")
+ private int mForegroundPaddingBottom = 0;
+
+ private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+ public FrameLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
+
+ if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
+ setMeasureAllChildren(true);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Describes how the foreground is positioned. Defaults to START and TOP.
+ *
+ * @param foregroundGravity See {@link android.view.Gravity}
+ *
+ * @see #getForegroundGravity()
+ *
+ * @attr ref android.R.styleable#View_foregroundGravity
+ */
+ @android.view.RemotableViewMethod
+ public void setForegroundGravity(int foregroundGravity) {
+ if (getForegroundGravity() != foregroundGravity) {
+ super.setForegroundGravity(foregroundGravity);
+
+ // calling get* again here because the set above may apply default constraints
+ final Drawable foreground = getForeground();
+ if (getForegroundGravity() == Gravity.FILL && foreground != null) {
+ Rect padding = new Rect();
+ if (foreground.getPadding(padding)) {
+ mForegroundPaddingLeft = padding.left;
+ mForegroundPaddingTop = padding.top;
+ mForegroundPaddingRight = padding.right;
+ mForegroundPaddingBottom = padding.bottom;
+ }
+ } else {
+ mForegroundPaddingLeft = 0;
+ mForegroundPaddingTop = 0;
+ mForegroundPaddingRight = 0;
+ mForegroundPaddingBottom = 0;
+ }
+
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
+ * and a height of {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.
+ */
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ }
+
+ int getPaddingLeftWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
+ mPaddingLeft + mForegroundPaddingLeft;
+ }
+
+ int getPaddingRightWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
+ mPaddingRight + mForegroundPaddingRight;
+ }
+
+ private int getPaddingTopWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
+ mPaddingTop + mForegroundPaddingTop;
+ }
+
+ private int getPaddingBottomWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
+ mPaddingBottom + mForegroundPaddingBottom;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+
+ final boolean measureMatchParentChildren =
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+ MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+ mMatchParentChildren.clear();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (mMeasureAllChildren || child.getVisibility() != GONE) {
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ if (measureMatchParentChildren) {
+ if (lp.width == LayoutParams.MATCH_PARENT ||
+ lp.height == LayoutParams.MATCH_PARENT) {
+ mMatchParentChildren.add(child);
+ }
+ }
+ }
+ }
+
+ // Account for padding too
+ maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+ maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+ // 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(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+ count = mMatchParentChildren.size();
+ if (count > 1) {
+ for (int i = 0; i < count; i++) {
+ final View child = mMatchParentChildren.get(i);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec;
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ final int width = Math.max(0, getMeasuredWidth()
+ - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+ - lp.leftMargin - lp.rightMargin);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ width, MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
+ lp.leftMargin + lp.rightMargin,
+ lp.width);
+ }
+
+ final int childHeightMeasureSpec;
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ final int height = Math.max(0, getMeasuredHeight()
+ - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+ - lp.topMargin - lp.bottomMargin);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ height, MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
+ lp.topMargin + lp.bottomMargin,
+ lp.height);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ layoutChildren(left, top, right, bottom, false /* no force left gravity */);
+ }
+
+ void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
+ final int count = getChildCount();
+
+ final int parentLeft = getPaddingLeftWithForeground();
+ final int parentRight = right - left - getPaddingRightWithForeground();
+
+ final int parentTop = getPaddingTopWithForeground();
+ final int parentBottom = bottom - top - getPaddingBottomWithForeground();
+
+ 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;
+ int childTop;
+
+ int gravity = lp.gravity;
+ if (gravity == -1) {
+ gravity = DEFAULT_CHILD_GRAVITY;
+ }
+
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+ final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
+ lp.leftMargin - lp.rightMargin;
+ break;
+ case Gravity.RIGHT:
+ if (!forceLeftGravity) {
+ childLeft = parentRight - width - lp.rightMargin;
+ break;
+ }
+ case Gravity.LEFT:
+ default:
+ childLeft = parentLeft + lp.leftMargin;
+ }
+
+ switch (verticalGravity) {
+ case Gravity.TOP:
+ childTop = parentTop + lp.topMargin;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ childTop = parentTop + (parentBottom - parentTop - height) / 2 +
+ lp.topMargin - lp.bottomMargin;
+ break;
+ case Gravity.BOTTOM:
+ childTop = parentBottom - height - lp.bottomMargin;
+ break;
+ default:
+ childTop = parentTop + lp.topMargin;
+ }
+
+ child.layout(childLeft, childTop, childLeft + width, childTop + height);
+ }
+ }
+ }
+
+ /**
+ * Sets whether to consider 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
+ */
+ @android.view.RemotableViewMethod
+ public void setMeasureAllChildren(boolean measureAll) {
+ mMeasureAllChildren = measureAll;
+ }
+
+ /**
+ * Determines whether all children, or just those in the VISIBLE or
+ * INVISIBLE state, are considered when measuring.
+ *
+ * @return Whether all children are considered when measuring.
+ *
+ * @deprecated This method is deprecated in favor of
+ * {@link #getMeasureAllChildren() getMeasureAllChildren()}, which was
+ * renamed for consistency with
+ * {@link #setMeasureAllChildren(boolean) setMeasureAllChildren()}.
+ */
+ @Deprecated
+ public boolean getConsiderGoneChildrenWhenMeasuring() {
+ return getMeasureAllChildren();
+ }
+
+ /**
+ * Determines whether all children, or just those in the VISIBLE or
+ * INVISIBLE state, are considered when measuring.
+ *
+ * @return Whether all children are considered when measuring.
+ */
+ public boolean getMeasureAllChildren() {
+ return mMeasureAllChildren;
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new FrameLayout.LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (sPreserveMarginParamsInLayoutParamConversion) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ }
+ }
+ return new LayoutParams(lp);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return FrameLayout.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("measurement:measureAllChildren", mMeasureAllChildren);
+ encoder.addProperty("padding:foregroundPaddingLeft", mForegroundPaddingLeft);
+ encoder.addProperty("padding:foregroundPaddingTop", mForegroundPaddingTop);
+ encoder.addProperty("padding:foregroundPaddingRight", mForegroundPaddingRight);
+ encoder.addProperty("padding:foregroundPaddingBottom", mForegroundPaddingBottom);
+ }
+
+ /**
+ * 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.
+ *
+ * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
+ */
+ public static class LayoutParams extends MarginLayoutParams {
+ /**
+ * Value for {@link #gravity} indicating that a gravity has not been
+ * explicitly specified.
+ */
+ public static final int UNSPECIFIED_GRAVITY = -1;
+
+ /**
+ * The gravity to apply with the View to which these layout parameters
+ * are associated.
+ * <p>
+ * The default value is {@link #UNSPECIFIED_GRAVITY}, which is treated
+ * by FrameLayout as {@code Gravity.TOP | Gravity.START}.
+ *
+ * @see android.view.Gravity
+ * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
+ */
+ public int gravity = UNSPECIFIED_GRAVITY;
+
+ public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
+ gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
+ a.recycle();
+ }
+
+ 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 #MATCH_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #MATCH_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;
+ }
+
+ public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height, margin values, and
+ * gravity of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(@NonNull LayoutParams source) {
+ super(source);
+
+ this.gravity = source.gravity;
+ }
+ }
+}
diff --git a/android/widget/Gallery.java b/android/widget/Gallery.java
new file mode 100644
index 0000000..7655f3d
--- /dev/null
+++ b/android/widget/Gallery.java
@@ -0,0 +1,1574 @@
+/*
+ * 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.NonNull;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Transformation;
+
+import com.android.internal.R;
+
+/**
+ * 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.
+ * <p>
+ * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
+ * layout parameters type.
+ *
+ * @attr ref android.R.styleable#Gallery_animationDuration
+ * @attr ref android.R.styleable#Gallery_spacing
+ * @attr ref android.R.styleable#Gallery_gravity
+ *
+ * @deprecated This widget is no longer supported. Other horizontally scrolling
+ * widgets include {@link HorizontalScrollView} and {@link android.support.v4.view.ViewPager}
+ * from the support library.
+ */
+@Deprecated
+@Widget
+public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener {
+
+ private static final String TAG = "Gallery";
+
+ private static final boolean localLOGV = false;
+
+ /**
+ * Duration in milliseconds from the start of a scroll during which we're
+ * unsure whether the user is scrolling or flinging.
+ */
+ private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250;
+
+ /**
+ * 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();
+
+ /**
+ * Sets mSuppressSelectionChanged = false. This is used to set it to false
+ * in the future. It will also trigger a selection changed.
+ */
+ private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mSuppressSelectionChanged = false;
+ selectionChanged();
+ }
+ };
+
+ /**
+ * 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 = true;
+
+ /**
+ * 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;
+
+ /**
+ * If true, we have received the "invoke" (center or enter buttons) key
+ * down. This is checked before we action on the "invoke" key up, and is
+ * subsequently cleared.
+ */
+ private boolean mReceivedInvokeKeyDown;
+
+ private AdapterContextMenuInfo mContextMenuInfo;
+
+ /**
+ * If true, this onScroll is the first for this user's drag (remember, a
+ * drag sends many onScrolls).
+ */
+ private boolean mIsFirstScroll;
+
+ /**
+ * If true, mFirstPosition is the position of the rightmost child, and
+ * the children are ordered right to left.
+ */
+ private boolean mIsRtl = true;
+
+ /**
+ * Offset between the center of the selected child view and the center of the Gallery.
+ * Used to reset position correctly during layout.
+ */
+ private int mSelectedCenterOffset;
+
+ 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes);
+
+ 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;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mGestureDetector == null) {
+ mGestureDetector = new GestureDetector(getContext(), this);
+ mGestureDetector.setIsLongpressEnabled(true);
+ }
+ }
+
+ /**
+ * 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();
+
+ final View selChild = mSelectedChild;
+ if (selChild != null) {
+ final int childLeft = selChild.getLeft();
+ final int childCenter = selChild.getWidth() / 2;
+ final int galleryCenter = getWidth() / 2;
+ mSelectedCenterOffset = childLeft + childCenter - galleryCenter;
+ }
+
+ onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
+
+ invalidate();
+ }
+
+ int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
+ int extremeItemPosition = motionToLeft != mIsRtl ? 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++) {
+ int n = mIsRtl ? (numChildren - 1 - i) : i;
+ final View child = getChildAt(n);
+ if (child.getRight() >= galleryLeft) {
+ break;
+ } else {
+ start = n;
+ count++;
+ mRecycler.put(firstPosition + n, child);
+ }
+ }
+ if (!mIsRtl) {
+ start = 0;
+ }
+ } else {
+ final int galleryRight = getWidth() - mPaddingRight;
+ for (int i = numChildren - 1; i >= 0; i--) {
+ int n = mIsRtl ? numChildren - 1 - i : i;
+ final View child = getChildAt(n);
+ if (child.getLeft() <= galleryRight) {
+ break;
+ } else {
+ start = n;
+ count++;
+ mRecycler.put(firstPosition + n, child);
+ }
+ }
+ if (mIsRtl) {
+ start = 0;
+ }
+ }
+
+ detachViewsFromParent(start, count);
+
+ if (toLeft != mIsRtl) {
+ 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();
+ }
+ mSelectedCenterOffset = 0;
+ invalidate();
+ }
+
+ @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();
+
+ // 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) {
+
+ mIsRtl = isLayoutRtl();
+
+ 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
+ int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) +
+ mSelectedCenterOffset;
+ 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() {
+ if (mIsRtl) {
+ fillToGalleryLeftRtl();
+ } else {
+ fillToGalleryLeftLtr();
+ }
+ }
+
+ private void fillToGalleryLeftRtl() {
+ int itemSpacing = mSpacing;
+ int galleryLeft = mPaddingLeft;
+ int numChildren = getChildCount();
+ int numItems = mItemCount;
+
+ // Set state for initial iteration
+ View prevIterationView = getChildAt(numChildren - 1);
+ int curPosition;
+ int curRightEdge;
+
+ if (prevIterationView != null) {
+ curPosition = mFirstPosition + numChildren;
+ curRightEdge = prevIterationView.getLeft() - itemSpacing;
+ } else {
+ // No children available!
+ mFirstPosition = curPosition = mItemCount - 1;
+ curRightEdge = mRight - mLeft - mPaddingRight;
+ mShouldStopFling = true;
+ }
+
+ while (curRightEdge > galleryLeft && curPosition < mItemCount) {
+ prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+ curRightEdge, false);
+
+ // Set state for next iteration
+ curRightEdge = prevIterationView.getLeft() - itemSpacing;
+ curPosition++;
+ }
+ }
+
+ private void fillToGalleryLeftLtr() {
+ 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() {
+ if (mIsRtl) {
+ fillToGalleryRightRtl();
+ } else {
+ fillToGalleryRightLtr();
+ }
+ }
+
+ private void fillToGalleryRightRtl() {
+ int itemSpacing = mSpacing;
+ int galleryRight = mRight - mLeft - mPaddingRight;
+
+ // Set state for initial iteration
+ View prevIterationView = getChildAt(0);
+ int curPosition;
+ int curLeftEdge;
+
+ if (prevIterationView != null) {
+ curPosition = mFirstPosition -1;
+ curLeftEdge = prevIterationView.getRight() + itemSpacing;
+ } else {
+ curPosition = 0;
+ curLeftEdge = mPaddingLeft;
+ mShouldStopFling = true;
+ }
+
+ while (curLeftEdge < galleryRight && curPosition >= 0) {
+ prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+ curLeftEdge, true);
+
+ // Remember some state
+ mFirstPosition = curPosition;
+
+ // Set state for next iteration
+ curLeftEdge = prevIterationView.getRight() + itemSpacing;
+ curPosition--;
+ }
+ }
+
+ private void fillToGalleryRightLtr() {
+ 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-coordinate indicating where this view should be placed. This
+ * will either be the left or right edge of the view, depending on
+ * the fromLeft parameter
+ * @param fromLeft Are we positioning 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
+ 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 parameters.
+ *
+ * @param child The view to position
+ * @param offset Offset from the selected position
+ * @param x X-coordinate indicating where this view should be placed. This
+ * will either be the left or right edge of the view, depending on
+ * the fromLeft parameter
+ * @param fromLeft Are we positioning 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 != mIsRtl ? -1 : 0, lp, true);
+
+ 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, 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
+ * @return Where the top of the child should be
+ */
+ private int calculateTop(View child, boolean duringLayout) {
+ int myHeight = duringLayout ? getMeasuredHeight() : 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+
+ if (!mShouldCallbackDuringFling) {
+ // We want to suppress selection changes
+
+ // Remove any future code to set mSuppressSelectionChanged = false
+ removeCallbacks(mDisableSuppressSelectionChangedRunnable);
+
+ // This will get reset once we scroll into slots
+ if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
+ }
+
+ // Fling the gallery!
+ mFlingRunnable.startUsingVelocity((int) -velocityX);
+
+ return true;
+ }
+
+ @Override
+ 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-
+ // info on the screen is up-to-date with the gallery's selection
+ if (!mShouldCallbackDuringFling) {
+ if (mIsFirstScroll) {
+ /*
+ * We're not notifying the client of selection changes during
+ * the fling, and this scroll could possibly be a fling. Don't
+ * do selection changes until we're sure it is not a fling.
+ */
+ if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
+ postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
+ }
+ } else {
+ if (mSuppressSelectionChanged) mSuppressSelectionChanged = false;
+ }
+
+ // Track the motion
+ trackMotionScroll(-1 * (int) distanceX);
+
+ mIsFirstScroll = false;
+ return true;
+ }
+
+ @Override
+ 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);
+ }
+
+ // Reset the multiple-scroll tracking state
+ mIsFirstScroll = 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();
+ }
+
+ @Override
+ public void onLongPress(@NonNull MotionEvent e) {
+ if (mDownTouchPosition < 0) {
+ return;
+ }
+
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+
+ final long id = getItemIdAtPosition(mDownTouchPosition);
+ dispatchLongPress(mDownTouchView, mDownTouchPosition, id, e.getX(), e.getY(), true);
+ }
+
+ // Unused methods from GestureDetector.OnGestureListener below
+
+ @Override
+ 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) {
+ if (isShowingContextMenuWithCoords()) {
+ return false;
+ }
+ return showContextMenuForChildInternal(originalView, 0, 0, false);
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView, float x, float y) {
+ return showContextMenuForChildInternal(originalView, x, y, true);
+ }
+
+ private boolean showContextMenuForChildInternal(View originalView, float x, float y,
+ boolean useOffsets) {
+ final int longPressPosition = getPositionForView(originalView);
+ if (longPressPosition < 0) {
+ return false;
+ }
+
+ final long longPressId = mAdapter.getItemId(longPressPosition);
+ return dispatchLongPress(originalView, longPressPosition, longPressId, x, y, useOffsets);
+ }
+
+ @Override
+ public boolean showContextMenu() {
+ return showContextMenuInternal(0, 0, false);
+ }
+
+ @Override
+ public boolean showContextMenu(float x, float y) {
+ return showContextMenuInternal(x, y, true);
+ }
+
+ private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
+ if (isPressed() && mSelectedPosition >= 0) {
+ final int index = mSelectedPosition - mFirstPosition;
+ final View v = getChildAt(index);
+ return dispatchLongPress(v, mSelectedPosition, mSelectedRowId, x, y, useOffsets);
+ }
+
+ return false;
+ }
+
+ private boolean dispatchLongPress(View view, int position, long id, float x, float y,
+ boolean useOffsets) {
+ boolean handled = false;
+
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
+ mDownTouchPosition, id);
+ }
+
+ if (!handled) {
+ mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
+
+ if (useOffsets) {
+ handled = super.showContextMenuForChild(view, x, y);
+ } else {
+ handled = super.showContextMenuForChild(this);
+ }
+ }
+
+ if (handled) {
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+
+ return handled;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Gallery steals all key events
+ return event.dispatch(this, null, null);
+ }
+
+ /**
+ * 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 (moveDirection(-1)) {
+ playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (moveDirection(1)) {
+ playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ mReceivedInvokeKeyDown = true;
+ // fallthrough to default handling
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (KeyEvent.isConfirmKey(keyCode)) {
+ if (mReceivedInvokeKeyDown) {
+ if (mItemCount > 0) {
+ dispatchPress(mSelectedChild);
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ dispatchUnpress();
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+
+ int selectedIndex = mSelectedPosition - mFirstPosition;
+ performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
+ .getItemId(mSelectedPosition));
+ }
+ }
+
+ // Clear the flag
+ mReceivedInvokeKeyDown = false;
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ boolean moveDirection(int direction) {
+ direction = isLayoutRtl() ? -direction : direction;
+ int targetPosition = mSelectedPosition + direction;
+
+ if (mItemCount > 0 && targetPosition >= 0 && targetPosition < mItemCount) {
+ scrollToChild(targetPosition - mFirstPosition);
+ 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 && oldSelectedChild != child) {
+
+ // 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 after the selected child earlier one
+ return i + 1;
+ } else {
+ // Keep the children before 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);
+ mSelectedChild.setSelected(true);
+ }
+
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Gallery.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setScrollable(mItemCount > 1);
+ if (isEnabled()) {
+ if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
+ final int currentChildIndex = mSelectedPosition - mFirstPosition;
+ return scrollToChild(currentChildIndex + 1);
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
+ final int currentChildIndex = mSelectedPosition - mFirstPosition;
+ return scrollToChild(currentChildIndex - 1);
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ /**
+ * 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();
+ }
+
+ @Override
+ 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 leftmost view as mDownTouchPosition
+ mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) :
+ mFirstPosition;
+
+ // Don't fling more than 1 screen
+ delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta);
+ } else {
+ // Moving towards the right. Use rightmost view as mDownTouchPosition
+ int offsetToLast = getChildCount() - 1;
+ mDownTouchPosition = mIsRtl ? mFirstPosition :
+ (mFirstPosition + getChildCount() - 1);
+
+ // 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/android/widget/GridLayout.java b/android/widget/GridLayout.java
new file mode 100644
index 0000000..cbd1e0a
--- /dev/null
+++ b/android/widget/GridLayout.java
@@ -0,0 +1,2985 @@
+/*
+ * Copyright (C) 2011 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 static android.view.Gravity.AXIS_PULL_AFTER;
+import static android.view.Gravity.AXIS_PULL_BEFORE;
+import static android.view.Gravity.AXIS_SPECIFIED;
+import static android.view.Gravity.AXIS_X_SHIFT;
+import static android.view.Gravity.AXIS_Y_SHIFT;
+import static android.view.Gravity.HORIZONTAL_GRAVITY_MASK;
+import static android.view.Gravity.RELATIVE_LAYOUT_DIRECTION;
+import static android.view.Gravity.VERTICAL_GRAVITY_MASK;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.LogPrinter;
+import android.util.Pair;
+import android.util.Printer;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A layout that places its children in a rectangular <em>grid</em>.
+ * <p>
+ * The grid is composed of a set of infinitely thin lines that separate the
+ * viewing area into <em>cells</em>. Throughout the API, grid lines are referenced
+ * by grid <em>indices</em>. A grid with {@code N} columns
+ * has {@code N + 1} grid indices that run from {@code 0}
+ * through {@code N} inclusive. Regardless of how GridLayout is
+ * configured, grid index {@code 0} is fixed to the leading edge of the
+ * container and grid index {@code N} is fixed to its trailing edge
+ * (after padding is taken into account).
+ *
+ * <h4>Row and Column Specs</h4>
+ *
+ * Children occupy one or more contiguous cells, as defined
+ * by their {@link GridLayout.LayoutParams#rowSpec rowSpec} and
+ * {@link GridLayout.LayoutParams#columnSpec columnSpec} layout parameters.
+ * Each spec defines the set of rows or columns that are to be
+ * occupied; and how children should be aligned within the resulting group of cells.
+ * Although cells do not normally overlap in a GridLayout, GridLayout does
+ * not prevent children being defined to occupy the same cell or group of cells.
+ * In this case however, there is no guarantee that children will not themselves
+ * overlap after the layout operation completes.
+ *
+ * <h4>Default Cell Assignment</h4>
+ *
+ * If a child does not specify the row and column indices of the cell it
+ * wishes to occupy, GridLayout assigns cell locations automatically using its:
+ * {@link GridLayout#setOrientation(int) orientation},
+ * {@link GridLayout#setRowCount(int) rowCount} and
+ * {@link GridLayout#setColumnCount(int) columnCount} properties.
+ *
+ * <h4>Space</h4>
+ *
+ * Space between children may be specified either by using instances of the
+ * dedicated {@link Space} view or by setting the
+ *
+ * {@link ViewGroup.MarginLayoutParams#leftMargin leftMargin},
+ * {@link ViewGroup.MarginLayoutParams#topMargin topMargin},
+ * {@link ViewGroup.MarginLayoutParams#rightMargin rightMargin} and
+ * {@link ViewGroup.MarginLayoutParams#bottomMargin bottomMargin}
+ *
+ * layout parameters. When the
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins}
+ * property is set, default margins around children are automatically
+ * allocated based on the prevailing UI style guide for the platform.
+ * Each of the margins so defined may be independently overridden by an assignment
+ * to the appropriate layout parameter.
+ * Default values will generally produce a reasonable spacing between components
+ * but values may change between different releases of the platform.
+ *
+ * <h4>Excess Space Distribution</h4>
+ *
+ * As of API 21, GridLayout's distribution of excess space accomodates the principle of weight.
+ * In the event that no weights are specified, the previous conventions are respected and
+ * columns and rows are taken as flexible if their views specify some form of alignment
+ * within their groups.
+ * <p>
+ * The flexibility of a view is therefore influenced by its alignment which is,
+ * in turn, typically defined by setting the
+ * {@link LayoutParams#setGravity(int) gravity} property of the child's layout parameters.
+ * If either a weight or alignment were defined along a given axis then the component
+ * is taken as <em>flexible</em> in that direction. If no weight or alignment was set,
+ * the component is instead assumed to be <em>inflexible</em>.
+ * <p>
+ * Multiple components in the same row or column group are
+ * considered to act in <em>parallel</em>. Such a
+ * group is flexible only if <em>all</em> of the components
+ * within it are flexible. Row and column groups that sit either side of a common boundary
+ * are instead considered to act in <em>series</em>. The composite group made of these two
+ * elements is flexible if <em>one</em> of its elements is flexible.
+ * <p>
+ * To make a column stretch, make sure all of the components inside it define a
+ * weight or a gravity. To prevent a column from stretching, ensure that one of the components
+ * in the column does not define a weight or a gravity.
+ * <p>
+ * When the principle of flexibility does not provide complete disambiguation,
+ * GridLayout's algorithms favour rows and columns that are closer to its <em>right</em>
+ * and <em>bottom</em> edges. To be more precise, GridLayout treats each of its layout
+ * parameters as a constraint in the a set of variables that define the grid-lines along a
+ * given axis. During layout, GridLayout solves the constraints so as to return the unique
+ * solution to those constraints for which all variables are less-than-or-equal-to
+ * the corresponding value in any other valid solution.
+ *
+ * <h4>Interpretation of GONE</h4>
+ *
+ * For layout purposes, GridLayout treats views whose visibility status is
+ * {@link View#GONE GONE}, as having zero width and height. This is subtly different from
+ * the policy of ignoring views that are marked as GONE outright. If, for example, a gone-marked
+ * view was alone in a column, that column would itself collapse to zero width if and only if
+ * no gravity was defined on the view. If gravity was defined, then the gone-marked
+ * view has no effect on the layout and the container should be laid out as if the view
+ * had never been added to it. GONE views are taken to have zero weight during excess space
+ * distribution.
+ * <p>
+ * These statements apply equally to rows as well as columns, and to groups of rows or columns.
+ *
+ * <p>
+ * See {@link GridLayout.LayoutParams} for a full description of the
+ * layout parameters used by GridLayout.
+ *
+ * @attr ref android.R.styleable#GridLayout_orientation
+ * @attr ref android.R.styleable#GridLayout_rowCount
+ * @attr ref android.R.styleable#GridLayout_columnCount
+ * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+ * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+ * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+ */
+@RemoteView
+public class GridLayout extends ViewGroup {
+
+ // Public constants
+
+ /** @hide */
+ @IntDef({HORIZONTAL, VERTICAL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Orientation {}
+
+ /**
+ * The horizontal orientation.
+ */
+ public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+ /**
+ * The vertical orientation.
+ */
+ public static final int VERTICAL = LinearLayout.VERTICAL;
+
+ /**
+ * The constant used to indicate that a value is undefined.
+ * Fields can use this value to indicate that their values
+ * have not yet been set. Similarly, methods can return this value
+ * to indicate that there is no suitable value that the implementation
+ * can return.
+ * The value used for the constant (currently {@link Integer#MIN_VALUE}) is
+ * intended to avoid confusion between valid values whose sign may not be known.
+ */
+ public static final int UNDEFINED = Integer.MIN_VALUE;
+
+ /** @hide */
+ @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AlignmentMode {}
+
+ /**
+ * This constant is an {@link #setAlignmentMode(int) alignmentMode}.
+ * When the {@code alignmentMode} is set to {@link #ALIGN_BOUNDS}, alignment
+ * is made between the edges of each component's raw
+ * view boundary: i.e. the area delimited by the component's:
+ * {@link android.view.View#getTop() top},
+ * {@link android.view.View#getLeft() left},
+ * {@link android.view.View#getBottom() bottom} and
+ * {@link android.view.View#getRight() right} properties.
+ * <p>
+ * For example, when {@code GridLayout} is in {@link #ALIGN_BOUNDS} mode,
+ * children that belong to a row group that uses {@link #TOP} alignment will
+ * all return the same value when their {@link android.view.View#getTop()}
+ * method is called.
+ *
+ * @see #setAlignmentMode(int)
+ */
+ public static final int ALIGN_BOUNDS = 0;
+
+ /**
+ * This constant is an {@link #setAlignmentMode(int) alignmentMode}.
+ * When the {@code alignmentMode} is set to {@link #ALIGN_MARGINS},
+ * the bounds of each view are extended outwards, according
+ * to their margins, before the edges of the resulting rectangle are aligned.
+ * <p>
+ * For example, when {@code GridLayout} is in {@link #ALIGN_MARGINS} mode,
+ * the quantity {@code top - layoutParams.topMargin} is the same for all children that
+ * belong to a row group that uses {@link #TOP} alignment.
+ *
+ * @see #setAlignmentMode(int)
+ */
+ public static final int ALIGN_MARGINS = 1;
+
+ // Misc constants
+
+ static final int MAX_SIZE = 100000;
+ static final int DEFAULT_CONTAINER_MARGIN = 0;
+ static final int UNINITIALIZED_HASH = 0;
+ static final Printer LOG_PRINTER = new LogPrinter(Log.DEBUG, GridLayout.class.getName());
+ static final Printer NO_PRINTER = new Printer() {
+ @Override
+ public void println(String x) {
+ }
+ };
+
+ // Defaults
+
+ private static final int DEFAULT_ORIENTATION = HORIZONTAL;
+ private static final int DEFAULT_COUNT = UNDEFINED;
+ private static final boolean DEFAULT_USE_DEFAULT_MARGINS = false;
+ private static final boolean DEFAULT_ORDER_PRESERVED = true;
+ private static final int DEFAULT_ALIGNMENT_MODE = ALIGN_MARGINS;
+
+ // TypedArray indices
+
+ private static final int ORIENTATION = R.styleable.GridLayout_orientation;
+ private static final int ROW_COUNT = R.styleable.GridLayout_rowCount;
+ private static final int COLUMN_COUNT = R.styleable.GridLayout_columnCount;
+ private static final int USE_DEFAULT_MARGINS = R.styleable.GridLayout_useDefaultMargins;
+ private static final int ALIGNMENT_MODE = R.styleable.GridLayout_alignmentMode;
+ private static final int ROW_ORDER_PRESERVED = R.styleable.GridLayout_rowOrderPreserved;
+ private static final int COLUMN_ORDER_PRESERVED = R.styleable.GridLayout_columnOrderPreserved;
+
+ // Instance variables
+
+ final Axis mHorizontalAxis = new Axis(true);
+ final Axis mVerticalAxis = new Axis(false);
+ int mOrientation = DEFAULT_ORIENTATION;
+ boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS;
+ int mAlignmentMode = DEFAULT_ALIGNMENT_MODE;
+ int mDefaultGap;
+ int mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
+ Printer mPrinter = LOG_PRINTER;
+
+ // Constructors
+
+ public GridLayout(Context context) {
+ this(context, null);
+ }
+
+ public GridLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public GridLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap);
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.GridLayout, defStyleAttr, defStyleRes);
+ try {
+ setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT));
+ setColumnCount(a.getInt(COLUMN_COUNT, DEFAULT_COUNT));
+ setOrientation(a.getInt(ORIENTATION, DEFAULT_ORIENTATION));
+ setUseDefaultMargins(a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS));
+ setAlignmentMode(a.getInt(ALIGNMENT_MODE, DEFAULT_ALIGNMENT_MODE));
+ setRowOrderPreserved(a.getBoolean(ROW_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED));
+ setColumnOrderPreserved(a.getBoolean(COLUMN_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED));
+ } finally {
+ a.recycle();
+ }
+ }
+
+ // Implementation
+
+ /**
+ * Returns the current orientation.
+ *
+ * @return either {@link #HORIZONTAL} or {@link #VERTICAL}
+ *
+ * @see #setOrientation(int)
+ *
+ * @attr ref android.R.styleable#GridLayout_orientation
+ */
+ @Orientation
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ *
+ * GridLayout uses the orientation property for two purposes:
+ * <ul>
+ * <li>
+ * To control the 'direction' in which default row/column indices are generated
+ * when they are not specified in a component's layout parameters.
+ * </li>
+ * <li>
+ * To control which axis should be processed first during the layout operation:
+ * when orientation is {@link #HORIZONTAL} the horizontal axis is laid out first.
+ * </li>
+ * </ul>
+ *
+ * The order in which axes are laid out is important if, for example, the height of
+ * one of GridLayout's children is dependent on its width - and its width is, in turn,
+ * dependent on the widths of other components.
+ * <p>
+ * If your layout contains a {@link TextView} (or derivative:
+ * {@code Button}, {@code EditText}, {@code CheckBox}, etc.) which is
+ * in multi-line mode (the default) it is normally best to leave GridLayout's
+ * orientation as {@code HORIZONTAL} - because {@code TextView} is capable of
+ * deriving its height for a given width, but not the other way around.
+ * <p>
+ * Other than the effects above, orientation does not affect the actual layout operation of
+ * GridLayout, so it's fine to leave GridLayout in {@code HORIZONTAL} mode even if
+ * the height of the intended layout greatly exceeds its width.
+ * <p>
+ * The default value of this property is {@link #HORIZONTAL}.
+ *
+ * @param orientation either {@link #HORIZONTAL} or {@link #VERTICAL}
+ *
+ * @see #getOrientation()
+ *
+ * @attr ref android.R.styleable#GridLayout_orientation
+ */
+ public void setOrientation(@Orientation int orientation) {
+ if (this.mOrientation != orientation) {
+ this.mOrientation = orientation;
+ invalidateStructure();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the current number of rows. This is either the last value that was set
+ * with {@link #setRowCount(int)} or, if no such value was set, the maximum
+ * value of each the upper bounds defined in {@link LayoutParams#rowSpec}.
+ *
+ * @return the current number of rows
+ *
+ * @see #setRowCount(int)
+ * @see LayoutParams#rowSpec
+ *
+ * @attr ref android.R.styleable#GridLayout_rowCount
+ */
+ public int getRowCount() {
+ return mVerticalAxis.getCount();
+ }
+
+ /**
+ * RowCount is used only to generate default row/column indices when
+ * they are not specified by a component's layout parameters.
+ *
+ * @param rowCount the number of rows
+ *
+ * @see #getRowCount()
+ * @see LayoutParams#rowSpec
+ *
+ * @attr ref android.R.styleable#GridLayout_rowCount
+ */
+ public void setRowCount(int rowCount) {
+ mVerticalAxis.setCount(rowCount);
+ invalidateStructure();
+ requestLayout();
+ }
+
+ /**
+ * Returns the current number of columns. This is either the last value that was set
+ * with {@link #setColumnCount(int)} or, if no such value was set, the maximum
+ * value of each the upper bounds defined in {@link LayoutParams#columnSpec}.
+ *
+ * @return the current number of columns
+ *
+ * @see #setColumnCount(int)
+ * @see LayoutParams#columnSpec
+ *
+ * @attr ref android.R.styleable#GridLayout_columnCount
+ */
+ public int getColumnCount() {
+ return mHorizontalAxis.getCount();
+ }
+
+ /**
+ * ColumnCount is used only to generate default column/column indices when
+ * they are not specified by a component's layout parameters.
+ *
+ * @param columnCount the number of columns.
+ *
+ * @see #getColumnCount()
+ * @see LayoutParams#columnSpec
+ *
+ * @attr ref android.R.styleable#GridLayout_columnCount
+ */
+ public void setColumnCount(int columnCount) {
+ mHorizontalAxis.setCount(columnCount);
+ invalidateStructure();
+ requestLayout();
+ }
+
+ /**
+ * Returns whether or not this GridLayout will allocate default margins when no
+ * corresponding layout parameters are defined.
+ *
+ * @return {@code true} if default margins should be allocated
+ *
+ * @see #setUseDefaultMargins(boolean)
+ *
+ * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+ */
+ public boolean getUseDefaultMargins() {
+ return mUseDefaultMargins;
+ }
+
+ /**
+ * When {@code true}, GridLayout allocates default margins around children
+ * based on the child's visual characteristics. Each of the
+ * margins so defined may be independently overridden by an assignment
+ * to the appropriate layout parameter.
+ * <p>
+ * When {@code false}, the default value of all margins is zero.
+ * <p>
+ * When setting to {@code true}, consider setting the value of the
+ * {@link #setAlignmentMode(int) alignmentMode}
+ * property to {@link #ALIGN_BOUNDS}.
+ * <p>
+ * The default value of this property is {@code false}.
+ *
+ * @param useDefaultMargins use {@code true} to make GridLayout allocate default margins
+ *
+ * @see #getUseDefaultMargins()
+ * @see #setAlignmentMode(int)
+ *
+ * @see MarginLayoutParams#leftMargin
+ * @see MarginLayoutParams#topMargin
+ * @see MarginLayoutParams#rightMargin
+ * @see MarginLayoutParams#bottomMargin
+ *
+ * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+ */
+ public void setUseDefaultMargins(boolean useDefaultMargins) {
+ this.mUseDefaultMargins = useDefaultMargins;
+ requestLayout();
+ }
+
+ /**
+ * Returns the alignment mode.
+ *
+ * @return the alignment mode; either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
+ *
+ * @see #ALIGN_BOUNDS
+ * @see #ALIGN_MARGINS
+ *
+ * @see #setAlignmentMode(int)
+ *
+ * @attr ref android.R.styleable#GridLayout_alignmentMode
+ */
+ @AlignmentMode
+ public int getAlignmentMode() {
+ return mAlignmentMode;
+ }
+
+ /**
+ * Sets the alignment mode to be used for all of the alignments between the
+ * children of this container.
+ * <p>
+ * The default value of this property is {@link #ALIGN_MARGINS}.
+ *
+ * @param alignmentMode either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
+ *
+ * @see #ALIGN_BOUNDS
+ * @see #ALIGN_MARGINS
+ *
+ * @see #getAlignmentMode()
+ *
+ * @attr ref android.R.styleable#GridLayout_alignmentMode
+ */
+ public void setAlignmentMode(@AlignmentMode int alignmentMode) {
+ this.mAlignmentMode = alignmentMode;
+ requestLayout();
+ }
+
+ /**
+ * Returns whether or not row boundaries are ordered by their grid indices.
+ *
+ * @return {@code true} if row boundaries must appear in the order of their indices,
+ * {@code false} otherwise
+ *
+ * @see #setRowOrderPreserved(boolean)
+ *
+ * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+ */
+ public boolean isRowOrderPreserved() {
+ return mVerticalAxis.isOrderPreserved();
+ }
+
+ /**
+ * When this property is {@code true}, GridLayout is forced to place the row boundaries
+ * so that their associated grid indices are in ascending order in the view.
+ * <p>
+ * When this property is {@code false} GridLayout is at liberty to place the vertical row
+ * boundaries in whatever order best fits the given constraints.
+ * <p>
+ * The default value of this property is {@code true}.
+
+ * @param rowOrderPreserved {@code true} to force GridLayout to respect the order
+ * of row boundaries
+ *
+ * @see #isRowOrderPreserved()
+ *
+ * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+ */
+ public void setRowOrderPreserved(boolean rowOrderPreserved) {
+ mVerticalAxis.setOrderPreserved(rowOrderPreserved);
+ invalidateStructure();
+ requestLayout();
+ }
+
+ /**
+ * Returns whether or not column boundaries are ordered by their grid indices.
+ *
+ * @return {@code true} if column boundaries must appear in the order of their indices,
+ * {@code false} otherwise
+ *
+ * @see #setColumnOrderPreserved(boolean)
+ *
+ * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+ */
+ public boolean isColumnOrderPreserved() {
+ return mHorizontalAxis.isOrderPreserved();
+ }
+
+ /**
+ * When this property is {@code true}, GridLayout is forced to place the column boundaries
+ * so that their associated grid indices are in ascending order in the view.
+ * <p>
+ * When this property is {@code false} GridLayout is at liberty to place the horizontal column
+ * boundaries in whatever order best fits the given constraints.
+ * <p>
+ * The default value of this property is {@code true}.
+ *
+ * @param columnOrderPreserved use {@code true} to force GridLayout to respect the order
+ * of column boundaries.
+ *
+ * @see #isColumnOrderPreserved()
+ *
+ * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+ */
+ public void setColumnOrderPreserved(boolean columnOrderPreserved) {
+ mHorizontalAxis.setOrderPreserved(columnOrderPreserved);
+ invalidateStructure();
+ requestLayout();
+ }
+
+ /**
+ * Return the printer that will log diagnostics from this layout.
+ *
+ * @see #setPrinter(android.util.Printer)
+ *
+ * @return the printer associated with this view
+ *
+ * @hide
+ */
+ public Printer getPrinter() {
+ return mPrinter;
+ }
+
+ /**
+ * Set the printer that will log diagnostics from this layout.
+ * The default value is created by {@link android.util.LogPrinter}.
+ *
+ * @param printer the printer associated with this layout
+ *
+ * @see #getPrinter()
+ *
+ * @hide
+ */
+ public void setPrinter(Printer printer) {
+ this.mPrinter = (printer == null) ? NO_PRINTER : printer;
+ }
+
+ // Static utility methods
+
+ static int max2(int[] a, int valueIfEmpty) {
+ int result = valueIfEmpty;
+ for (int i = 0, N = a.length; i < N; i++) {
+ result = Math.max(result, a[i]);
+ }
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ static <T> T[] append(T[] a, T[] b) {
+ T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length);
+ System.arraycopy(a, 0, result, 0, a.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+ static Alignment getAlignment(int gravity, boolean horizontal) {
+ int mask = horizontal ? HORIZONTAL_GRAVITY_MASK : VERTICAL_GRAVITY_MASK;
+ int shift = horizontal ? AXIS_X_SHIFT : AXIS_Y_SHIFT;
+ int flags = (gravity & mask) >> shift;
+ switch (flags) {
+ case (AXIS_SPECIFIED | AXIS_PULL_BEFORE):
+ return horizontal ? LEFT : TOP;
+ case (AXIS_SPECIFIED | AXIS_PULL_AFTER):
+ return horizontal ? RIGHT : BOTTOM;
+ case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER):
+ return FILL;
+ case AXIS_SPECIFIED:
+ return CENTER;
+ case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | RELATIVE_LAYOUT_DIRECTION):
+ return START;
+ case (AXIS_SPECIFIED | AXIS_PULL_AFTER | RELATIVE_LAYOUT_DIRECTION):
+ return END;
+ default:
+ return UNDEFINED_ALIGNMENT;
+ }
+ }
+
+ /** @noinspection UnusedParameters*/
+ private int getDefaultMargin(View c, boolean horizontal, boolean leading) {
+ if (c.getClass() == Space.class) {
+ return 0;
+ }
+ return mDefaultGap / 2;
+ }
+
+ private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) {
+ return /*isAtEdge ? DEFAULT_CONTAINER_MARGIN :*/ getDefaultMargin(c, horizontal, leading);
+ }
+
+ private int getDefaultMargin(View c, LayoutParams p, boolean horizontal, boolean leading) {
+ if (!mUseDefaultMargins) {
+ return 0;
+ }
+ Spec spec = horizontal ? p.columnSpec : p.rowSpec;
+ Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+ Interval span = spec.span;
+ boolean leading1 = (horizontal && isLayoutRtl()) ? !leading : leading;
+ boolean isAtEdge = leading1 ? (span.min == 0) : (span.max == axis.getCount());
+
+ return getDefaultMargin(c, isAtEdge, horizontal, leading);
+ }
+
+ int getMargin1(View view, boolean horizontal, boolean leading) {
+ LayoutParams lp = getLayoutParams(view);
+ int margin = horizontal ?
+ (leading ? lp.leftMargin : lp.rightMargin) :
+ (leading ? lp.topMargin : lp.bottomMargin);
+ return margin == UNDEFINED ? getDefaultMargin(view, lp, horizontal, leading) : margin;
+ }
+
+ private int getMargin(View view, boolean horizontal, boolean leading) {
+ if (mAlignmentMode == ALIGN_MARGINS) {
+ return getMargin1(view, horizontal, leading);
+ } else {
+ Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+ int[] margins = leading ? axis.getLeadingMargins() : axis.getTrailingMargins();
+ LayoutParams lp = getLayoutParams(view);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ int index = leading ? spec.span.min : spec.span.max;
+ return margins[index];
+ }
+ }
+
+ private int getTotalMargin(View child, boolean horizontal) {
+ return getMargin(child, horizontal, true) + getMargin(child, horizontal, false);
+ }
+
+ private static boolean fits(int[] a, int value, int start, int end) {
+ if (end > a.length) {
+ return false;
+ }
+ for (int i = start; i < end; i++) {
+ if (a[i] > value) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void procrusteanFill(int[] a, int start, int end, int value) {
+ int length = a.length;
+ Arrays.fill(a, Math.min(start, length), Math.min(end, length), value);
+ }
+
+ private static void setCellGroup(LayoutParams lp, int row, int rowSpan, int col, int colSpan) {
+ lp.setRowSpecSpan(new Interval(row, row + rowSpan));
+ lp.setColumnSpecSpan(new Interval(col, col + colSpan));
+ }
+
+ // Logic to avert infinite loops by ensuring that the cells can be placed somewhere.
+ private static int clip(Interval minorRange, boolean minorWasDefined, int count) {
+ int size = minorRange.size();
+ if (count == 0) {
+ return size;
+ }
+ int min = minorWasDefined ? min(minorRange.min, count) : 0;
+ return min(size, count - min);
+ }
+
+ // install default indices for cells that don't define them
+ private void validateLayoutParams() {
+ final boolean horizontal = (mOrientation == HORIZONTAL);
+ final Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+ final int count = (axis.definedCount != UNDEFINED) ? axis.definedCount : 0;
+
+ int major = 0;
+ int minor = 0;
+ int[] maxSizes = new int[count];
+
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+
+ final Spec majorSpec = horizontal ? lp.rowSpec : lp.columnSpec;
+ final Interval majorRange = majorSpec.span;
+ final boolean majorWasDefined = majorSpec.startDefined;
+ final int majorSpan = majorRange.size();
+ if (majorWasDefined) {
+ major = majorRange.min;
+ }
+
+ final Spec minorSpec = horizontal ? lp.columnSpec : lp.rowSpec;
+ final Interval minorRange = minorSpec.span;
+ final boolean minorWasDefined = minorSpec.startDefined;
+ final int minorSpan = clip(minorRange, minorWasDefined, count);
+ if (minorWasDefined) {
+ minor = minorRange.min;
+ }
+
+ if (count != 0) {
+ // Find suitable row/col values when at least one is undefined.
+ if (!majorWasDefined || !minorWasDefined) {
+ while (!fits(maxSizes, major, minor, minor + minorSpan)) {
+ if (minorWasDefined) {
+ major++;
+ } else {
+ if (minor + minorSpan <= count) {
+ minor++;
+ } else {
+ minor = 0;
+ major++;
+ }
+ }
+ }
+ }
+ procrusteanFill(maxSizes, minor, minor + minorSpan, major + majorSpan);
+ }
+
+ if (horizontal) {
+ setCellGroup(lp, major, majorSpan, minor, minorSpan);
+ } else {
+ setCellGroup(lp, minor, minorSpan, major, majorSpan);
+ }
+
+ minor = minor + minorSpan;
+ }
+ }
+
+ private void invalidateStructure() {
+ mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
+ mHorizontalAxis.invalidateStructure();
+ mVerticalAxis.invalidateStructure();
+ // This can end up being done twice. Better twice than not at all.
+ invalidateValues();
+ }
+
+ private void invalidateValues() {
+ // Need null check because requestLayout() is called in View's initializer,
+ // before we are set up.
+ if (mHorizontalAxis != null && mVerticalAxis != null) {
+ mHorizontalAxis.invalidateValues();
+ mVerticalAxis.invalidateValues();
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void onSetLayoutParams(View child, ViewGroup.LayoutParams layoutParams) {
+ super.onSetLayoutParams(child, layoutParams);
+
+ if (!checkLayoutParams(layoutParams)) {
+ handleInvalidParams("supplied LayoutParams are of the wrong type");
+ }
+
+ invalidateStructure();
+ }
+
+ final LayoutParams getLayoutParams(View c) {
+ return (LayoutParams) c.getLayoutParams();
+ }
+
+ private static void handleInvalidParams(String msg) {
+ throw new IllegalArgumentException(msg + ". ");
+ }
+
+ private void checkLayoutParams(LayoutParams lp, boolean horizontal) {
+ String groupName = horizontal ? "column" : "row";
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ Interval span = spec.span;
+ if (span.min != UNDEFINED && span.min < 0) {
+ handleInvalidParams(groupName + " indices must be positive");
+ }
+ Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+ int count = axis.definedCount;
+ if (count != UNDEFINED) {
+ if (span.max > count) {
+ handleInvalidParams(groupName +
+ " indices (start + span) mustn't exceed the " + groupName + " count");
+ }
+ if (span.size() > count) {
+ handleInvalidParams(groupName + " span mustn't exceed the " + groupName + " count");
+ }
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ if (!(p instanceof LayoutParams)) {
+ return false;
+ }
+ LayoutParams lp = (LayoutParams) p;
+
+ checkLayoutParams(lp, true);
+ checkLayoutParams(lp, false);
+
+ return true;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (sPreserveMarginParamsInLayoutParamConversion) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ }
+ }
+ return new LayoutParams(lp);
+ }
+
+ // Draw grid
+
+ private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) {
+ if (isLayoutRtl()) {
+ int width = getWidth();
+ graphics.drawLine(width - x1, y1, width - x2, y2, paint);
+ } else {
+ graphics.drawLine(x1, y1, x2, y2, paint);
+ }
+ }
+
+ @Override
+ protected void onDebugDrawMargins(Canvas canvas, Paint paint) {
+ // Apply defaults, so as to remove UNDEFINED values
+ LayoutParams lp = new LayoutParams();
+ for (int i = 0; i < getChildCount(); i++) {
+ View c = getChildAt(i);
+ lp.setMargins(
+ getMargin1(c, true, true),
+ getMargin1(c, false, true),
+ getMargin1(c, true, false),
+ getMargin1(c, false, false));
+ lp.onDebugDraw(c, canvas, paint);
+ }
+ }
+
+ @Override
+ protected void onDebugDraw(Canvas canvas) {
+ Paint paint = new Paint();
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(Color.argb(50, 255, 255, 255));
+
+ Insets insets = getOpticalInsets();
+
+ int top = getPaddingTop() + insets.top;
+ int left = getPaddingLeft() + insets.left;
+ int right = getWidth() - getPaddingRight() - insets.right;
+ int bottom = getHeight() - getPaddingBottom() - insets.bottom;
+
+ int[] xs = mHorizontalAxis.locations;
+ if (xs != null) {
+ for (int i = 0, length = xs.length; i < length; i++) {
+ int x = left + xs[i];
+ drawLine(canvas, x, top, x, bottom, paint);
+ }
+ }
+
+ int[] ys = mVerticalAxis.locations;
+ if (ys != null) {
+ for (int i = 0, length = ys.length; i < length; i++) {
+ int y = top + ys[i];
+ drawLine(canvas, left, y, right, y, paint);
+ }
+ }
+
+ super.onDebugDraw(canvas);
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ invalidateStructure();
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ invalidateStructure();
+ }
+
+ /**
+ * We need to call invalidateStructure() when a child's GONE flag changes state.
+ * This implementation is a catch-all, invalidating on any change in the visibility flags.
+ *
+ * @hide
+ */
+ @Override
+ protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
+ super.onChildVisibilityChanged(child, oldVisibility, newVisibility);
+ if (oldVisibility == GONE || newVisibility == GONE) {
+ invalidateStructure();
+ }
+ }
+
+ private int computeLayoutParamsHashCode() {
+ int result = 1;
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) continue;
+ LayoutParams lp = (LayoutParams) c.getLayoutParams();
+ result = 31 * result + lp.hashCode();
+ }
+ return result;
+ }
+
+ private void consistencyCheck() {
+ if (mLastLayoutParamsHashCode == UNINITIALIZED_HASH) {
+ validateLayoutParams();
+ mLastLayoutParamsHashCode = computeLayoutParamsHashCode();
+ } else if (mLastLayoutParamsHashCode != computeLayoutParamsHashCode()) {
+ mPrinter.println("The fields of some layout parameters were modified in between "
+ + "layout operations. Check the javadoc for GridLayout.LayoutParams#rowSpec.");
+ invalidateStructure();
+ consistencyCheck();
+ }
+ }
+
+ // Measurement
+
+ // Note: padding has already been removed from the supplied specs
+ private void measureChildWithMargins2(View child, int parentWidthSpec, int parentHeightSpec,
+ int childWidth, int childHeight) {
+ int childWidthSpec = getChildMeasureSpec(parentWidthSpec,
+ getTotalMargin(child, true), childWidth);
+ int childHeightSpec = getChildMeasureSpec(parentHeightSpec,
+ getTotalMargin(child, false), childHeight);
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ // Note: padding has already been removed from the supplied specs
+ private void measureChildrenWithMargins(int widthSpec, int heightSpec, boolean firstPass) {
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) continue;
+ LayoutParams lp = getLayoutParams(c);
+ if (firstPass) {
+ measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height);
+ } else {
+ boolean horizontal = (mOrientation == HORIZONTAL);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ if (spec.getAbsoluteAlignment(horizontal) == FILL) {
+ Interval span = spec.span;
+ Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+ int[] locations = axis.getLocations();
+ int cellSize = locations[span.max] - locations[span.min];
+ int viewSize = cellSize - getTotalMargin(c, horizontal);
+ if (horizontal) {
+ measureChildWithMargins2(c, widthSpec, heightSpec, viewSize, lp.height);
+ } else {
+ measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, viewSize);
+ }
+ }
+ }
+ }
+ }
+
+ static int adjust(int measureSpec, int delta) {
+ return makeMeasureSpec(
+ MeasureSpec.getSize(measureSpec + delta), MeasureSpec.getMode(measureSpec));
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ consistencyCheck();
+
+ /** If we have been called by {@link View#measure(int, int)}, one of width or height
+ * is likely to have changed. We must invalidate if so. */
+ invalidateValues();
+
+ int hPadding = getPaddingLeft() + getPaddingRight();
+ int vPadding = getPaddingTop() + getPaddingBottom();
+
+ int widthSpecSansPadding = adjust( widthSpec, -hPadding);
+ int heightSpecSansPadding = adjust(heightSpec, -vPadding);
+
+ measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, true);
+
+ int widthSansPadding;
+ int heightSansPadding;
+
+ // Use the orientation property to decide which axis should be laid out first.
+ if (mOrientation == HORIZONTAL) {
+ widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
+ measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
+ heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
+ } else {
+ heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
+ measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
+ widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
+ }
+
+ int measuredWidth = Math.max(widthSansPadding + hPadding, getSuggestedMinimumWidth());
+ int measuredHeight = Math.max(heightSansPadding + vPadding, getSuggestedMinimumHeight());
+
+ setMeasuredDimension(
+ resolveSizeAndState(measuredWidth, widthSpec, 0),
+ resolveSizeAndState(measuredHeight, heightSpec, 0));
+ }
+
+ private int getMeasurement(View c, boolean horizontal) {
+ return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight();
+ }
+
+ final int getMeasurementIncludingMargin(View c, boolean horizontal) {
+ if (c.getVisibility() == View.GONE) {
+ return 0;
+ }
+ return getMeasurement(c, horizontal) + getTotalMargin(c, horizontal);
+ }
+
+ @Override
+ public void requestLayout() {
+ super.requestLayout();
+ invalidateValues();
+ }
+
+ // Layout container
+
+ /**
+ * {@inheritDoc}
+ */
+ /*
+ The layout operation is implemented by delegating the heavy lifting to the
+ to the mHorizontalAxis and mVerticalAxis instances of the internal Axis class.
+ Together they compute the locations of the vertical and horizontal lines of
+ the grid (respectively!).
+
+ This method is then left with the simpler task of applying margins, gravity
+ and sizing to each child view and then placing it in its cell.
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ consistencyCheck();
+
+ int targetWidth = right - left;
+ int targetHeight = bottom - top;
+
+ int paddingLeft = getPaddingLeft();
+ int paddingTop = getPaddingTop();
+ int paddingRight = getPaddingRight();
+ int paddingBottom = getPaddingBottom();
+
+ mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight);
+ mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom);
+
+ int[] hLocations = mHorizontalAxis.getLocations();
+ int[] vLocations = mVerticalAxis.getLocations();
+
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) continue;
+ LayoutParams lp = getLayoutParams(c);
+ Spec columnSpec = lp.columnSpec;
+ Spec rowSpec = lp.rowSpec;
+
+ Interval colSpan = columnSpec.span;
+ Interval rowSpan = rowSpec.span;
+
+ int x1 = hLocations[colSpan.min];
+ int y1 = vLocations[rowSpan.min];
+
+ int x2 = hLocations[colSpan.max];
+ int y2 = vLocations[rowSpan.max];
+
+ int cellWidth = x2 - x1;
+ int cellHeight = y2 - y1;
+
+ int pWidth = getMeasurement(c, true);
+ int pHeight = getMeasurement(c, false);
+
+ Alignment hAlign = columnSpec.getAbsoluteAlignment(true);
+ Alignment vAlign = rowSpec.getAbsoluteAlignment(false);
+
+ Bounds boundsX = mHorizontalAxis.getGroupBounds().getValue(i);
+ Bounds boundsY = mVerticalAxis.getGroupBounds().getValue(i);
+
+ // Gravity offsets: the location of the alignment group relative to its cell group.
+ int gravityOffsetX = hAlign.getGravityOffset(c, cellWidth - boundsX.size(true));
+ int gravityOffsetY = vAlign.getGravityOffset(c, cellHeight - boundsY.size(true));
+
+ int leftMargin = getMargin(c, true, true);
+ int topMargin = getMargin(c, false, true);
+ int rightMargin = getMargin(c, true, false);
+ int bottomMargin = getMargin(c, false, false);
+
+ int sumMarginsX = leftMargin + rightMargin;
+ int sumMarginsY = topMargin + bottomMargin;
+
+ // Alignment offsets: the location of the view relative to its alignment group.
+ int alignmentOffsetX = boundsX.getOffset(this, c, hAlign, pWidth + sumMarginsX, true);
+ int alignmentOffsetY = boundsY.getOffset(this, c, vAlign, pHeight + sumMarginsY, false);
+
+ int width = hAlign.getSizeInCell(c, pWidth, cellWidth - sumMarginsX);
+ int height = vAlign.getSizeInCell(c, pHeight, cellHeight - sumMarginsY);
+
+ int dx = x1 + gravityOffsetX + alignmentOffsetX;
+
+ int cx = !isLayoutRtl() ? paddingLeft + leftMargin + dx :
+ targetWidth - width - paddingRight - rightMargin - dx;
+ int cy = paddingTop + y1 + gravityOffsetY + alignmentOffsetY + topMargin;
+
+ if (width != c.getMeasuredWidth() || height != c.getMeasuredHeight()) {
+ c.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
+ }
+ c.layout(cx, cy, cx + width, cy + height);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return GridLayout.class.getName();
+ }
+
+ // Inner classes
+
+ /*
+ This internal class houses the algorithm for computing the locations of grid lines;
+ along either the horizontal or vertical axis. A GridLayout uses two instances of this class -
+ distinguished by the "horizontal" flag which is true for the horizontal axis and false
+ for the vertical one.
+ */
+ final class Axis {
+ private static final int NEW = 0;
+ private static final int PENDING = 1;
+ private static final int COMPLETE = 2;
+
+ public final boolean horizontal;
+
+ public int definedCount = UNDEFINED;
+ private int maxIndex = UNDEFINED;
+
+ PackedMap<Spec, Bounds> groupBounds;
+ public boolean groupBoundsValid = false;
+
+ PackedMap<Interval, MutableInt> forwardLinks;
+ public boolean forwardLinksValid = false;
+
+ PackedMap<Interval, MutableInt> backwardLinks;
+ public boolean backwardLinksValid = false;
+
+ public int[] leadingMargins;
+ public boolean leadingMarginsValid = false;
+
+ public int[] trailingMargins;
+ public boolean trailingMarginsValid = false;
+
+ public Arc[] arcs;
+ public boolean arcsValid = false;
+
+ public int[] locations;
+ public boolean locationsValid = false;
+
+ public boolean hasWeights;
+ public boolean hasWeightsValid = false;
+ public int[] deltas;
+
+ boolean orderPreserved = DEFAULT_ORDER_PRESERVED;
+
+ private MutableInt parentMin = new MutableInt(0);
+ private MutableInt parentMax = new MutableInt(-MAX_SIZE);
+
+ private Axis(boolean horizontal) {
+ this.horizontal = horizontal;
+ }
+
+ private int calculateMaxIndex() {
+ // the number Integer.MIN_VALUE + 1 comes up in undefined cells
+ int result = -1;
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ LayoutParams params = getLayoutParams(c);
+ Spec spec = horizontal ? params.columnSpec : params.rowSpec;
+ Interval span = spec.span;
+ result = max(result, span.min);
+ result = max(result, span.max);
+ result = max(result, span.size());
+ }
+ return result == -1 ? UNDEFINED : result;
+ }
+
+ private int getMaxIndex() {
+ if (maxIndex == UNDEFINED) {
+ maxIndex = max(0, calculateMaxIndex()); // use zero when there are no children
+ }
+ return maxIndex;
+ }
+
+ public int getCount() {
+ return max(definedCount, getMaxIndex());
+ }
+
+ public void setCount(int count) {
+ if (count != UNDEFINED && count < getMaxIndex()) {
+ handleInvalidParams((horizontal ? "column" : "row") +
+ "Count must be greater than or equal to the maximum of all grid indices " +
+ "(and spans) defined in the LayoutParams of each child");
+ }
+ this.definedCount = count;
+ }
+
+ public boolean isOrderPreserved() {
+ return orderPreserved;
+ }
+
+ public void setOrderPreserved(boolean orderPreserved) {
+ this.orderPreserved = orderPreserved;
+ invalidateStructure();
+ }
+
+ private PackedMap<Spec, Bounds> createGroupBounds() {
+ Assoc<Spec, Bounds> assoc = Assoc.of(Spec.class, Bounds.class);
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ // we must include views that are GONE here, see introductory javadoc
+ LayoutParams lp = getLayoutParams(c);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ Bounds bounds = spec.getAbsoluteAlignment(horizontal).getBounds();
+ assoc.put(spec, bounds);
+ }
+ return assoc.pack();
+ }
+
+ private void computeGroupBounds() {
+ Bounds[] values = groupBounds.values;
+ for (int i = 0; i < values.length; i++) {
+ values[i].reset();
+ }
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ // we must include views that are GONE here, see introductory javadoc
+ LayoutParams lp = getLayoutParams(c);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ int size = getMeasurementIncludingMargin(c, horizontal) +
+ ((spec.weight == 0) ? 0 : getDeltas()[i]);
+ groupBounds.getValue(i).include(GridLayout.this, c, spec, this, size);
+ }
+ }
+
+ public PackedMap<Spec, Bounds> getGroupBounds() {
+ if (groupBounds == null) {
+ groupBounds = createGroupBounds();
+ }
+ if (!groupBoundsValid) {
+ computeGroupBounds();
+ groupBoundsValid = true;
+ }
+ return groupBounds;
+ }
+
+ // Add values computed by alignment - taking the max of all alignments in each span
+ private PackedMap<Interval, MutableInt> createLinks(boolean min) {
+ Assoc<Interval, MutableInt> result = Assoc.of(Interval.class, MutableInt.class);
+ Spec[] keys = getGroupBounds().keys;
+ for (int i = 0, N = keys.length; i < N; i++) {
+ Interval span = min ? keys[i].span : keys[i].span.inverse();
+ result.put(span, new MutableInt());
+ }
+ return result.pack();
+ }
+
+ private void computeLinks(PackedMap<Interval, MutableInt> links, boolean min) {
+ MutableInt[] spans = links.values;
+ for (int i = 0; i < spans.length; i++) {
+ spans[i].reset();
+ }
+
+ // Use getter to trigger a re-evaluation
+ Bounds[] bounds = getGroupBounds().values;
+ for (int i = 0; i < bounds.length; i++) {
+ int size = bounds[i].size(min);
+ MutableInt valueHolder = links.getValue(i);
+ // this effectively takes the max() of the minima and the min() of the maxima
+ valueHolder.value = max(valueHolder.value, min ? size : -size);
+ }
+ }
+
+ private PackedMap<Interval, MutableInt> getForwardLinks() {
+ if (forwardLinks == null) {
+ forwardLinks = createLinks(true);
+ }
+ if (!forwardLinksValid) {
+ computeLinks(forwardLinks, true);
+ forwardLinksValid = true;
+ }
+ return forwardLinks;
+ }
+
+ private PackedMap<Interval, MutableInt> getBackwardLinks() {
+ if (backwardLinks == null) {
+ backwardLinks = createLinks(false);
+ }
+ if (!backwardLinksValid) {
+ computeLinks(backwardLinks, false);
+ backwardLinksValid = true;
+ }
+ return backwardLinks;
+ }
+
+ private void include(List<Arc> arcs, Interval key, MutableInt size,
+ boolean ignoreIfAlreadyPresent) {
+ /*
+ Remove self referential links.
+ These appear:
+ . as parental constraints when GridLayout has no children
+ . when components have been marked as GONE
+ */
+ if (key.size() == 0) {
+ return;
+ }
+ // this bit below should really be computed outside here -
+ // its just to stop default (row/col > 0) constraints obliterating valid entries
+ if (ignoreIfAlreadyPresent) {
+ for (Arc arc : arcs) {
+ Interval span = arc.span;
+ if (span.equals(key)) {
+ return;
+ }
+ }
+ }
+ arcs.add(new Arc(key, size));
+ }
+
+ private void include(List<Arc> arcs, Interval key, MutableInt size) {
+ include(arcs, key, size, true);
+ }
+
+ // Group arcs by their first vertex, returning an array of arrays.
+ // This is linear in the number of arcs.
+ Arc[][] groupArcsByFirstVertex(Arc[] arcs) {
+ int N = getCount() + 1; // the number of vertices
+ Arc[][] result = new Arc[N][];
+ int[] sizes = new int[N];
+ for (Arc arc : arcs) {
+ sizes[arc.span.min]++;
+ }
+ for (int i = 0; i < sizes.length; i++) {
+ result[i] = new Arc[sizes[i]];
+ }
+ // reuse the sizes array to hold the current last elements as we insert each arc
+ Arrays.fill(sizes, 0);
+ for (Arc arc : arcs) {
+ int i = arc.span.min;
+ result[i][sizes[i]++] = arc;
+ }
+
+ return result;
+ }
+
+ private Arc[] topologicalSort(final Arc[] arcs) {
+ return new Object() {
+ Arc[] result = new Arc[arcs.length];
+ int cursor = result.length - 1;
+ Arc[][] arcsByVertex = groupArcsByFirstVertex(arcs);
+ int[] visited = new int[getCount() + 1];
+
+ void walk(int loc) {
+ switch (visited[loc]) {
+ case NEW: {
+ visited[loc] = PENDING;
+ for (Arc arc : arcsByVertex[loc]) {
+ walk(arc.span.max);
+ result[cursor--] = arc;
+ }
+ visited[loc] = COMPLETE;
+ break;
+ }
+ case PENDING: {
+ // le singe est dans l'arbre
+ assert false;
+ break;
+ }
+ case COMPLETE: {
+ break;
+ }
+ }
+ }
+
+ Arc[] sort() {
+ for (int loc = 0, N = arcsByVertex.length; loc < N; loc++) {
+ walk(loc);
+ }
+ assert cursor == -1;
+ return result;
+ }
+ }.sort();
+ }
+
+ private Arc[] topologicalSort(List<Arc> arcs) {
+ return topologicalSort(arcs.toArray(new Arc[arcs.size()]));
+ }
+
+ private void addComponentSizes(List<Arc> result, PackedMap<Interval, MutableInt> links) {
+ for (int i = 0; i < links.keys.length; i++) {
+ Interval key = links.keys[i];
+ include(result, key, links.values[i], false);
+ }
+ }
+
+ private Arc[] createArcs() {
+ List<Arc> mins = new ArrayList<Arc>();
+ List<Arc> maxs = new ArrayList<Arc>();
+
+ // Add the minimum values from the components.
+ addComponentSizes(mins, getForwardLinks());
+ // Add the maximum values from the components.
+ addComponentSizes(maxs, getBackwardLinks());
+
+ // Add ordering constraints to prevent row/col sizes from going negative
+ if (orderPreserved) {
+ // Add a constraint for every row/col
+ for (int i = 0; i < getCount(); i++) {
+ include(mins, new Interval(i, i + 1), new MutableInt(0));
+ }
+ }
+
+ // Add the container constraints. Use the version of include that allows
+ // duplicate entries in case a child spans the entire grid.
+ int N = getCount();
+ include(mins, new Interval(0, N), parentMin, false);
+ include(maxs, new Interval(N, 0), parentMax, false);
+
+ // Sort
+ Arc[] sMins = topologicalSort(mins);
+ Arc[] sMaxs = topologicalSort(maxs);
+
+ return append(sMins, sMaxs);
+ }
+
+ private void computeArcs() {
+ // getting the links validates the values that are shared by the arc list
+ getForwardLinks();
+ getBackwardLinks();
+ }
+
+ public Arc[] getArcs() {
+ if (arcs == null) {
+ arcs = createArcs();
+ }
+ if (!arcsValid) {
+ computeArcs();
+ arcsValid = true;
+ }
+ return arcs;
+ }
+
+ private boolean relax(int[] locations, Arc entry) {
+ if (!entry.valid) {
+ return false;
+ }
+ Interval span = entry.span;
+ int u = span.min;
+ int v = span.max;
+ int value = entry.value.value;
+ int candidate = locations[u] + value;
+ if (candidate > locations[v]) {
+ locations[v] = candidate;
+ return true;
+ }
+ return false;
+ }
+
+ private void init(int[] locations) {
+ Arrays.fill(locations, 0);
+ }
+
+ private String arcsToString(List<Arc> arcs) {
+ String var = horizontal ? "x" : "y";
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+ for (Arc arc : arcs) {
+ if (first) {
+ first = false;
+ } else {
+ result = result.append(", ");
+ }
+ int src = arc.span.min;
+ int dst = arc.span.max;
+ int value = arc.value.value;
+ result.append((src < dst) ?
+ var + dst + "-" + var + src + ">=" + value :
+ var + src + "-" + var + dst + "<=" + -value);
+
+ }
+ return result.toString();
+ }
+
+ private void logError(String axisName, Arc[] arcs, boolean[] culprits0) {
+ List<Arc> culprits = new ArrayList<Arc>();
+ List<Arc> removed = new ArrayList<Arc>();
+ for (int c = 0; c < arcs.length; c++) {
+ Arc arc = arcs[c];
+ if (culprits0[c]) {
+ culprits.add(arc);
+ }
+ if (!arc.valid) {
+ removed.add(arc);
+ }
+ }
+ mPrinter.println(axisName + " constraints: " + arcsToString(culprits) +
+ " are inconsistent; permanently removing: " + arcsToString(removed) + ". ");
+ }
+
+ /*
+ Bellman-Ford variant - modified to reduce typical running time from O(N^2) to O(N)
+
+ GridLayout converts its requirements into a system of linear constraints of the
+ form:
+
+ x[i] - x[j] < a[k]
+
+ Where the x[i] are variables and the a[k] are constants.
+
+ For example, if the variables were instead labeled x, y, z we might have:
+
+ x - y < 17
+ y - z < 23
+ z - x < 42
+
+ This is a special case of the Linear Programming problem that is, in turn,
+ equivalent to the single-source shortest paths problem on a digraph, for
+ which the O(n^2) Bellman-Ford algorithm the most commonly used general solution.
+ */
+ private boolean solve(Arc[] arcs, int[] locations) {
+ return solve(arcs, locations, true);
+ }
+
+ private boolean solve(Arc[] arcs, int[] locations, boolean modifyOnError) {
+ String axisName = horizontal ? "horizontal" : "vertical";
+ int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1.
+ boolean[] originalCulprits = null;
+
+ for (int p = 0; p < arcs.length; p++) {
+ init(locations);
+
+ // We take one extra pass over traditional Bellman-Ford (and omit their final step)
+ for (int i = 0; i < N; i++) {
+ boolean changed = false;
+ for (int j = 0, length = arcs.length; j < length; j++) {
+ changed |= relax(locations, arcs[j]);
+ }
+ if (!changed) {
+ if (originalCulprits != null) {
+ logError(axisName, arcs, originalCulprits);
+ }
+ return true;
+ }
+ }
+
+ if (!modifyOnError) {
+ return false; // cannot solve with these constraints
+ }
+
+ boolean[] culprits = new boolean[arcs.length];
+ for (int i = 0; i < N; i++) {
+ for (int j = 0, length = arcs.length; j < length; j++) {
+ culprits[j] |= relax(locations, arcs[j]);
+ }
+ }
+
+ if (p == 0) {
+ originalCulprits = culprits;
+ }
+
+ for (int i = 0; i < arcs.length; i++) {
+ if (culprits[i]) {
+ Arc arc = arcs[i];
+ // Only remove max values, min values alone cannot be inconsistent
+ if (arc.span.min < arc.span.max) {
+ continue;
+ }
+ arc.valid = false;
+ break;
+ }
+ }
+ }
+ return true;
+ }
+
+ private void computeMargins(boolean leading) {
+ int[] margins = leading ? leadingMargins : trailingMargins;
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) continue;
+ LayoutParams lp = getLayoutParams(c);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ Interval span = spec.span;
+ int index = leading ? span.min : span.max;
+ margins[index] = max(margins[index], getMargin1(c, horizontal, leading));
+ }
+ }
+
+ // External entry points
+
+ public int[] getLeadingMargins() {
+ if (leadingMargins == null) {
+ leadingMargins = new int[getCount() + 1];
+ }
+ if (!leadingMarginsValid) {
+ computeMargins(true);
+ leadingMarginsValid = true;
+ }
+ return leadingMargins;
+ }
+
+ public int[] getTrailingMargins() {
+ if (trailingMargins == null) {
+ trailingMargins = new int[getCount() + 1];
+ }
+ if (!trailingMarginsValid) {
+ computeMargins(false);
+ trailingMarginsValid = true;
+ }
+ return trailingMargins;
+ }
+
+ private boolean solve(int[] a) {
+ return solve(getArcs(), a);
+ }
+
+ private boolean computeHasWeights() {
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+ LayoutParams lp = getLayoutParams(child);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ if (spec.weight != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasWeights() {
+ if (!hasWeightsValid) {
+ hasWeights = computeHasWeights();
+ hasWeightsValid = true;
+ }
+ return hasWeights;
+ }
+
+ public int[] getDeltas() {
+ if (deltas == null) {
+ deltas = new int[getChildCount()];
+ }
+ return deltas;
+ }
+
+ private void shareOutDelta(int totalDelta, float totalWeight) {
+ Arrays.fill(deltas, 0);
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ final View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) {
+ continue;
+ }
+ LayoutParams lp = getLayoutParams(c);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ float weight = spec.weight;
+ if (weight != 0) {
+ int delta = Math.round((weight * totalDelta / totalWeight));
+ deltas[i] = delta;
+ // the two adjustments below are to counter the above rounding and avoid
+ // off-by-ones at the end
+ totalDelta -= delta;
+ totalWeight -= weight;
+ }
+ }
+ }
+
+ private void solveAndDistributeSpace(int[] a) {
+ Arrays.fill(getDeltas(), 0);
+ solve(a);
+ int deltaMax = parentMin.value * getChildCount() + 1; //exclusive
+ if (deltaMax < 2) {
+ return; //don't have any delta to distribute
+ }
+ int deltaMin = 0; //inclusive
+
+ float totalWeight = calculateTotalWeight();
+
+ int validDelta = -1; //delta for which a solution exists
+ boolean validSolution = true;
+ // do a binary search to find the max delta that won't conflict with constraints
+ while(deltaMin < deltaMax) {
+ // cast to long to prevent overflow.
+ final int delta = (int) (((long) deltaMin + deltaMax) / 2);
+ invalidateValues();
+ shareOutDelta(delta, totalWeight);
+ validSolution = solve(getArcs(), a, false);
+ if (validSolution) {
+ validDelta = delta;
+ deltaMin = delta + 1;
+ } else {
+ deltaMax = delta;
+ }
+ }
+ if (validDelta > 0 && !validSolution) {
+ // last solution was not successful but we have a successful one. Use it.
+ invalidateValues();
+ shareOutDelta(validDelta, totalWeight);
+ solve(a);
+ }
+ }
+
+ private float calculateTotalWeight() {
+ float totalWeight = 0f;
+ for (int i = 0, N = getChildCount(); i < N; i++) {
+ View c = getChildAt(i);
+ if (c.getVisibility() == View.GONE) {
+ continue;
+ }
+ LayoutParams lp = getLayoutParams(c);
+ Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+ totalWeight += spec.weight;
+ }
+ return totalWeight;
+ }
+
+ private void computeLocations(int[] a) {
+ if (!hasWeights()) {
+ solve(a);
+ } else {
+ solveAndDistributeSpace(a);
+ }
+ if (!orderPreserved) {
+ // Solve returns the smallest solution to the constraint system for which all
+ // values are positive. One value is therefore zero - though if the row/col
+ // order is not preserved this may not be the first vertex. For consistency,
+ // translate all the values so that they measure the distance from a[0]; the
+ // leading edge of the parent. After this transformation some values may be
+ // negative.
+ int a0 = a[0];
+ for (int i = 0, N = a.length; i < N; i++) {
+ a[i] = a[i] - a0;
+ }
+ }
+ }
+
+ public int[] getLocations() {
+ if (locations == null) {
+ int N = getCount() + 1;
+ locations = new int[N];
+ }
+ if (!locationsValid) {
+ computeLocations(locations);
+ locationsValid = true;
+ }
+ return locations;
+ }
+
+ private int size(int[] locations) {
+ // The parental edges are attached to vertices 0 and N - even when order is not
+ // being preserved and other vertices fall outside this range. Measure the distance
+ // between vertices 0 and N, assuming that locations[0] = 0.
+ return locations[getCount()];
+ }
+
+ private void setParentConstraints(int min, int max) {
+ parentMin.value = min;
+ parentMax.value = -max;
+ locationsValid = false;
+ }
+
+ private int getMeasure(int min, int max) {
+ setParentConstraints(min, max);
+ return size(getLocations());
+ }
+
+ public int getMeasure(int measureSpec) {
+ int mode = MeasureSpec.getMode(measureSpec);
+ int size = MeasureSpec.getSize(measureSpec);
+ switch (mode) {
+ case MeasureSpec.UNSPECIFIED: {
+ return getMeasure(0, MAX_SIZE);
+ }
+ case MeasureSpec.EXACTLY: {
+ return getMeasure(size, size);
+ }
+ case MeasureSpec.AT_MOST: {
+ return getMeasure(0, size);
+ }
+ default: {
+ assert false;
+ return 0;
+ }
+ }
+ }
+
+ public void layout(int size) {
+ setParentConstraints(size, size);
+ getLocations();
+ }
+
+ public void invalidateStructure() {
+ maxIndex = UNDEFINED;
+
+ groupBounds = null;
+ forwardLinks = null;
+ backwardLinks = null;
+
+ leadingMargins = null;
+ trailingMargins = null;
+ arcs = null;
+
+ locations = null;
+
+ deltas = null;
+ hasWeightsValid = false;
+
+ invalidateValues();
+ }
+
+ public void invalidateValues() {
+ groupBoundsValid = false;
+ forwardLinksValid = false;
+ backwardLinksValid = false;
+
+ leadingMarginsValid = false;
+ trailingMarginsValid = false;
+ arcsValid = false;
+
+ locationsValid = false;
+ }
+ }
+
+ /**
+ * Layout information associated with each of the children of a GridLayout.
+ * <p>
+ * GridLayout supports both row and column spanning and arbitrary forms of alignment within
+ * each cell group. The fundamental parameters associated with each cell group are
+ * gathered into their vertical and horizontal components and stored
+ * in the {@link #rowSpec} and {@link #columnSpec} layout parameters.
+ * {@link GridLayout.Spec Specs} are immutable structures
+ * and may be shared between the layout parameters of different children.
+ * <p>
+ * The row and column specs contain the leading and trailing indices along each axis
+ * and together specify the four grid indices that delimit the cells of this cell group.
+ * <p>
+ * The alignment properties of the row and column specs together specify
+ * both aspects of alignment within the cell group. It is also possible to specify a child's
+ * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)}
+ * method.
+ * <p>
+ * The weight property is also included in Spec and specifies the proportion of any
+ * excess space that is due to the associated view.
+ *
+ * <h4>WRAP_CONTENT and MATCH_PARENT</h4>
+ *
+ * Because the default values of the {@link #width} and {@link #height}
+ * properties are both {@link #WRAP_CONTENT}, this value never needs to be explicitly
+ * declared in the layout parameters of GridLayout's children. In addition,
+ * GridLayout does not distinguish the special size value {@link #MATCH_PARENT} from
+ * {@link #WRAP_CONTENT}. A component's ability to expand to the size of the parent is
+ * instead controlled by the principle of <em>flexibility</em>,
+ * as discussed in {@link GridLayout}.
+ *
+ * <h4>Summary</h4>
+ *
+ * You should not need to use either of the special size values:
+ * {@code WRAP_CONTENT} or {@code MATCH_PARENT} when configuring the children of
+ * a GridLayout.
+ *
+ * <h4>Default values</h4>
+ *
+ * <ul>
+ * <li>{@link #width} = {@link #WRAP_CONTENT}</li>
+ * <li>{@link #height} = {@link #WRAP_CONTENT}</li>
+ * <li>{@link #topMargin} = 0 when
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+ * {@code false}; otherwise {@link #UNDEFINED}, to
+ * indicate that a default value should be computed on demand. </li>
+ * <li>{@link #leftMargin} = 0 when
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+ * {@code false}; otherwise {@link #UNDEFINED}, to
+ * indicate that a default value should be computed on demand. </li>
+ * <li>{@link #bottomMargin} = 0 when
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+ * {@code false}; otherwise {@link #UNDEFINED}, to
+ * indicate that a default value should be computed on demand. </li>
+ * <li>{@link #rightMargin} = 0 when
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+ * {@code false}; otherwise {@link #UNDEFINED}, to
+ * indicate that a default value should be computed on demand. </li>
+ * <li>{@link #rowSpec}<code>.row</code> = {@link #UNDEFINED} </li>
+ * <li>{@link #rowSpec}<code>.rowSpan</code> = 1 </li>
+ * <li>{@link #rowSpec}<code>.alignment</code> = {@link #BASELINE} </li>
+ * <li>{@link #rowSpec}<code>.weight</code> = 0 </li>
+ * <li>{@link #columnSpec}<code>.column</code> = {@link #UNDEFINED} </li>
+ * <li>{@link #columnSpec}<code>.columnSpan</code> = 1 </li>
+ * <li>{@link #columnSpec}<code>.alignment</code> = {@link #START} </li>
+ * <li>{@link #columnSpec}<code>.weight</code> = 0 </li>
+ * </ul>
+ *
+ * See {@link GridLayout} for a more complete description of the conventions
+ * used by GridLayout in the interpretation of the properties of this class.
+ *
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_row
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_rowWeight
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_column
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_columnWeight
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity
+ */
+ public static class LayoutParams extends MarginLayoutParams {
+
+ // Default values
+
+ private static final int DEFAULT_WIDTH = WRAP_CONTENT;
+ private static final int DEFAULT_HEIGHT = WRAP_CONTENT;
+ private static final int DEFAULT_MARGIN = UNDEFINED;
+ private static final int DEFAULT_ROW = UNDEFINED;
+ private static final int DEFAULT_COLUMN = UNDEFINED;
+ private static final Interval DEFAULT_SPAN = new Interval(UNDEFINED, UNDEFINED + 1);
+ private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size();
+
+ // TypedArray indices
+
+ private static final int MARGIN = R.styleable.ViewGroup_MarginLayout_layout_margin;
+ private static final int LEFT_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginLeft;
+ private static final int TOP_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginTop;
+ private static final int RIGHT_MARGIN =
+ R.styleable.ViewGroup_MarginLayout_layout_marginRight;
+ private static final int BOTTOM_MARGIN =
+ R.styleable.ViewGroup_MarginLayout_layout_marginBottom;
+ private static final int COLUMN = R.styleable.GridLayout_Layout_layout_column;
+ private static final int COLUMN_SPAN = R.styleable.GridLayout_Layout_layout_columnSpan;
+ private static final int COLUMN_WEIGHT = R.styleable.GridLayout_Layout_layout_columnWeight;
+
+ private static final int ROW = R.styleable.GridLayout_Layout_layout_row;
+ private static final int ROW_SPAN = R.styleable.GridLayout_Layout_layout_rowSpan;
+ private static final int ROW_WEIGHT = R.styleable.GridLayout_Layout_layout_rowWeight;
+
+ private static final int GRAVITY = R.styleable.GridLayout_Layout_layout_gravity;
+
+ // Instance variables
+
+ /**
+ * The spec that defines the vertical characteristics of the cell group
+ * described by these layout parameters.
+ * If an assignment is made to this field after a measurement or layout operation
+ * has already taken place, a call to
+ * {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
+ * must be made to notify GridLayout of the change. GridLayout is normally able
+ * to detect when code fails to observe this rule, issue a warning and take steps to
+ * compensate for the omission. This facility is implemented on a best effort basis
+ * and should not be relied upon in production code - so it is best to include the above
+ * calls to remove the warnings as soon as it is practical.
+ */
+ public Spec rowSpec = Spec.UNDEFINED;
+
+ /**
+ * The spec that defines the horizontal characteristics of the cell group
+ * described by these layout parameters.
+ * If an assignment is made to this field after a measurement or layout operation
+ * has already taken place, a call to
+ * {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
+ * must be made to notify GridLayout of the change. GridLayout is normally able
+ * to detect when code fails to observe this rule, issue a warning and take steps to
+ * compensate for the omission. This facility is implemented on a best effort basis
+ * and should not be relied upon in production code - so it is best to include the above
+ * calls to remove the warnings as soon as it is practical.
+ */
+ public Spec columnSpec = Spec.UNDEFINED;
+
+ // Constructors
+
+ private LayoutParams(
+ int width, int height,
+ int left, int top, int right, int bottom,
+ Spec rowSpec, Spec columnSpec) {
+ super(width, height);
+ setMargins(left, top, right, bottom);
+ this.rowSpec = rowSpec;
+ this.columnSpec = columnSpec;
+ }
+
+ /**
+ * Constructs a new LayoutParams instance for this <code>rowSpec</code>
+ * and <code>columnSpec</code>. All other fields are initialized with
+ * default values as defined in {@link LayoutParams}.
+ *
+ * @param rowSpec the rowSpec
+ * @param columnSpec the columnSpec
+ */
+ public LayoutParams(Spec rowSpec, Spec columnSpec) {
+ this(DEFAULT_WIDTH, DEFAULT_HEIGHT,
+ DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN,
+ rowSpec, columnSpec);
+ }
+
+ /**
+ * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}.
+ */
+ public LayoutParams() {
+ this(Spec.UNDEFINED, Spec.UNDEFINED);
+ }
+
+ // Copying constructors
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams params) {
+ super(params);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams params) {
+ super(params);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height, margin values, row spec,
+ * and column spec of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ super(source);
+
+ this.rowSpec = source.rowSpec;
+ this.columnSpec = source.columnSpec;
+ }
+
+ // AttributeSet constructors
+
+ /**
+ * {@inheritDoc}
+ *
+ * Values not defined in the attribute set take the default values
+ * defined in {@link LayoutParams}.
+ */
+ public LayoutParams(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ reInitSuper(context, attrs);
+ init(context, attrs);
+ }
+
+ // Implementation
+
+ // Reinitialise the margins using a different default policy than MarginLayoutParams.
+ // Here we use the value UNDEFINED (as distinct from zero) to represent the undefined state
+ // so that a layout manager default can be accessed post set up. We need this as, at the
+ // point of installation, we do not know how many rows/cols there are and therefore
+ // which elements are positioned next to the container's trailing edges. We need to
+ // know this as margins around the container's boundary should have different
+ // defaults to those between peers.
+
+ // This method could be parametrized and moved into MarginLayout.
+ private void reInitSuper(Context context, AttributeSet attrs) {
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
+ try {
+ int margin = a.getDimensionPixelSize(MARGIN, DEFAULT_MARGIN);
+
+ this.leftMargin = a.getDimensionPixelSize(LEFT_MARGIN, margin);
+ this.topMargin = a.getDimensionPixelSize(TOP_MARGIN, margin);
+ this.rightMargin = a.getDimensionPixelSize(RIGHT_MARGIN, margin);
+ this.bottomMargin = a.getDimensionPixelSize(BOTTOM_MARGIN, margin);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout_Layout);
+ try {
+ int gravity = a.getInt(GRAVITY, Gravity.NO_GRAVITY);
+
+ int column = a.getInt(COLUMN, DEFAULT_COLUMN);
+ int colSpan = a.getInt(COLUMN_SPAN, DEFAULT_SPAN_SIZE);
+ float colWeight = a.getFloat(COLUMN_WEIGHT, Spec.DEFAULT_WEIGHT);
+ this.columnSpec = spec(column, colSpan, getAlignment(gravity, true), colWeight);
+
+ int row = a.getInt(ROW, DEFAULT_ROW);
+ int rowSpan = a.getInt(ROW_SPAN, DEFAULT_SPAN_SIZE);
+ float rowWeight = a.getFloat(ROW_WEIGHT, Spec.DEFAULT_WEIGHT);
+ this.rowSpec = spec(row, rowSpan, getAlignment(gravity, false), rowWeight);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ /**
+ * Describes how the child views are positioned. Default is {@code LEFT | BASELINE}.
+ * See {@link Gravity}.
+ *
+ * @param gravity the new gravity value
+ *
+ * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity
+ */
+ public void setGravity(int gravity) {
+ rowSpec = rowSpec.copyWriteAlignment(getAlignment(gravity, false));
+ columnSpec = columnSpec.copyWriteAlignment(getAlignment(gravity, true));
+ }
+
+ @Override
+ protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) {
+ this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH);
+ this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT);
+ }
+
+ final void setRowSpecSpan(Interval span) {
+ rowSpec = rowSpec.copyWriteSpan(span);
+ }
+
+ final void setColumnSpecSpan(Interval span) {
+ columnSpec = columnSpec.copyWriteSpan(span);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LayoutParams that = (LayoutParams) o;
+
+ if (!columnSpec.equals(that.columnSpec)) return false;
+ if (!rowSpec.equals(that.rowSpec)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = rowSpec.hashCode();
+ result = 31 * result + columnSpec.hashCode();
+ return result;
+ }
+ }
+
+ /*
+ In place of a HashMap from span to Int, use an array of key/value pairs - stored in Arcs.
+ Add the mutables completesCycle flag to avoid creating another hash table for detecting cycles.
+ */
+ final static class Arc {
+ public final Interval span;
+ public final MutableInt value;
+ public boolean valid = true;
+
+ public Arc(Interval span, MutableInt value) {
+ this.span = span;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return span + " " + (!valid ? "+>" : "->") + " " + value;
+ }
+ }
+
+ // A mutable Integer - used to avoid heap allocation during the layout operation
+
+ final static class MutableInt {
+ public int value;
+
+ public MutableInt() {
+ reset();
+ }
+
+ public MutableInt(int value) {
+ this.value = value;
+ }
+
+ public void reset() {
+ value = Integer.MIN_VALUE;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(value);
+ }
+ }
+
+ final static class Assoc<K, V> extends ArrayList<Pair<K, V>> {
+ private final Class<K> keyType;
+ private final Class<V> valueType;
+
+ private Assoc(Class<K> keyType, Class<V> valueType) {
+ this.keyType = keyType;
+ this.valueType = valueType;
+ }
+
+ public static <K, V> Assoc<K, V> of(Class<K> keyType, Class<V> valueType) {
+ return new Assoc<K, V>(keyType, valueType);
+ }
+
+ public void put(K key, V value) {
+ add(Pair.create(key, value));
+ }
+
+ @SuppressWarnings(value = "unchecked")
+ public PackedMap<K, V> pack() {
+ int N = size();
+ K[] keys = (K[]) Array.newInstance(keyType, N);
+ V[] values = (V[]) Array.newInstance(valueType, N);
+ for (int i = 0; i < N; i++) {
+ keys[i] = get(i).first;
+ values[i] = get(i).second;
+ }
+ return new PackedMap<K, V>(keys, values);
+ }
+ }
+
+ /*
+ This data structure is used in place of a Map where we have an index that refers to the order
+ in which each key/value pairs were added to the map. In this case we store keys and values
+ in arrays of a length that is equal to the number of unique keys. We also maintain an
+ array of indexes from insertion order to the compacted arrays of keys and values.
+
+ Note that behavior differs from that of a LinkedHashMap in that repeated entries
+ *do* get added multiples times. So the length of index is equals to the number of
+ items added.
+
+ This is useful in the GridLayout class where we can rely on the order of children not
+ changing during layout - to use integer-based lookup for our internal structures
+ rather than using (and storing) an implementation of Map<Key, ?>.
+ */
+ @SuppressWarnings(value = "unchecked")
+ final static class PackedMap<K, V> {
+ public final int[] index;
+ public final K[] keys;
+ public final V[] values;
+
+ private PackedMap(K[] keys, V[] values) {
+ this.index = createIndex(keys);
+
+ this.keys = compact(keys, index);
+ this.values = compact(values, index);
+ }
+
+ public V getValue(int i) {
+ return values[index[i]];
+ }
+
+ private static <K> int[] createIndex(K[] keys) {
+ int size = keys.length;
+ int[] result = new int[size];
+
+ Map<K, Integer> keyToIndex = new HashMap<K, Integer>();
+ for (int i = 0; i < size; i++) {
+ K key = keys[i];
+ Integer index = keyToIndex.get(key);
+ if (index == null) {
+ index = keyToIndex.size();
+ keyToIndex.put(key, index);
+ }
+ result[i] = index;
+ }
+ return result;
+ }
+
+ /*
+ Create a compact array of keys or values using the supplied index.
+ */
+ private static <K> K[] compact(K[] a, int[] index) {
+ int size = a.length;
+ Class<?> componentType = a.getClass().getComponentType();
+ K[] result = (K[]) Array.newInstance(componentType, max2(index, -1) + 1);
+
+ // this overwrite duplicates, retaining the last equivalent entry
+ for (int i = 0; i < size; i++) {
+ result[index[i]] = a[i];
+ }
+ return result;
+ }
+ }
+
+ /*
+ For each group (with a given alignment) we need to store the amount of space required
+ before the alignment point and the amount of space required after it. One side of this
+ calculation is always 0 for START and END alignments but we don't make use of this.
+ For CENTER and BASELINE alignments both sides are needed and in the BASELINE case no
+ simple optimisations are possible.
+
+ The general algorithm therefore is to create a Map (actually a PackedMap) from
+ group to Bounds and to loop through all Views in the group taking the maximum
+ of the values for each View.
+ */
+ static class Bounds {
+ public int before;
+ public int after;
+ public int flexibility; // we're flexible iff all included specs are flexible
+
+ private Bounds() {
+ reset();
+ }
+
+ protected void reset() {
+ before = Integer.MIN_VALUE;
+ after = Integer.MIN_VALUE;
+ flexibility = CAN_STRETCH; // from the above, we're flexible when empty
+ }
+
+ protected void include(int before, int after) {
+ this.before = max(this.before, before);
+ this.after = max(this.after, after);
+ }
+
+ protected int size(boolean min) {
+ if (!min) {
+ if (canStretch(flexibility)) {
+ return MAX_SIZE;
+ }
+ }
+ return before + after;
+ }
+
+ protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean horizontal) {
+ return before - a.getAlignmentValue(c, size, gl.getLayoutMode());
+ }
+
+ protected final void include(GridLayout gl, View c, Spec spec, Axis axis, int size) {
+ this.flexibility &= spec.getFlexibility();
+ boolean horizontal = axis.horizontal;
+ Alignment alignment = spec.getAbsoluteAlignment(axis.horizontal);
+ // todo test this works correctly when the returned value is UNDEFINED
+ int before = alignment.getAlignmentValue(c, size, gl.getLayoutMode());
+ include(before, size - before);
+ }
+
+ @Override
+ public String toString() {
+ return "Bounds{" +
+ "before=" + before +
+ ", after=" + after +
+ '}';
+ }
+ }
+
+ /**
+ * An Interval represents a contiguous range of values that lie between
+ * the interval's {@link #min} and {@link #max} values.
+ * <p>
+ * Intervals are immutable so may be passed as values and used as keys in hash tables.
+ * It is not necessary to have multiple instances of Intervals which have the same
+ * {@link #min} and {@link #max} values.
+ * <p>
+ * Intervals are often written as {@code [min, max]} and represent the set of values
+ * {@code x} such that {@code min <= x < max}.
+ */
+ final static class Interval {
+ /**
+ * The minimum value.
+ */
+ public final int min;
+
+ /**
+ * The maximum value.
+ */
+ public final int max;
+
+ /**
+ * Construct a new Interval, {@code interval}, where:
+ * <ul>
+ * <li> {@code interval.min = min} </li>
+ * <li> {@code interval.max = max} </li>
+ * </ul>
+ *
+ * @param min the minimum value.
+ * @param max the maximum value.
+ */
+ public Interval(int min, int max) {
+ this.min = min;
+ this.max = max;
+ }
+
+ int size() {
+ return max - min;
+ }
+
+ Interval inverse() {
+ return new Interval(max, min);
+ }
+
+ /**
+ * Returns {@code true} if the {@link #getClass class},
+ * {@link #min} and {@link #max} properties of this Interval and the
+ * supplied parameter are pairwise equal; {@code false} otherwise.
+ *
+ * @param that the object to compare this interval with
+ *
+ * @return {@code true} if the specified object is equal to this
+ * {@code Interval}, {@code false} otherwise.
+ */
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (that == null || getClass() != that.getClass()) {
+ return false;
+ }
+
+ Interval interval = (Interval) that;
+
+ if (max != interval.max) {
+ return false;
+ }
+ //noinspection RedundantIfStatement
+ if (min != interval.min) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = min;
+ result = 31 * result + max;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + min + ", " + max + "]";
+ }
+ }
+
+ /**
+ * A Spec defines the horizontal or vertical characteristics of a group of
+ * cells. Each spec. defines the <em>grid indices</em> and <em>alignment</em>
+ * along the appropriate axis.
+ * <p>
+ * The <em>grid indices</em> are the leading and trailing edges of this cell group.
+ * See {@link GridLayout} for a description of the conventions used by GridLayout
+ * for grid indices.
+ * <p>
+ * The <em>alignment</em> property specifies how cells should be aligned in this group.
+ * For row groups, this specifies the vertical alignment.
+ * For column groups, this specifies the horizontal alignment.
+ * <p>
+ * Use the following static methods to create specs:
+ * <ul>
+ * <li>{@link #spec(int)}</li>
+ * <li>{@link #spec(int, int)}</li>
+ * <li>{@link #spec(int, Alignment)}</li>
+ * <li>{@link #spec(int, int, Alignment)}</li>
+ * <li>{@link #spec(int, float)}</li>
+ * <li>{@link #spec(int, int, float)}</li>
+ * <li>{@link #spec(int, Alignment, float)}</li>
+ * <li>{@link #spec(int, int, Alignment, float)}</li>
+ * </ul>
+ *
+ */
+ public static class Spec {
+ static final Spec UNDEFINED = spec(GridLayout.UNDEFINED);
+ static final float DEFAULT_WEIGHT = 0;
+
+ final boolean startDefined;
+ final Interval span;
+ final Alignment alignment;
+ final float weight;
+
+ private Spec(boolean startDefined, Interval span, Alignment alignment, float weight) {
+ this.startDefined = startDefined;
+ this.span = span;
+ this.alignment = alignment;
+ this.weight = weight;
+ }
+
+ private Spec(boolean startDefined, int start, int size, Alignment alignment, float weight) {
+ this(startDefined, new Interval(start, start + size), alignment, weight);
+ }
+
+ private Alignment getAbsoluteAlignment(boolean horizontal) {
+ if (alignment != UNDEFINED_ALIGNMENT) {
+ return alignment;
+ }
+ if (weight == 0f) {
+ return horizontal ? START : BASELINE;
+ }
+ return FILL;
+ }
+
+ final Spec copyWriteSpan(Interval span) {
+ return new Spec(startDefined, span, alignment, weight);
+ }
+
+ final Spec copyWriteAlignment(Alignment alignment) {
+ return new Spec(startDefined, span, alignment, weight);
+ }
+
+ final int getFlexibility() {
+ return (alignment == UNDEFINED_ALIGNMENT && weight == 0) ? INFLEXIBLE : CAN_STRETCH;
+ }
+
+ /**
+ * Returns {@code true} if the {@code class}, {@code alignment} and {@code span}
+ * properties of this Spec and the supplied parameter are pairwise equal,
+ * {@code false} otherwise.
+ *
+ * @param that the object to compare this spec with
+ *
+ * @return {@code true} if the specified object is equal to this
+ * {@code Spec}; {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (that == null || getClass() != that.getClass()) {
+ return false;
+ }
+
+ Spec spec = (Spec) that;
+
+ if (!alignment.equals(spec.alignment)) {
+ return false;
+ }
+ //noinspection RedundantIfStatement
+ if (!span.equals(spec.span)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = span.hashCode();
+ result = 31 * result + alignment.hashCode();
+ return result;
+ }
+ }
+
+ /**
+ * Return a Spec, {@code spec}, where:
+ * <ul>
+ * <li> {@code spec.span = [start, start + size]} </li>
+ * <li> {@code spec.alignment = alignment} </li>
+ * <li> {@code spec.weight = weight} </li>
+ * </ul>
+ * <p>
+ * To leave the start index undefined, use the value {@link #UNDEFINED}.
+ *
+ * @param start the start
+ * @param size the size
+ * @param alignment the alignment
+ * @param weight the weight
+ */
+ public static Spec spec(int start, int size, Alignment alignment, float weight) {
+ return new Spec(start != UNDEFINED, start, size, alignment, weight);
+ }
+
+ /**
+ * Equivalent to: {@code spec(start, 1, alignment, weight)}.
+ *
+ * @param start the start
+ * @param alignment the alignment
+ * @param weight the weight
+ */
+ public static Spec spec(int start, Alignment alignment, float weight) {
+ return spec(start, 1, alignment, weight);
+ }
+
+ /**
+ * Equivalent to: {@code spec(start, 1, default_alignment, weight)} -
+ * where {@code default_alignment} is specified in
+ * {@link android.widget.GridLayout.LayoutParams}.
+ *
+ * @param start the start
+ * @param size the size
+ * @param weight the weight
+ */
+ public static Spec spec(int start, int size, float weight) {
+ return spec(start, size, UNDEFINED_ALIGNMENT, weight);
+ }
+
+ /**
+ * Equivalent to: {@code spec(start, 1, weight)}.
+ *
+ * @param start the start
+ * @param weight the weight
+ */
+ public static Spec spec(int start, float weight) {
+ return spec(start, 1, weight);
+ }
+
+ /**
+ * Equivalent to: {@code spec(start, size, alignment, 0f)}.
+ *
+ * @param start the start
+ * @param size the size
+ * @param alignment the alignment
+ */
+ public static Spec spec(int start, int size, Alignment alignment) {
+ return spec(start, size, alignment, Spec.DEFAULT_WEIGHT);
+ }
+
+ /**
+ * Return a Spec, {@code spec}, where:
+ * <ul>
+ * <li> {@code spec.span = [start, start + 1]} </li>
+ * <li> {@code spec.alignment = alignment} </li>
+ * </ul>
+ * <p>
+ * To leave the start index undefined, use the value {@link #UNDEFINED}.
+ *
+ * @param start the start index
+ * @param alignment the alignment
+ *
+ * @see #spec(int, int, Alignment)
+ */
+ public static Spec spec(int start, Alignment alignment) {
+ return spec(start, 1, alignment);
+ }
+
+ /**
+ * Return a Spec, {@code spec}, where:
+ * <ul>
+ * <li> {@code spec.span = [start, start + size]} </li>
+ * </ul>
+ * <p>
+ * To leave the start index undefined, use the value {@link #UNDEFINED}.
+ *
+ * @param start the start
+ * @param size the size
+ *
+ * @see #spec(int, Alignment)
+ */
+ public static Spec spec(int start, int size) {
+ return spec(start, size, UNDEFINED_ALIGNMENT);
+ }
+
+ /**
+ * Return a Spec, {@code spec}, where:
+ * <ul>
+ * <li> {@code spec.span = [start, start + 1]} </li>
+ * </ul>
+ * <p>
+ * To leave the start index undefined, use the value {@link #UNDEFINED}.
+ *
+ * @param start the start index
+ *
+ * @see #spec(int, int)
+ */
+ public static Spec spec(int start) {
+ return spec(start, 1);
+ }
+
+ /**
+ * Alignments specify where a view should be placed within a cell group and
+ * what size it should be.
+ * <p>
+ * The {@link LayoutParams} class contains a {@link LayoutParams#rowSpec rowSpec}
+ * and a {@link LayoutParams#columnSpec columnSpec} each of which contains an
+ * {@code alignment}. Overall placement of the view in the cell
+ * group is specified by the two alignments which act along each axis independently.
+ * <p>
+ * The GridLayout class defines the most common alignments used in general layout:
+ * {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #START},
+ * {@link #END}, {@link #CENTER}, {@link #BASELINE} and {@link #FILL}.
+ */
+ /*
+ * An Alignment implementation must define {@link #getAlignmentValue(View, int, int)},
+ * to return the appropriate value for the type of alignment being defined.
+ * The enclosing algorithms position the children
+ * so that the locations defined by the alignment values
+ * are the same for all of the views in a group.
+ * <p>
+ */
+ public static abstract class Alignment {
+ Alignment() {
+ }
+
+ abstract int getGravityOffset(View view, int cellDelta);
+
+ /**
+ * Returns an alignment value. In the case of vertical alignments the value
+ * returned should indicate the distance from the top of the view to the
+ * alignment location.
+ * For horizontal alignments measurement is made from the left edge of the component.
+ *
+ * @param view the view to which this alignment should be applied
+ * @param viewSize the measured size of the view
+ * @param mode the basis of alignment: CLIP or OPTICAL
+ * @return the alignment value
+ */
+ abstract int getAlignmentValue(View view, int viewSize, int mode);
+
+ /**
+ * Returns the size of the view specified by this alignment.
+ * In the case of vertical alignments this method should return a height; for
+ * horizontal alignments this method should return the width.
+ * <p>
+ * The default implementation returns {@code viewSize}.
+ *
+ * @param view the view to which this alignment should be applied
+ * @param viewSize the measured size of the view
+ * @param cellSize the size of the cell into which this view will be placed
+ * @return the aligned size
+ */
+ int getSizeInCell(View view, int viewSize, int cellSize) {
+ return viewSize;
+ }
+
+ Bounds getBounds() {
+ return new Bounds();
+ }
+ }
+
+ static final Alignment UNDEFINED_ALIGNMENT = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return UNDEFINED;
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return UNDEFINED;
+ }
+ };
+
+ /**
+ * Indicates that a view should be aligned with the <em>start</em>
+ * edges of the other views in its cell group.
+ */
+ private static final Alignment LEADING = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return 0;
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return 0;
+ }
+ };
+
+ /**
+ * Indicates that a view should be aligned with the <em>end</em>
+ * edges of the other views in its cell group.
+ */
+ private static final Alignment TRAILING = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return cellDelta;
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return viewSize;
+ }
+ };
+
+ /**
+ * Indicates that a view should be aligned with the <em>top</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment TOP = LEADING;
+
+ /**
+ * Indicates that a view should be aligned with the <em>bottom</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment BOTTOM = TRAILING;
+
+ /**
+ * Indicates that a view should be aligned with the <em>start</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment START = LEADING;
+
+ /**
+ * Indicates that a view should be aligned with the <em>end</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment END = TRAILING;
+
+ private static Alignment createSwitchingAlignment(final Alignment ltr, final Alignment rtl) {
+ return new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return (!view.isLayoutRtl() ? ltr : rtl).getGravityOffset(view, cellDelta);
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return (!view.isLayoutRtl() ? ltr : rtl).getAlignmentValue(view, viewSize, mode);
+ }
+ };
+ }
+
+ /**
+ * Indicates that a view should be aligned with the <em>left</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment LEFT = createSwitchingAlignment(START, END);
+
+ /**
+ * Indicates that a view should be aligned with the <em>right</em>
+ * edges of the other views in its cell group.
+ */
+ public static final Alignment RIGHT = createSwitchingAlignment(END, START);
+
+ /**
+ * Indicates that a view should be <em>centered</em> with the other views in its cell group.
+ * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and {@link
+ * LayoutParams#columnSpec columnSpecs}.
+ */
+ public static final Alignment CENTER = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return cellDelta >> 1;
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return viewSize >> 1;
+ }
+ };
+
+ /**
+ * Indicates that a view should be aligned with the <em>baselines</em>
+ * of the other views in its cell group.
+ * This constant may only be used as an alignment in {@link LayoutParams#rowSpec rowSpecs}.
+ *
+ * @see View#getBaseline()
+ */
+ public static final Alignment BASELINE = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return 0; // baseline gravity is top
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ if (view.getVisibility() == GONE) {
+ return 0;
+ }
+ int baseline = view.getBaseline();
+ return baseline == -1 ? UNDEFINED : baseline;
+ }
+
+ @Override
+ public Bounds getBounds() {
+ return new Bounds() {
+ /*
+ In a baseline aligned row in which some components define a baseline
+ and some don't, we need a third variable to properly account for all
+ the sizes. This tracks the maximum size of all the components -
+ including those that don't define a baseline.
+ */
+ private int size;
+
+ @Override
+ protected void reset() {
+ super.reset();
+ size = Integer.MIN_VALUE;
+ }
+
+ @Override
+ protected void include(int before, int after) {
+ super.include(before, after);
+ size = max(size, before + after);
+ }
+
+ @Override
+ protected int size(boolean min) {
+ return max(super.size(min), size);
+ }
+
+ @Override
+ protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean hrz) {
+ return max(0, super.getOffset(gl, c, a, size, hrz));
+ }
+ };
+ }
+ };
+
+ /**
+ * Indicates that a view should expanded to fit the boundaries of its cell group.
+ * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and
+ * {@link LayoutParams#columnSpec columnSpecs}.
+ */
+ public static final Alignment FILL = new Alignment() {
+ @Override
+ int getGravityOffset(View view, int cellDelta) {
+ return 0;
+ }
+
+ @Override
+ public int getAlignmentValue(View view, int viewSize, int mode) {
+ return UNDEFINED;
+ }
+
+ @Override
+ public int getSizeInCell(View view, int viewSize, int cellSize) {
+ return cellSize;
+ }
+ };
+
+ static boolean canStretch(int flexibility) {
+ return (flexibility & CAN_STRETCH) != 0;
+ }
+
+ private static final int INFLEXIBLE = 0;
+ private static final int CAN_STRETCH = 2;
+}
diff --git a/android/widget/GridView.java b/android/widget/GridView.java
new file mode 100644
index 0000000..fcb44af
--- /dev/null
+++ b/android/widget/GridView.java
@@ -0,0 +1,2424 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewRootImpl;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.animation.GridLayoutAnimationController;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * A view that shows items in two-dimensional scrolling grid. The items in the
+ * grid come from the {@link ListAdapter} associated with this view.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid
+ * View</a> guide.</p>
+ *
+ * @attr ref android.R.styleable#GridView_horizontalSpacing
+ * @attr ref android.R.styleable#GridView_verticalSpacing
+ * @attr ref android.R.styleable#GridView_stretchMode
+ * @attr ref android.R.styleable#GridView_columnWidth
+ * @attr ref android.R.styleable#GridView_numColumns
+ * @attr ref android.R.styleable#GridView_gravity
+ */
+@RemoteView
+public class GridView extends AbsListView {
+ /** @hide */
+ @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StretchMode {}
+
+ /**
+ * Disables stretching.
+ *
+ * @see #setStretchMode(int)
+ */
+ public static final int NO_STRETCH = 0;
+ /**
+ * Stretches the spacing between columns.
+ *
+ * @see #setStretchMode(int)
+ */
+ public static final int STRETCH_SPACING = 1;
+ /**
+ * Stretches columns.
+ *
+ * @see #setStretchMode(int)
+ */
+ public static final int STRETCH_COLUMN_WIDTH = 2;
+ /**
+ * Stretches the spacing between columns. The spacing is uniform.
+ *
+ * @see #setStretchMode(int)
+ */
+ public static final int STRETCH_SPACING_UNIFORM = 3;
+
+ /**
+ * Creates as many columns as can fit on screen.
+ *
+ * @see #setNumColumns(int)
+ */
+ 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.START;
+
+ private final Rect mTempRect = new Rect();
+
+ public GridView(Context context) {
+ this(context, null);
+ }
+
+ public GridView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.gridViewStyle);
+ }
+
+ public GridView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.GridView, defStyleAttr, defStyleRes);
+
+ int hSpacing = a.getDimensionPixelOffset(
+ R.styleable.GridView_horizontalSpacing, 0);
+ setHorizontalSpacing(hSpacing);
+
+ int vSpacing = a.getDimensionPixelOffset(
+ R.styleable.GridView_verticalSpacing, 0);
+ setVerticalSpacing(vSpacing);
+
+ int index = a.getInt(R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH);
+ if (index >= 0) {
+ setStretchMode(index);
+ }
+
+ int columnWidth = a.getDimensionPixelOffset(R.styleable.GridView_columnWidth, -1);
+ if (columnWidth > 0) {
+ setColumnWidth(columnWidth);
+ }
+
+ int numColumns = a.getInt(R.styleable.GridView_numColumns, 1);
+ setNumColumns(numColumns);
+
+ index = a.getInt(R.styleable.GridView_gravity, -1);
+ if (index >= 0) {
+ setGravity(index);
+ }
+
+ a.recycle();
+ }
+
+ @Override
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+ * through the specified intent.
+ * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+ public void setRemoteViewsAdapter(Intent intent) {
+ super.setRemoteViewsAdapter(intent);
+ }
+
+ /**
+ * Sets the data behind this GridView.
+ *
+ * @param adapter the adapter providing the grid's data
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (mAdapter != null && mDataSetObserver != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ resetList();
+ mRecycler.clear();
+ mAdapter = adapter;
+
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+
+ // AbsListView#setAdapter will update choice mode states.
+ super.setAdapter(adapter);
+
+ 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) {
+ int paddingTop = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ paddingTop = getListPaddingTop();
+ }
+ final int startOffset = count > 0 ?
+ getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop;
+ int position = mFirstPosition + count;
+ if (mStackFromBottom) {
+ position += numColumns - 1;
+ }
+ fillDown(position, startOffset);
+ correctTooHigh(numColumns, verticalSpacing, getChildCount());
+ } else {
+ int paddingBottom = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ paddingBottom = getListPaddingBottom();
+ }
+ final int startOffset = count > 0 ?
+ getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom;
+ 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;
+
+ int end = (mBottom - mTop);
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ end -= mListPadding.bottom;
+ }
+
+ while (nextTop < end && pos < mItemCount) {
+ View temp = makeRow(pos, nextTop, true);
+ if (temp != null) {
+ selectedView = temp;
+ }
+
+ // mReferenceView will change with each call to makeRow()
+ // do not cache in a local variable outside of this loop
+ nextTop = mReferenceView.getBottom() + mVerticalSpacing;
+
+ pos += mNumColumns;
+ }
+
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+ return selectedView;
+ }
+
+ private View makeRow(int startPos, int y, boolean flow) {
+ final int columnWidth = mColumnWidth;
+ final int horizontalSpacing = mHorizontalSpacing;
+
+ final boolean isLayoutRtl = isLayoutRtl();
+
+ int last;
+ int nextLeft;
+
+ if (isLayoutRtl) {
+ nextLeft = getWidth() - mListPadding.right - columnWidth -
+ ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
+ } else {
+ nextLeft = mListPadding.left +
+ ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
+ }
+
+ if (!mStackFromBottom) {
+ last = Math.min(startPos + mNumColumns, mItemCount);
+ } else {
+ last = startPos + 1;
+ startPos = Math.max(0, startPos - mNumColumns + 1);
+
+ if (last - startPos < mNumColumns) {
+ final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing);
+ nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft;
+ }
+ }
+
+ View selectedView = null;
+
+ final boolean hasFocus = shouldShowSelector();
+ final boolean inClick = touchModeDrawsInPressedState();
+ final int selectedPosition = mSelectedPosition;
+
+ View child = null;
+ final int nextChildDir = isLayoutRtl ? -1 : +1;
+ 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;
+ child = makeAndAddView(pos, y, flow, nextLeft, selected, where);
+
+ nextLeft += nextChildDir * columnWidth;
+ if (pos < last - 1) {
+ nextLeft += nextChildDir * horizontalSpacing;
+ }
+
+ if (selected && (hasFocus || inClick)) {
+ selectedView = child;
+ }
+ }
+
+ mReferenceView = 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;
+
+ int end = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ 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);
+ }
+
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 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 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;
+ // We didn't have anything to layout, bail out
+ if (referenceView == null) {
+ return null;
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will
+ * scroll such that the indicated position is displayed.
+ * @param position Scroll to this adapter position.
+ */
+ @android.view.RemotableViewMethod
+ public void smoothScrollToPosition(int position) {
+ super.smoothScrollToPosition(position);
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position offset. The view will
+ * scroll such that the indicated position is displayed.
+ * @param offset The amount to offset from the adapter position to scroll to.
+ */
+ @android.view.RemotableViewMethod
+ public void smoothScrollByOffset(int offset) {
+ super.smoothScrollByOffset(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 boolean determineColumns(int availableSpace) {
+ final int requestedHorizontalSpacing = mRequestedHorizontalSpacing;
+ final int stretchMode = mStretchMode;
+ final int requestedColumnWidth = mRequestedColumnWidth;
+ boolean didNotInitiallyFit = false;
+
+ 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);
+
+ if (spaceLeftOver < 0) {
+ didNotInitiallyFit = true;
+ }
+
+ 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;
+
+ case STRETCH_SPACING_UNIFORM:
+ // Stretch the spacing between columns
+ mColumnWidth = requestedColumnWidth;
+ if (mNumColumns > 1) {
+ mHorizontalSpacing = requestedHorizontalSpacing
+ + spaceLeftOver / (mNumColumns + 1);
+ } else {
+ mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
+ }
+ break;
+ }
+
+ break;
+ }
+ return didNotInitiallyFit;
+ }
+
+ @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;
+ boolean didNotInitiallyFit = determineColumns(childWidth);
+
+ int childHeight = 0;
+ int childState = 0;
+
+ mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+ final int count = mItemCount;
+ if (count > 0) {
+ final View child = obtainView(0, mIsScrap);
+
+ AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
+ if (p == null) {
+ p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+ child.setLayoutParams(p);
+ }
+ p.viewType = mAdapter.getItemViewType(0);
+ p.isEnabled = mAdapter.isEnabled(0);
+ p.forceAdd = true;
+
+ int childHeightSpec = getChildMeasureSpec(
+ MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
+ MeasureSpec.UNSPECIFIED), 0, p.height);
+ int childWidthSpec = getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
+ child.measure(childWidthSpec, childHeightSpec);
+
+ childHeight = child.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+ if (mRecycler.shouldRecycleViewType(p.viewType)) {
+ mRecycler.addScrapView(child, -1);
+ }
+ }
+
+ 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;
+ }
+
+ if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) {
+ int ourSize = (mRequestedNumColumns*mColumnWidth)
+ + ((mRequestedNumColumns-1)*mHorizontalSpacing)
+ + mListPadding.left + mListPadding.right;
+ if (ourSize > widthSize || didNotInitiallyFit) {
+ widthSize |= MEASURED_STATE_TOO_SMALL;
+ }
+ }
+
+ 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);
+
+ AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
+ View accessibilityFocusLayoutRestoreView = null;
+ int accessibilityFocusPosition = INVALID_POSITION;
+
+ // Remember which child, if any, had accessibility focus. This must
+ // occur before recycling any views, since that will clear
+ // accessibility focus.
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
+ if (focusHost != null) {
+ final View focusChild = getAccessibilityFocusedChild(focusHost);
+ if (focusChild != null) {
+ if (!dataChanged || focusChild.hasTransientState()
+ || mAdapterHasStableIds) {
+ // The views won't be changing, so try to maintain
+ // focus on the current host and virtual view.
+ accessibilityFocusLayoutRestoreView = focusHost;
+ accessibilityFocusLayoutRestoreNode = viewRootImpl
+ .getAccessibilityFocusedVirtualView();
+ }
+
+ // Try to maintain focus at the same position.
+ accessibilityFocusPosition = getPositionForView(focusChild);
+ }
+ }
+ }
+
+ // 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), firstPosition+i);
+ }
+ } else {
+ recycleBin.fillActiveViews(childCount, firstPosition);
+ }
+
+ // Clear out old views
+ detachAllViewsFromParent();
+ recycleBin.removeSkippedScrap();
+
+ 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(mAdapter == null || isInTouchMode() ?
+ INVALID_POSITION : 0);
+ sel = fillFromTop(childrenTop);
+ } else {
+ final int last = mItemCount - 1;
+ setSelectedPositionInt(mAdapter == null || isInTouchMode() ?
+ INVALID_POSITION : 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(INVALID_POSITION, sel);
+ mSelectedTop = sel.getTop();
+ } else {
+ final boolean inTouchMode = mTouchMode > TOUCH_MODE_DOWN
+ && mTouchMode < TOUCH_MODE_SCROLL;
+ if (inTouchMode) {
+ // If the user's finger is down, select the motion position.
+ final View child = getChildAt(mMotionPosition - mFirstPosition);
+ if (child != null) {
+ positionSelector(mMotionPosition, child);
+ }
+ } else if (mSelectedPosition != INVALID_POSITION) {
+ // If we had previously positioned the selector somewhere,
+ // put it back there. It might not match up with the data,
+ // but it's transitioning out so it's not a big deal.
+ final View child = getChildAt(mSelectorPosition - mFirstPosition);
+ if (child != null) {
+ positionSelector(mSelectorPosition, child);
+ }
+ } else {
+ // Otherwise, clear selection.
+ mSelectedTop = 0;
+ mSelectorRect.setEmpty();
+ }
+ }
+
+ // Attempt to restore accessibility focus, if necessary.
+ if (viewRootImpl != null) {
+ final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
+ if (newAccessibilityFocusedView == null) {
+ if (accessibilityFocusLayoutRestoreView != null
+ && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
+ final AccessibilityNodeProvider provider =
+ accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
+ if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
+ final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
+ accessibilityFocusLayoutRestoreNode.getSourceNodeId());
+ provider.performAction(virtualViewId,
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ } else {
+ accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
+ }
+ } else if (accessibilityFocusPosition != INVALID_POSITION) {
+ // Bound the position within the visible children.
+ final int position = MathUtils.constrain(
+ accessibilityFocusPosition - mFirstPosition, 0,
+ getChildCount() - 1);
+ final View restoreView = getChildAt(position);
+ if (restoreView != null) {
+ restoreView.requestAccessibilityFocus();
+ }
+ }
+ }
+ }
+
+ mLayoutMode = LAYOUT_NORMAL;
+ mDataChanged = false;
+ if (mPositionScrollAfterLayout != null) {
+ post(mPositionScrollAfterLayout);
+ mPositionScrollAfterLayout = null;
+ }
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+
+ updateScrollIndicators();
+
+ if (mItemCount > 0) {
+ checkSelectionChanged();
+ }
+
+ invokeOnItemScrollListener();
+ } finally {
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = false;
+ }
+ }
+ }
+
+
+ /**
+ * Obtains the view and adds 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 {@code true} to align top edge to y, {@code false} to align
+ * bottom edge to y
+ * @param childrenLeft left edge where children should be positioned
+ * @param selected {@code true} if the position is selected, {@code false}
+ * otherwise
+ * @param where position at which 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) {
+ if (!mDataChanged) {
+ // Try to use an existing view for this position
+ final View activeView = mRecycler.getActiveView(position);
+ if (activeView != null) {
+ // Found it -- we're using an existing child
+ // This just needs to be positioned
+ setupChild(activeView, position, y, flow, childrenLeft, selected, true, where);
+ return activeView;
+ }
+ }
+
+ // Make a new view for this position, or convert an unused view if
+ // possible.
+ final View child = obtainView(position, mIsScrap);
+
+ // This needs to be positioned and measured.
+ setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where);
+
+ return child;
+ }
+
+ /**
+ * Adds 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 {@code true} to align top edge to y, {@code false} to
+ * align bottom edge to y
+ * @param childrenLeft left edge where children should be positioned
+ * @param selected {@code true} if the position is selected, {@code false}
+ * otherwise
+ * @param isAttachedToWindow {@code true} if the view is already attached
+ * to the window, e.g. whether it was reused, or
+ * {@code false} otherwise
+ * @param where position at which to add new item in the list
+ *
+ */
+ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
+ boolean selected, boolean isAttachedToWindow, int where) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem");
+
+ boolean isSelected = selected && shouldShowSelector();
+ final boolean updateChildSelected = isSelected != child.isSelected();
+ final int mode = mTouchMode;
+ final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
+ && mMotionPosition == position;
+ final boolean updateChildPressed = isPressed != child.isPressed();
+ final boolean needToMeasure = !isAttachedToWindow || 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 = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+ }
+ p.viewType = mAdapter.getItemViewType(position);
+ p.isEnabled = mAdapter.isEnabled(position);
+
+ // Set up view state before attaching the view, since we may need to
+ // rely on the jumpDrawablesToCurrentState() call that occurs as part
+ // of view attachment.
+ if (updateChildSelected) {
+ child.setSelected(isSelected);
+ if (isSelected) {
+ requestFocus();
+ }
+ }
+
+ if (updateChildPressed) {
+ child.setPressed(isPressed);
+ }
+
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(mCheckStates.get(position));
+ } else if (getContext().getApplicationInfo().targetSdkVersion
+ >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ child.setActivated(mCheckStates.get(position));
+ }
+ }
+
+ if (isAttachedToWindow && !p.forceAdd) {
+ attachViewToParent(child, where, p);
+
+ // If the view isn't attached, or if it's attached but for a different
+ // position, then jump the drawables.
+ if (!isAttachedToWindow
+ || (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
+ != position) {
+ child.jumpDrawablesToCurrentState();
+ }
+ } else {
+ p.forceAdd = false;
+ addViewInLayout(child, where, p, true);
+ }
+
+ 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 = flowDown ? y : y - h;
+
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+ switch (absoluteGravity & 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.isDrawingCacheEnabled()) {
+ child.setDrawingCacheEnabled(true);
+ }
+
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ /**
+ * 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;
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+ requestLayout();
+ }
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position the position of the new selection
+ */
+ @Override
+ void setSelectionInt(int position) {
+ int previousSelectedPosition = mNextSelectedPosition;
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ setNextSelectedPositionInt(position);
+ layoutChildren();
+
+ final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition :
+ mNextSelectedPosition;
+ final int previous = mStackFromBottom ? mItemCount - 1
+ - previousSelectedPosition : previousSelectedPosition;
+
+ final int nextRow = next / mNumColumns;
+ final int previousRow = previous / mNumColumns;
+
+ if (nextRow != previousRow) {
+ awakenScrollBars();
+ }
+
+ }
+
+ @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 (KeyEvent.isConfirmKey(keyCode)
+ && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
+ handled = resurrectSelectionIfNeeded();
+ if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
+ keyPressed();
+ handled = true;
+ }
+ }
+
+ if (!handled && action != KeyEvent.ACTION_UP) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_UP:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_END:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ // TODO: Sometimes it is useful to be able to TAB through the items in
+ // a GridView sequentially. Unfortunately this can create an
+ // asymmetry in TAB navigation order unless the list selection
+ // always reverts to the top or bottom when receiving TAB focus from
+ // another widget.
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded()
+ || sequenceScroll(FOCUS_FORWARD);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = resurrectSelectionIfNeeded()
+ || sequenceScroll(FOCUS_BACKWARD);
+ }
+ break;
+ }
+ }
+
+ if (handled) {
+ return true;
+ }
+
+ if (sendToTextFilter(keyCode, count, event)) {
+ return true;
+ }
+
+ 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());
+ } else if (direction == FOCUS_DOWN) {
+ nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount());
+ }
+
+ if (nextPage >= 0) {
+ setSelectionInt(nextPage);
+ invokeOnItemScrollListener();
+ awakenScrollBars();
+ 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);
+ invokeOnItemScrollListener();
+ moved = true;
+ } else if (direction == FOCUS_DOWN) {
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ setSelectionInt(mItemCount - 1);
+ invokeOnItemScrollListener();
+ moved = true;
+ }
+
+ if (moved) {
+ awakenScrollBars();
+ }
+
+ 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;
+ }
+
+ final boolean isLayoutRtl = isLayoutRtl();
+ if (selectedPosition > startOfRowPos && ((direction == FOCUS_LEFT && !isLayoutRtl) ||
+ (direction == FOCUS_RIGHT && isLayoutRtl))) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(Math.max(0, selectedPosition - 1));
+ moved = true;
+ } else if (selectedPosition < endOfRowPos && ((direction == FOCUS_LEFT && isLayoutRtl) ||
+ (direction == FOCUS_RIGHT && !isLayoutRtl))) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1));
+ moved = true;
+ }
+
+ if (moved) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ invokeOnItemScrollListener();
+ }
+
+ if (moved) {
+ awakenScrollBars();
+ }
+
+ return moved;
+ }
+
+ /**
+ * Goes to the next or previous item according to the order set by the
+ * adapter.
+ */
+ boolean sequenceScroll(int direction) {
+ int selectedPosition = mSelectedPosition;
+ int numColumns = mNumColumns;
+ int count = mItemCount;
+
+ int startOfRow;
+ int endOfRow;
+ if (!mStackFromBottom) {
+ startOfRow = (selectedPosition / numColumns) * numColumns;
+ endOfRow = Math.min(startOfRow + numColumns - 1, count - 1);
+ } else {
+ int invertedSelection = count - 1 - selectedPosition;
+ endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns;
+ startOfRow = Math.max(0, endOfRow - numColumns + 1);
+ }
+
+ boolean moved = false;
+ boolean showScroll = false;
+ switch (direction) {
+ case FOCUS_FORWARD:
+ if (selectedPosition < count - 1) {
+ // Move to the next item.
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition + 1);
+ moved = true;
+ // Show the scrollbar only if changing rows.
+ showScroll = selectedPosition == endOfRow;
+ }
+ break;
+
+ case FOCUS_BACKWARD:
+ if (selectedPosition > 0) {
+ // Move to the previous item.
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition - 1);
+ moved = true;
+ // Show the scrollbar only if changing rows.
+ showScroll = selectedPosition == startOfRow;
+ }
+ break;
+ }
+
+ if (moved) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ invokeOnItemScrollListener();
+ }
+
+ if (showScroll) {
+ awakenScrollBars();
+ }
+
+ 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, FOCUS_FORWARD, FOCUS_BACKWARD}
+ * @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.min(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;
+ case View.FOCUS_FORWARD:
+ // coming from top-left, need to be first in top row
+ return childIndex == rowStart && rowStart == 0;
+ case View.FOCUS_BACKWARD:
+ // coming from bottom-right, need to be last in bottom row
+ return childIndex == rowEnd && rowEnd == count - 1;
+ default:
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+ + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
+ }
+ }
+
+ /**
+ * Set the gravity for this grid. Gravity 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();
+ }
+ }
+
+ /**
+ * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT
+ *
+ * @return the gravity that will be applied to this grid's children
+ *
+ * @attr ref android.R.styleable#GridView_gravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Returns the amount of horizontal spacing currently used between each item in the grid.
+ *
+ * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)}
+ * has been called but layout is not yet complete, this method may return a stale value.
+ * To get the horizontal spacing that was explicitly requested use
+ * {@link #getRequestedHorizontalSpacing()}.</p>
+ *
+ * @return Current horizontal spacing between each item in pixels
+ *
+ * @see #setHorizontalSpacing(int)
+ * @see #getRequestedHorizontalSpacing()
+ *
+ * @attr ref android.R.styleable#GridView_horizontalSpacing
+ */
+ public int getHorizontalSpacing() {
+ return mHorizontalSpacing;
+ }
+
+ /**
+ * Returns the requested amount of horizontal spacing between each item in the grid.
+ *
+ * <p>The value returned may have been supplied during inflation as part of a style,
+ * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}.
+ * If layout is not yet complete or if GridView calculated a different horizontal spacing
+ * from what was requested, this may return a different value from
+ * {@link #getHorizontalSpacing()}.</p>
+ *
+ * @return The currently requested horizontal spacing between items, in pixels
+ *
+ * @see #setHorizontalSpacing(int)
+ * @see #getHorizontalSpacing()
+ *
+ * @attr ref android.R.styleable#GridView_horizontalSpacing
+ */
+ public int getRequestedHorizontalSpacing() {
+ return mRequestedHorizontalSpacing;
+ }
+
+ /**
+ * 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.
+ *
+ * @see #getVerticalSpacing()
+ *
+ * @attr ref android.R.styleable#GridView_verticalSpacing
+ */
+ public void setVerticalSpacing(int verticalSpacing) {
+ if (verticalSpacing != mVerticalSpacing) {
+ mVerticalSpacing = verticalSpacing;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ /**
+ * Returns the amount of vertical spacing between each item in the grid.
+ *
+ * @return The vertical spacing between items in pixels
+ *
+ * @see #setVerticalSpacing(int)
+ *
+ * @attr ref android.R.styleable#GridView_verticalSpacing
+ */
+ public int getVerticalSpacing() {
+ return mVerticalSpacing;
+ }
+
+ /**
+ * Control how items are stretched to fill their space.
+ *
+ * @param stretchMode Either {@link #NO_STRETCH},
+ * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}.
+ *
+ * @attr ref android.R.styleable#GridView_stretchMode
+ */
+ public void setStretchMode(@StretchMode int stretchMode) {
+ if (stretchMode != mStretchMode) {
+ mStretchMode = stretchMode;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ @StretchMode
+ 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();
+ }
+ }
+
+ /**
+ * Return the width of a column in the grid.
+ *
+ * <p>This may not be valid yet if a layout is pending.</p>
+ *
+ * @return The column width in pixels
+ *
+ * @see #setColumnWidth(int)
+ * @see #getRequestedColumnWidth()
+ *
+ * @attr ref android.R.styleable#GridView_columnWidth
+ */
+ public int getColumnWidth() {
+ return mColumnWidth;
+ }
+
+ /**
+ * Return the requested width of a column in the grid.
+ *
+ * <p>This may not be the actual column width used. Use {@link #getColumnWidth()}
+ * to retrieve the current real width of a column.</p>
+ *
+ * @return The requested column width in pixels
+ *
+ * @see #setColumnWidth(int)
+ * @see #getColumnWidth()
+ *
+ * @attr ref android.R.styleable#GridView_columnWidth
+ */
+ public int getRequestedColumnWidth() {
+ return mRequestedColumnWidth;
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Get the number of columns in the grid.
+ * Returns {@link #AUTO_FIT} if the Grid has never been laid out.
+ *
+ * @attr ref android.R.styleable#GridView_numColumns
+ *
+ * @see #setNumColumns(int)
+ */
+ @ViewDebug.ExportedProperty
+ public int getNumColumns() {
+ return mNumColumns;
+ }
+
+ /**
+ * 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 numColumns = mNumColumns;
+ final int rowCount = (mItemCount + numColumns - 1) / numColumns;
+ // In case of stackFromBottom the calculation of whichRow needs
+ // to take into account that counting from the top the first row
+ // might not be entirely filled.
+ final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) -
+ mItemCount) : 0;
+ final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns;
+ return Math.max(whichRow * 100 - (top * 100) / height +
+ (int) ((float) mScrollY / getHeight() * rowCount * 100), 0);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ // TODO: Account for vertical spacing too
+ final int numColumns = mNumColumns;
+ final int rowCount = (mItemCount + numColumns - 1) / numColumns;
+ int result = Math.max(rowCount * 100, 0);
+ if (mScrollY != 0) {
+ // Compensate for overscroll
+ result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100));
+ }
+ return result;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return GridView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ final int columnsCount = getNumColumns();
+ final int rowsCount = getCount() / columnsCount;
+ final int selectionMode = getSelectionModeForAccessibility();
+ final CollectionInfo collectionInfo = CollectionInfo.obtain(
+ rowsCount, columnsCount, false, selectionMode);
+ info.setCollectionInfo(collectionInfo);
+
+ if (columnsCount > 0 || rowsCount > 0) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ switch (action) {
+ case R.id.accessibilityActionScrollToPosition: {
+ // GridView only supports scrolling in one direction, so we can
+ // ignore the column argument.
+ final int numColumns = getNumColumns();
+ final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+ final int position = Math.min(row * numColumns, getCount() - 1);
+ if (row >= 0) {
+ // The accessibility service gets data asynchronously, so
+ // we'll be a little lenient by clamping the last position.
+ smoothScrollToPosition(position);
+ return true;
+ }
+ } break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoForItem(
+ View view, int position, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoForItem(view, position, info);
+
+ final int count = getCount();
+ final int columnsCount = getNumColumns();
+ final int rowsCount = count / columnsCount;
+
+ final int row;
+ final int column;
+ if (!mStackFromBottom) {
+ column = position % columnsCount;
+ row = position / columnsCount;
+ } else {
+ final int invertedIndex = count - 1 - position;
+
+ column = columnsCount - 1 - (invertedIndex % columnsCount);
+ row = rowsCount - 1 - invertedIndex / columnsCount;
+ }
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ final boolean isSelected = isItemChecked(position);
+ final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
+ row, 1, column, 1, isHeading, isSelected);
+ info.setCollectionItemInfo(itemInfo);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("numColumns", getNumColumns());
+ }
+}
diff --git a/android/widget/HeaderViewListAdapter.java b/android/widget/HeaderViewListAdapter.java
new file mode 100644
index 0000000..f9d8f92
--- /dev/null
+++ b/android/widget/HeaderViewListAdapter.java
@@ -0,0 +1,270 @@
+/*
+ * 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 final ListAdapter mAdapter;
+
+ // These two ArrayList are assumed to NOT be null.
+ // They are indeed created when declared in ListView and then shared.
+ ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
+ ArrayList<ListView.FixedViewInfo> mFooterViewInfos;
+
+ // Used as a placeholder in case the provided info views are indeed null.
+ // Currently only used by some CTS tests, which may be removed.
+ static final ArrayList<ListView.FixedViewInfo> EMPTY_INFO_LIST =
+ new ArrayList<ListView.FixedViewInfo>();
+
+ boolean mAreAllFixedViewsSelectable;
+
+ private final boolean mIsFilterable;
+
+ public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
+ ArrayList<ListView.FixedViewInfo> footerViewInfos,
+ ListAdapter adapter) {
+ mAdapter = adapter;
+ mIsFilterable = adapter instanceof Filterable;
+
+ if (headerViewInfos == null) {
+ mHeaderViewInfos = EMPTY_INFO_LIST;
+ } else {
+ mHeaderViewInfos = headerViewInfos;
+ }
+
+ if (footerViewInfos == null) {
+ mFooterViewInfos = EMPTY_INFO_LIST;
+ } else {
+ mFooterViewInfos = footerViewInfos;
+ }
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+ }
+
+ public int getHeadersCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ public int getFootersCount() {
+ return mFooterViewInfos.size();
+ }
+
+ public boolean isEmpty() {
+ return mAdapter == null || mAdapter.isEmpty();
+ }
+
+ private boolean areAllListInfosSelectable(ArrayList<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) {
+ // Header (negative positions will throw an IndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).isSelectable;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.isEnabled(adjPosition);
+ }
+ }
+
+ // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
+ }
+
+ public Object getItem(int position) {
+ // Header (negative positions will throw an IndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).data;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItem(adjPosition);
+ }
+ }
+
+ // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).data;
+ }
+
+ public long getItemId(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ public boolean hasStableIds() {
+ if (mAdapter != null) {
+ return mAdapter.hasStableIds();
+ }
+ return false;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Header (negative positions will throw an IndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).view;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getView(adjPosition, convertView, parent);
+ }
+ }
+
+ // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).view;
+ }
+
+ public int getItemViewType(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemViewType(adjPosition);
+ }
+ }
+
+ return 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/android/widget/HeterogeneousExpandableList.java b/android/widget/HeterogeneousExpandableList.java
new file mode 100644
index 0000000..e7e0933
--- /dev/null
+++ b/android/widget/HeterogeneousExpandableList.java
@@ -0,0 +1,106 @@
+/*
+ * 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.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Additional methods that when implemented make an
+ * {@link ExpandableListAdapter} take advantage of the {@link Adapter} view type
+ * mechanism.
+ * <p>
+ * An {@link ExpandableListAdapter} declares it has one view type for its group items
+ * and one view type for its child items. Although adapted for most {@link ExpandableListView}s,
+ * these values should be tuned for heterogeneous {@link ExpandableListView}s.
+ * </p>
+ * Lists that contain different types of group and/or child item views, should use an adapter that
+ * implements this interface. This way, the recycled views that will be provided to
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * and
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * will be of the appropriate group or child type, resulting in a more efficient reuse of the
+ * previously created views.
+ */
+public interface HeterogeneousExpandableList {
+ /**
+ * Get the type of group View that will be created by
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * . for the specified group item.
+ *
+ * @param groupPosition the position of the group for which the type should be returned.
+ * @return An integer representing the type of group View. Two group views should share the same
+ * type if one can be converted to the other in
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * . Note: Integers must be in the range 0 to {@link #getGroupTypeCount} - 1.
+ * {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned.
+ * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE
+ * @see #getGroupTypeCount()
+ */
+ int getGroupType(int groupPosition);
+
+ /**
+ * Get the type of child View that will be created by
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * for the specified child item.
+ *
+ * @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 An integer representing the type of child View. Two child views should share the same
+ * type if one can be converted to the other in
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * Note: Integers must be in the range 0 to {@link #getChildTypeCount} - 1.
+ * {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned.
+ * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE
+ * @see #getChildTypeCount()
+ */
+ int getChildType(int groupPosition, int childPosition);
+
+ /**
+ * <p>
+ * Returns the number of types of group Views that will be created by
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * . Each type represents a set of views that can be converted in
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * . If the adapter always returns the same type of View for all group items, this method should
+ * return 1.
+ * </p>
+ * This method will only be called when the adapter is set on the {@link AdapterView}.
+ *
+ * @return The number of types of group Views that will be created by this adapter.
+ * @see #getChildTypeCount()
+ * @see #getGroupType(int)
+ */
+ int getGroupTypeCount();
+
+ /**
+ * <p>
+ * Returns the number of types of child Views that will be created by
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * . Each type represents a set of views that can be converted in
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * , for any group. If the adapter always returns the same type of View for
+ * all child items, this method should return 1.
+ * </p>
+ * This method will only be called when the adapter is set on the {@link AdapterView}.
+ *
+ * @return The total number of types of child Views that will be created by this adapter.
+ * @see #getGroupTypeCount()
+ * @see #getChildType(int, int)
+ */
+ int getChildTypeCount();
+}
diff --git a/android/widget/HorizontalScrollView.java b/android/widget/HorizontalScrollView.java
new file mode 100644
index 0000000..0bf2460
--- /dev/null
+++ b/android/widget/HorizontalScrollView.java
@@ -0,0 +1,1771 @@
+/*
+ * Copyright (C) 2009 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.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+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 HorizontalScrollView
+ * 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 horizontal orientation, presenting a horizontal
+ * array of top-level items that the user can scroll through.
+ *
+ * <p>The {@link TextView} class also
+ * takes care of its own scrolling, so does not require a HorizontalScrollView, but
+ * using the two together is possible to achieve the effect of a text view
+ * within a larger container.
+ *
+ * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
+ * use either {@link ScrollView} or {@link ListView}.
+ *
+ * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+ */
+public class HorizontalScrollView extends FrameLayout {
+ private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
+
+ private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
+
+ private static final String TAG = "HorizontalScrollView";
+
+ private long mLastScroll;
+
+ private final Rect mTempRect = new Rect();
+ private OverScroller mScroller;
+ private EdgeEffect mEdgeGlowLeft;
+ private EdgeEffect mEdgeGlowRight;
+
+ /**
+ * Position of the last motion event.
+ */
+ private int mLastMotionX;
+
+ /**
+ * 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.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ private boolean mFillViewport;
+
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ private boolean mSmoothScrollingEnabled = true;
+
+ private int mTouchSlop;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+
+ private int mOverscrollDistance;
+ private int mOverflingDistance;
+
+ private float mHorizontalScrollFactor;
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ private SavedState mSavedState;
+
+ public HorizontalScrollView(Context context) {
+ this(context, null);
+ }
+
+ public HorizontalScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
+ }
+
+ public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public HorizontalScrollView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initScrollView();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes);
+
+ setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
+
+ a.recycle();
+
+ if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+ setRevealOnFocusHint(false);
+ }
+ }
+
+ @Override
+ protected float getLeftFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+
+ final int length = getHorizontalFadingEdgeLength();
+ if (mScrollX < length) {
+ return mScrollX / (float) length;
+ }
+
+ return 1.0f;
+ }
+
+ @Override
+ protected float getRightFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+
+ final int length = getHorizontalFadingEdgeLength();
+ final int rightEdge = getWidth() - mPaddingRight;
+ final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
+ 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 * (mRight - mLeft));
+ }
+
+
+ private void initScrollView() {
+ mScroller = new OverScroller(getContext());
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setWillNotDraw(false);
+ final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ mHorizontalScrollFactor = configuration.getScaledHorizontalScrollFactor();
+ }
+
+ @Override
+ public void addView(View child) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("HorizontalScrollView can host only one direct child");
+ }
+
+ super.addView(child);
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("HorizontalScrollView 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("HorizontalScrollView 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("HorizontalScrollView can host only one direct child");
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * @return Returns true this HorizontalScrollView can be scrolled
+ */
+ private boolean canScroll() {
+ View child = getChildAt(0);
+ if (child != null) {
+ int childWidth = child.getWidth();
+ return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
+ }
+ return false;
+ }
+
+ /**
+ * Indicates whether this HorizontalScrollView's content is stretched to
+ * fill the viewport.
+ *
+ * @return True if the content fills the viewport, false otherwise.
+ *
+ * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+ */
+ public boolean isFillViewport() {
+ return mFillViewport;
+ }
+
+ /**
+ * Indicates this HorizontalScrollView whether it should stretch its content width
+ * to fill the viewport or not.
+ *
+ * @param fillViewport True to stretch the content's width to the viewport's
+ * boundaries, false otherwise.
+ *
+ * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+ */
+ 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 widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ return;
+ }
+
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ final int widthPadding;
+ final int heightPadding;
+ final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion >= Build.VERSION_CODES.M) {
+ widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
+ heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
+ } else {
+ widthPadding = mPaddingLeft + mPaddingRight;
+ heightPadding = mPaddingTop + mPaddingBottom;
+ }
+
+ int desiredWidth = getMeasuredWidth() - widthPadding;
+ if (child.getMeasuredWidth() < desiredWidth) {
+ final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ desiredWidth, MeasureSpec.EXACTLY);
+ final int childHeightMeasureSpec = getChildMeasureSpec(
+ heightMeasureSpec, heightPadding, lp.height);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || 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_RIGHT);
+ return nextFocused != null && nextFocused != this &&
+ nextFocused.requestFocus(View.FOCUS_RIGHT);
+ }
+ return false;
+ }
+
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_LEFT);
+ } else {
+ handled = fullScroll(View.FOCUS_LEFT);
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_RIGHT);
+ } else {
+ handled = fullScroll(View.FOCUS_RIGHT);
+ }
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
+ break;
+ }
+ }
+
+ return handled;
+ }
+
+ private boolean inChild(int x, int y) {
+ if (getChildCount() > 0) {
+ final int scrollX = mScrollX;
+ final View child = getChildAt(0);
+ return !(y < child.getTop()
+ || y >= child.getBottom()
+ || x < child.getLeft() - scrollX
+ || x >= child.getRight() - scrollX);
+ }
+ return false;
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ @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 (super.onInterceptTouchEvent(ev)) {
+ return true;
+ }
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ 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. mLastMotionX is set to the x value
+ * of the down event.
+ */
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + activePointerId
+ + " in onInterceptTouchEvent");
+ break;
+ }
+
+ final int x = (int) ev.getX(pointerIndex);
+ final int xDiff = (int) Math.abs(x - mLastMotionX);
+ if (xDiff > mTouchSlop) {
+ mIsBeingDragged = true;
+ mLastMotionX = x;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ final int x = (int) ev.getX();
+ if (!inChild((int) x, (int) ev.getY())) {
+ mIsBeingDragged = false;
+ recycleVelocityTracker();
+ break;
+ }
+
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionX = x;
+ mActivePointerId = ev.getPointerId(0);
+
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+
+ /*
+ * 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;
+ mActivePointerId = INVALID_POINTER;
+ if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
+ postInvalidateOnAnimation();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int index = ev.getActionIndex();
+ mLastMotionX = (int) ev.getX(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
+ 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) {
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ if (getChildCount() == 0) {
+ return false;
+ }
+ if ((mIsBeingDragged = !mScroller.isFinished())) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ /*
+ * 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
+ mLastMotionX = (int) ev.getX();
+ mActivePointerId = ev.getPointerId(0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (activePointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+ break;
+ }
+
+ final int x = (int) ev.getX(activePointerIndex);
+ int deltaX = mLastMotionX - x;
+ if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ mIsBeingDragged = true;
+ if (deltaX > 0) {
+ deltaX -= mTouchSlop;
+ } else {
+ deltaX += mTouchSlop;
+ }
+ }
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ mLastMotionX = x;
+
+ final int oldX = mScrollX;
+ final int oldY = mScrollY;
+ final int range = getScrollRange();
+ final int overscrollMode = getOverScrollMode();
+ final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+ // Calling overScrollBy will call onOverScrolled, which
+ // calls onScrollChanged if applicable.
+ if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
+ mOverscrollDistance, 0, true)) {
+ // Break our velocity if we hit a scroll barrier.
+ mVelocityTracker.clear();
+ }
+
+ if (canOverscroll) {
+ final int pulledToX = oldX + deltaX;
+ if (pulledToX < 0) {
+ mEdgeGlowLeft.onPull((float) deltaX / getWidth(),
+ 1.f - ev.getY(activePointerIndex) / getHeight());
+ if (!mEdgeGlowRight.isFinished()) {
+ mEdgeGlowRight.onRelease();
+ }
+ } else if (pulledToX > range) {
+ mEdgeGlowRight.onPull((float) deltaX / getWidth(),
+ ev.getY(activePointerIndex) / getHeight());
+ if (!mEdgeGlowLeft.isFinished()) {
+ mEdgeGlowLeft.onRelease();
+ }
+ }
+ if (mEdgeGlowLeft != null
+ && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
+ postInvalidateOnAnimation();
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
+
+ if (getChildCount() > 0) {
+ if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+ fling(-initialVelocity);
+ } else {
+ if (mScroller.springBack(mScrollX, mScrollY, 0,
+ getScrollRange(), 0, 0)) {
+ postInvalidateOnAnimation();
+ }
+ }
+ }
+
+ mActivePointerId = INVALID_POINTER;
+ mIsBeingDragged = false;
+ recycleVelocityTracker();
+
+ if (mEdgeGlowLeft != null) {
+ mEdgeGlowLeft.onRelease();
+ mEdgeGlowRight.onRelease();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged && getChildCount() > 0) {
+ if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
+ postInvalidateOnAnimation();
+ }
+ mActivePointerId = INVALID_POINTER;
+ mIsBeingDragged = false;
+ recycleVelocityTracker();
+
+ if (mEdgeGlowLeft != null) {
+ mEdgeGlowLeft.onRelease();
+ mEdgeGlowRight.onRelease();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ return true;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionX = (int) ev.getX(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL: {
+ if (!mIsBeingDragged) {
+ final float axisValue;
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+ axisValue = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ } else {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ }
+ } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ } else {
+ axisValue = 0;
+ }
+
+ final int delta = Math.round(axisValue * mHorizontalScrollFactor);
+ if (delta != 0) {
+ final int range = getScrollRange();
+ int oldScrollX = mScrollX;
+ int newScrollX = oldScrollX + delta;
+ if (newScrollX < 0) {
+ newScrollX = 0;
+ } else if (newScrollX > range) {
+ newScrollX = range;
+ }
+ if (newScrollX != oldScrollX) {
+ super.scrollTo(newScrollX, mScrollY);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY,
+ boolean clampedX, boolean clampedY) {
+ // Treat animating scrolls differently; see #computeScroll() for why.
+ if (!mScroller.isFinished()) {
+ final int oldX = mScrollX;
+ final int oldY = mScrollY;
+ mScrollX = scrollX;
+ mScrollY = scrollY;
+ invalidateParentIfNeeded();
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ if (clampedX) {
+ mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
+ }
+ } else {
+ super.scrollTo(scrollX, scrollY);
+ }
+
+ awakenScrollBars();
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case R.id.accessibilityActionScrollRight: {
+ if (!isEnabled()) {
+ return false;
+ }
+ final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+ final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
+ if (targetScrollX != mScrollX) {
+ smoothScrollTo(targetScrollX, 0);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ case R.id.accessibilityActionScrollLeft: {
+ if (!isEnabled()) {
+ return false;
+ }
+ final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+ final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
+ if (targetScrollX != mScrollX) {
+ smoothScrollTo(targetScrollX, 0);
+ return true;
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return HorizontalScrollView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ final int scrollRange = getScrollRange();
+ if (scrollRange > 0) {
+ info.setScrollable(true);
+ if (isEnabled() && mScrollX > 0) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_LEFT);
+ }
+ if (isEnabled() && mScrollX < scrollRange) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_RIGHT);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setScrollable(getScrollRange() > 0);
+ event.setScrollX(mScrollX);
+ event.setScrollY(mScrollY);
+ event.setMaxScrollX(getScrollRange());
+ event.setMaxScrollY(mScrollY);
+ }
+
+ private int getScrollRange() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0,
+ child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
+ }
+ return scrollRange;
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in this View's bounds
+ * (excluding fading edges) pretending that this View's left is located at
+ * the parameter left.
+ * </p>
+ *
+ * @param leftFocus look for a candidate is the one at the left of the bounds
+ * if leftFocus is true, or at the right of the bounds if leftFocus
+ * is false
+ * @param left the left 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 leftFocus,
+ final int left, 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 = getHorizontalFadingEdgeLength() / 2;
+ final int leftWithoutFadingEdge = left + fadingEdgeLength;
+ final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
+
+ if ((preferredFocusable != null)
+ && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
+ && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
+ return preferredFocusable;
+ }
+
+ return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
+ rightWithoutFadingEdge);
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in the specified bounds.
+ * </p>
+ *
+ * @param leftFocus look for a candidate is the one at the left of the bounds
+ * if leftFocus is true, or at the right of the bounds if
+ * leftFocus is false
+ * @param left the left offset of the bounds in which a focusable must be
+ * found
+ * @param right the right 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 leftFocus, int left, int right) {
+
+ List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+ View focusCandidate = null;
+
+ /*
+ * A fully contained focusable is one where its left is below the bound's
+ * left, and its right is above the bound's right. 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 viewLeft = view.getLeft();
+ int viewRight = view.getRight();
+
+ if (left < viewRight && viewLeft < right) {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+
+ final boolean viewIsFullyContained = (left < viewLeft) &&
+ (viewRight < right);
+
+ if (focusCandidate == null) {
+ /* No candidate, take this one */
+ focusCandidate = view;
+ foundFullyContainedFocusable = viewIsFullyContained;
+ } else {
+ final boolean viewIsCloserToBoundary =
+ (leftFocus && viewLeft < focusCandidate.getLeft()) ||
+ (!leftFocus && viewRight > focusCandidate.getRight());
+
+ 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 left or right and give the focus
+ * to the leftmost/rightmost 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_LEFT}
+ * to go one page left or {@link android.view.View#FOCUS_RIGHT}
+ * to go one page right
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean pageScroll(int direction) {
+ boolean right = direction == View.FOCUS_RIGHT;
+ int width = getWidth();
+
+ if (right) {
+ mTempRect.left = getScrollX() + width;
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(0);
+ if (mTempRect.left + width > view.getRight()) {
+ mTempRect.left = view.getRight() - width;
+ }
+ }
+ } else {
+ mTempRect.left = getScrollX() - width;
+ if (mTempRect.left < 0) {
+ mTempRect.left = 0;
+ }
+ }
+ mTempRect.right = mTempRect.left + width;
+
+ return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
+ }
+
+ /**
+ * <p>Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the left or right and give the focus
+ * to the leftmost/rightmost 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_LEFT}
+ * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
+ * to go the right
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean fullScroll(int direction) {
+ boolean right = direction == View.FOCUS_RIGHT;
+ int width = getWidth();
+
+ mTempRect.left = 0;
+ mTempRect.right = width;
+
+ if (right) {
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(0);
+ mTempRect.right = view.getRight();
+ mTempRect.left = mTempRect.right - width;
+ }
+ }
+
+ return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
+ }
+
+ /**
+ * <p>Scrolls the view to make the area defined by <code>left</code> and
+ * <code>right</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_LEFT}
+ * to go left {@link android.view.View#FOCUS_RIGHT} to right
+ * @param left the left offset of the new area to be made visible
+ * @param right the right 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 left, int right) {
+ boolean handled = true;
+
+ int width = getWidth();
+ int containerLeft = getScrollX();
+ int containerRight = containerLeft + width;
+ boolean goLeft = direction == View.FOCUS_LEFT;
+
+ View newFocused = findFocusableViewInBounds(goLeft, left, right);
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (left >= containerLeft && right <= containerRight) {
+ handled = false;
+ } else {
+ int delta = goLeft ? (left - containerLeft) : (right - containerRight);
+ doScrollX(delta);
+ }
+
+ if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+ return handled;
+ }
+
+ /**
+ * Handle scrolling in response to a left or right 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);
+ doScrollX(scrollDelta);
+ nextFocused.requestFocus(direction);
+ } else {
+ // no new focus
+ int scrollDelta = maxJump;
+
+ if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
+ scrollDelta = getScrollX();
+ } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
+
+ int daRight = getChildAt(0).getRight();
+
+ int screenRight = getScrollX() + getWidth();
+
+ if (daRight - screenRight < maxJump) {
+ scrollDelta = daRight - screenRight;
+ }
+ }
+ if (scrollDelta == 0) {
+ return false;
+ }
+ doScrollX(direction == View.FOCUS_RIGHT ? 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.right + delta) >= getScrollX()
+ && (mTempRect.left - delta) <= (getScrollX() + getWidth());
+ }
+
+ /**
+ * Smooth scroll by a X delta
+ *
+ * @param delta the number of pixels to scroll by on the X axis
+ */
+ private void doScrollX(int delta) {
+ if (delta != 0) {
+ if (mSmoothScrollingEnabled) {
+ smoothScrollBy(delta, 0);
+ } else {
+ scrollBy(delta, 0);
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ return;
+ }
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ if (duration > ANIMATED_SCROLL_GAP) {
+ final int width = getWidth() - mPaddingRight - mPaddingLeft;
+ final int right = getChildAt(0).getWidth();
+ final int maxX = Math.max(0, right - width);
+ final int scrollX = mScrollX;
+ dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
+
+ mScroller.startScroll(scrollX, mScrollY, dx, 0);
+ postInvalidateOnAnimation();
+ } 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 width of all of its
+ * children.</p>
+ */
+ @Override
+ protected int computeHorizontalScrollRange() {
+ final int count = getChildCount();
+ final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
+ if (count == 0) {
+ return contentWidth;
+ }
+
+ int scrollRange = getChildAt(0).getRight();
+ final int scrollX = mScrollX;
+ final int overscrollRight = Math.max(0, scrollRange - contentWidth);
+ if (scrollX < 0) {
+ scrollRange -= scrollX;
+ } else if (scrollX > overscrollRight) {
+ scrollRange += scrollX - overscrollRight;
+ }
+
+ return scrollRange;
+ }
+
+ @Override
+ protected int computeHorizontalScrollOffset() {
+ return Math.max(0, super.computeHorizontalScrollOffset());
+ }
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ final int horizontalPadding = mPaddingLeft + mPaddingRight;
+ final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
+ MeasureSpec.UNSPECIFIED);
+
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom, lp.height);
+ 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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+ final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
+ widthUsed;
+ final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
+ 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 (oldX != x || oldY != y) {
+ final int range = getScrollRange();
+ final int overscrollMode = getOverScrollMode();
+ final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+ overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
+ mOverflingDistance, 0, false);
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+
+ if (canOverscroll) {
+ if (x < 0 && oldX >= 0) {
+ mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
+ } else if (x > range && oldX <= range) {
+ mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
+ }
+ }
+ }
+
+ if (!awakenScrollBars()) {
+ postInvalidateOnAnimation();
+ }
+ }
+ }
+
+ /**
+ * 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(scrollDelta, 0);
+ }
+ }
+
+ /**
+ * 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(delta, 0);
+ } else {
+ smoothScrollBy(delta, 0);
+ }
+ }
+ return scroll;
+ }
+
+ /**
+ * Compute the amount to scroll in the X 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) {
+ if (getChildCount() == 0) return 0;
+
+ int width = getWidth();
+ int screenLeft = getScrollX();
+ int screenRight = screenLeft + width;
+
+ int fadingEdge = getHorizontalFadingEdgeLength();
+
+ // leave room for left fading edge as long as rect isn't at very left
+ if (rect.left > 0) {
+ screenLeft += fadingEdge;
+ }
+
+ // leave room for right fading edge as long as rect isn't at very right
+ if (rect.right < getChildAt(0).getWidth()) {
+ screenRight -= fadingEdge;
+ }
+
+ int scrollXDelta = 0;
+
+ if (rect.right > screenRight && rect.left > screenLeft) {
+ // need to move right to get it in view: move right just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.width() > width) {
+ // just enough to get screen size chunk on
+ scrollXDelta += (rect.left - screenLeft);
+ } else {
+ // get entire rect at right of screen
+ scrollXDelta += (rect.right - screenRight);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int right = getChildAt(0).getRight();
+ int distanceToRight = right - screenRight;
+ scrollXDelta = Math.min(scrollXDelta, distanceToRight);
+
+ } else if (rect.left < screenLeft && rect.right < screenRight) {
+ // need to move right to get it in view: move right just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.width() > width) {
+ // screen size chunk
+ scrollXDelta -= (screenRight - rect.right);
+ } else {
+ // entire rect at left
+ scrollXDelta -= (screenLeft - rect.left);
+ }
+
+ // make sure we aren't scrolling any further than the left our content
+ scrollXDelta = Math.max(scrollXDelta, -getScrollX());
+ }
+ return scrollXDelta;
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (focused != null && focused.getRevealOnFocusHint()) {
+ 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_RIGHT;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ direction = View.FOCUS_LEFT;
+ }
+
+ 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());
+
+ return scrollToChildRect(rectangle, immediate);
+ }
+
+ @Override
+ public void requestLayout() {
+ mIsLayoutDirty = true;
+ super.requestLayout();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int childWidth = 0;
+ int childMargins = 0;
+
+ if (getChildCount() > 0) {
+ childWidth = getChildAt(0).getMeasuredWidth();
+ LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
+ childMargins = childParams.leftMargin + childParams.rightMargin;
+ }
+
+ final int available = r - l - getPaddingLeftWithForeground() -
+ getPaddingRightWithForeground() - childMargins;
+
+ final boolean forceLeftGravity = (childWidth > available);
+
+ layoutChildren(l, t, r, b, forceLeftGravity);
+
+ mIsLayoutDirty = false;
+ // Give a child focus if it needs it
+ if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+ scrollToChild(mChildToScrollTo);
+ }
+ mChildToScrollTo = null;
+
+ if (!isLaidOut()) {
+ final int scrollRange = Math.max(0,
+ childWidth - (r - l - mPaddingLeft - mPaddingRight));
+ if (mSavedState != null) {
+ mScrollX = isLayoutRtl()
+ ? scrollRange - mSavedState.scrollOffsetFromStart
+ : mSavedState.scrollOffsetFromStart;
+ mSavedState = null;
+ } else {
+ if (isLayoutRtl()) {
+ mScrollX = scrollRange - mScrollX;
+ } // mScrollX default value is "0" for LTR
+ }
+ // Don't forget to clamp
+ if (mScrollX > scrollRange) {
+ mScrollX = scrollRange;
+ } else if (mScrollX < 0) {
+ mScrollX = 0;
+ }
+ }
+
+ // Calling this with the present values causes it to re-claim them
+ scrollTo(mScrollX, mScrollY);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ View currentFocused = findFocus();
+ if (null == currentFocused || this == currentFocused)
+ return;
+
+ final int maxJump = mRight - mLeft;
+
+ if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
+ currentFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollX(scrollDelta);
+ }
+ }
+
+ /**
+ * Return true if child is a descendant of parent, (or equal to the parent).
+ */
+ private static 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 velocityX The initial velocity in the X direction. Positive
+ * numbers mean that the finger/cursor is moving down the screen,
+ * which means we want to scroll towards the left.
+ */
+ public void fling(int velocityX) {
+ if (getChildCount() > 0) {
+ int width = getWidth() - mPaddingRight - mPaddingLeft;
+ int right = getChildAt(0).getWidth();
+
+ mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
+ Math.max(0, right - width), 0, 0, width/2, 0);
+
+ final boolean movingRight = velocityX > 0;
+
+ View currentFocused = findFocus();
+ View newFocused = findFocusableViewInMyBounds(movingRight,
+ mScroller.getFinalX(), currentFocused);
+
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (newFocused != currentFocused) {
+ newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
+ }
+
+ postInvalidateOnAnimation();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This version also clamps the scrolling to the bounds of our child.
+ */
+ @Override
+ 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, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
+ y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
+ if (x != mScrollX || y != mScrollY) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ @Override
+ public void setOverScrollMode(int mode) {
+ if (mode != OVER_SCROLL_NEVER) {
+ if (mEdgeGlowLeft == null) {
+ Context context = getContext();
+ mEdgeGlowLeft = new EdgeEffect(context);
+ mEdgeGlowRight = new EdgeEffect(context);
+ }
+ } else {
+ mEdgeGlowLeft = null;
+ mEdgeGlowRight = null;
+ }
+ super.setOverScrollMode(mode);
+ }
+
+ @SuppressWarnings({"SuspiciousNameCombination"})
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mEdgeGlowLeft != null) {
+ final int scrollX = mScrollX;
+ if (!mEdgeGlowLeft.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight() - mPaddingTop - mPaddingBottom;
+
+ canvas.rotate(270);
+ canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
+ mEdgeGlowLeft.setSize(height, getWidth());
+ if (mEdgeGlowLeft.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - mPaddingTop - mPaddingBottom;
+
+ canvas.rotate(90);
+ canvas.translate(-mPaddingTop,
+ -(Math.max(getScrollRange(), scrollX) + width));
+ mEdgeGlowRight.setSize(height, width);
+ if (mEdgeGlowRight.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+ }
+
+ private static int clamp(int n, int my, int child) {
+ if (my >= child || n < 0) {
+ return 0;
+ }
+ if ((my + n) > child) {
+ return child - my;
+ }
+ return n;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ // Some old apps reused IDs in ways they shouldn't have.
+ // Don't break them, but they don't get scroll state restoration.
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mSavedState = ss;
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ // Some old apps reused IDs in ways they shouldn't have.
+ // Don't break them, but they don't get scroll state restoration.
+ return super.onSaveInstanceState();
+ }
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.scrollOffsetFromStart = isLayoutRtl() ? -mScrollX : mScrollX;
+ return ss;
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("layout:fillViewPort", mFillViewport);
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int scrollOffsetFromStart;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ scrollOffsetFromStart = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(scrollOffsetFromStart);
+ }
+
+ @Override
+ public String toString() {
+ return "HorizontalScrollView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " scrollPosition=" + scrollOffsetFromStart
+ + "}";
+ }
+
+ 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];
+ }
+ };
+ }
+}
diff --git a/android/widget/ImageButton.java b/android/widget/ImageButton.java
new file mode 100644
index 0000000..e1b0c91
--- /dev/null
+++ b/android/widget/ImageButton.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * <p>
+ * Displays a button with an image (instead of text) that can be pressed
+ * or clicked by the user. By default, an ImageButton looks like a regular
+ * {@link android.widget.Button}, with the standard button background
+ * that changes color during different button states. The image on the surface
+ * of the button is defined either by the {@code android:src} attribute in the
+ * {@code <ImageButton>} XML element or by the
+ * {@link #setImageResource(int)} method.</p>
+ *
+ * <p>To remove the standard button background image, define your own
+ * background image or set the background color to be transparent.</p>
+ * <p>To indicate the different button states (focused, selected, etc.), you can
+ * define a different image for each state. E.g., a blue image by default, an
+ * orange one for when focused, and a yellow one for when pressed. An easy way to
+ * do this is with an XML drawable "selector." For example:</p>
+ * <pre>
+ * <?xml version="1.0" encoding="utf-8"?>
+ * <selector xmlns:android="http://schemas.android.com/apk/res/android">
+ * <item android:state_pressed="true"
+ * android:drawable="@drawable/button_pressed" /> <!-- pressed -->
+ * <item android:state_focused="true"
+ * android:drawable="@drawable/button_focused" /> <!-- focused -->
+ * <item android:drawable="@drawable/button_normal" /> <!-- default -->
+ * </selector></pre>
+ *
+ * <p>Save the XML file in your project {@code res/drawable/} folder and then
+ * reference it as a drawable for the source of your ImageButton (in the
+ * {@code android:src} attribute). Android will automatically change the image
+ * based on the state of the button and the corresponding images
+ * defined in the XML.</p>
+ *
+ * <p>The order of the {@code <item>} elements is important because they are
+ * evaluated in order. This is why the "normal" button image comes last, because
+ * it will only be applied after {@code android:state_pressed} and {@code
+ * android:state_focused} have both evaluated false.</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/button.html">Buttons</a>
+ * guide.</p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#ImageView Button Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+@RemoteView
+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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setFocusable(true);
+ }
+
+ @Override
+ protected boolean onSetAlpha(int alpha) {
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ImageButton.class.getName();
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+}
diff --git a/android/widget/ImageSwitcher.java b/android/widget/ImageSwitcher.java
new file mode 100644
index 0000000..112fcc3
--- /dev/null
+++ b/android/widget/ImageSwitcher.java
@@ -0,0 +1,105 @@
+/*
+ * 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.annotation.DrawableRes;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+
+/**
+ * {@link ViewSwitcher} that switches between two ImageViews when a new
+ * image is set on it. The views added to an ImageSwitcher must all be
+ * {@link ImageView ImageViews}.
+ */
+public class ImageSwitcher extends ViewSwitcher
+{
+ /**
+ * Creates a new empty ImageSwitcher.
+ *
+ * @param context the application's environment
+ */
+ public ImageSwitcher(Context context)
+ {
+ super(context);
+ }
+
+ /**
+ * Creates a new empty ImageSwitcher for the given context and with the
+ * specified set attributes.
+ *
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public ImageSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Sets a new image on the ImageSwitcher with the given resource id.
+ * This will set that image resource on the next ImageView in the switcher and will
+ * then switch to that view.
+ *
+ * @param resid a Drawable resource id
+ *
+ * @see ImageView#setImageResource(int)
+ */
+ public void setImageResource(@DrawableRes int resid)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageResource(resid);
+ showNext();
+ }
+
+ /**
+ * Sets a new image on the ImageSwitcher with the given Uri.
+ * This will set that image on the next ImageView in the switcher and will
+ * then switch to that view.
+ *
+ * @param uri the Uri of an image
+ *
+ * @see ImageView#setImageURI(Uri)
+ */
+ public void setImageURI(Uri uri)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageURI(uri);
+ showNext();
+ }
+
+ /**
+ * Sets a new drawable on the ImageSwitcher.
+ * This will set that drawable on the next ImageView in the switcher and will
+ * then switch to that view.
+ *
+ * @param drawable the drawable to be set or {@code null} to clear the content
+ *
+ * @see ImageView#setImageDrawable(Drawable)
+ */
+ public void setImageDrawable(Drawable drawable)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageDrawable(drawable);
+ showNext();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ImageSwitcher.class.getName();
+ }
+}
diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java
new file mode 100644
index 0000000..1dc5b44
--- /dev/null
+++ b/android/widget/ImageView.java
@@ -0,0 +1,1653 @@
+/*
+ * 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.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Xfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Displays image resources, for example {@link android.graphics.Bitmap}
+ * or {@link android.graphics.drawable.Drawable} resources.
+ * ImageView is also commonly used to {@link #setImageTintMode(PorterDuff.Mode)
+ * apply tints to an image} and handle {@link #setScaleType(ScaleType) image scaling}.
+ *
+ * <p>
+ * The following XML snippet is a common example of using an ImageView to display an image resource:
+ * </p>
+ * <pre>
+ * <LinearLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent">
+ * <ImageView
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:src="@mipmap/ic_launcher"
+ * />
+ * </LinearLayout>
+ * </pre>
+ *
+ * <p>
+ * To learn more about Drawables, see: <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ * To learn more about working with Bitmaps, see: <a href="{@docRoot}topic/performance/graphics/index.htm">Handling Bitmaps</a>.
+ * </p>
+ *
+ * @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
+ * @attr ref android.R.styleable#ImageView_cropToPadding
+ */
+@RemoteView
+public class ImageView extends View {
+ private static final String LOG_TAG = "ImageView";
+
+ // 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 = null;
+ private boolean mHasColorFilter = false;
+ private Xfermode mXfermode;
+ private int mAlpha = 255;
+ private final int mViewAlphaScale = 256;
+ private boolean mColorMod = false;
+
+ private Drawable mDrawable = null;
+ private BitmapDrawable mRecycleableBitmapDrawable = null;
+ private ColorStateList mDrawableTintList = null;
+ private PorterDuff.Mode mDrawableTintMode = null;
+ private boolean mHasDrawableTint = false;
+ private boolean mHasDrawableTintMode = false;
+
+ 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 final RectF mTempSrc = new RectF();
+ private final RectF mTempDst = new RectF();
+
+ private boolean mCropToPadding;
+
+ private int mBaseline = -1;
+ private boolean mBaselineAlignBottom = false;
+
+ /** Compatibility modes dependent on targetSdkVersion of the app. */
+ private static boolean sCompatDone;
+
+ /** AdjustViewBounds behavior will be in compatibility mode for older apps. */
+ private static boolean sCompatAdjustViewBounds;
+
+ /** Whether to pass Resources when creating the source from a stream. */
+ private static boolean sCompatUseCorrectStreamDensity;
+
+ /** Whether to use pre-Nougat drawable visibility dispatching conditions. */
+ private static boolean sCompatDrawableVisibilityDispatch;
+
+ 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, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ initImageView();
+
+ // ImageView is not important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
+ }
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
+
+ final Drawable d = a.getDrawable(R.styleable.ImageView_src);
+ if (d != null) {
+ setImageDrawable(d);
+ }
+
+ mBaselineAlignBottom = a.getBoolean(R.styleable.ImageView_baselineAlignBottom, false);
+ mBaseline = a.getDimensionPixelSize(R.styleable.ImageView_baseline, -1);
+
+ setAdjustViewBounds(a.getBoolean(R.styleable.ImageView_adjustViewBounds, false));
+ setMaxWidth(a.getDimensionPixelSize(R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
+ setMaxHeight(a.getDimensionPixelSize(R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));
+
+ final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
+ if (index >= 0) {
+ setScaleType(sScaleTypeArray[index]);
+ }
+
+ if (a.hasValue(R.styleable.ImageView_tint)) {
+ mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
+ mHasDrawableTint = true;
+
+ // Prior to L, this attribute would always set a color filter with
+ // blending mode SRC_ATOP. Preserve that default behavior.
+ mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
+ mHasDrawableTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.ImageView_tintMode)) {
+ mDrawableTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
+ mHasDrawableTintMode = true;
+ }
+
+ applyImageTint();
+
+ final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
+ if (alpha != 255) {
+ setImageAlpha(alpha);
+ }
+
+ mCropToPadding = a.getBoolean(
+ R.styleable.ImageView_cropToPadding, false);
+
+ a.recycle();
+
+ //need inflate syntax/reader for matrix
+ }
+
+ private void initImageView() {
+ mMatrix = new Matrix();
+ mScaleType = ScaleType.FIT_CENTER;
+
+ if (!sCompatDone) {
+ final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+ sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
+ sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
+ sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
+ sCompatDone = true;
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable dr) {
+ return mDrawable == dr || super.verifyDrawable(dr);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mDrawable != null) mDrawable.jumpToCurrentState();
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable dr) {
+ if (dr == mDrawable) {
+ if (dr != null) {
+ // update cached drawable dimensions if they've changed
+ final int w = dr.getIntrinsicWidth();
+ final int h = dr.getIntrinsicHeight();
+ if (w != mDrawableWidth || h != mDrawableHeight) {
+ mDrawableWidth = w;
+ mDrawableHeight = h;
+ // updates the matrix, which is dependent on the bounds
+ configureBounds();
+ }
+ }
+ /* 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
+ public boolean hasOverlappingRendering() {
+ return (getBackground() != null && getBackground().getCurrent() != null);
+ }
+
+ /** @hide */
+ @Override
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEventInternal(event);
+
+ final CharSequence contentDescription = getContentDescription();
+ if (!TextUtils.isEmpty(contentDescription)) {
+ event.getText().add(contentDescription);
+ }
+ }
+
+ /**
+ * True when ImageView is adjusting its bounds
+ * to preserve the aspect ratio of its drawable
+ *
+ * @return whether to adjust the bounds of this view
+ * to preserve the original aspect ratio of the drawable
+ *
+ * @see #setAdjustViewBounds(boolean)
+ *
+ * @attr ref android.R.styleable#ImageView_adjustViewBounds
+ */
+ public boolean getAdjustViewBounds() {
+ return mAdjustViewBounds;
+ }
+
+ /**
+ * Set this to true if you want the ImageView to adjust its bounds
+ * to preserve the aspect ratio of its drawable.
+ *
+ * <p><strong>Note:</strong> If the application targets API level 17 or lower,
+ * adjustViewBounds will allow the drawable to shrink the view bounds, but not grow
+ * to fill available measured space in all cases. This is for compatibility with
+ * legacy {@link android.view.View.MeasureSpec MeasureSpec} and
+ * {@link android.widget.RelativeLayout RelativeLayout} behavior.</p>
+ *
+ * @param adjustViewBounds Whether to adjust the bounds of this view
+ * to preserve the original aspect ratio of the drawable.
+ *
+ * @see #getAdjustViewBounds()
+ *
+ * @attr ref android.R.styleable#ImageView_adjustViewBounds
+ */
+ @android.view.RemotableViewMethod
+ public void setAdjustViewBounds(boolean adjustViewBounds) {
+ mAdjustViewBounds = adjustViewBounds;
+ if (adjustViewBounds) {
+ setScaleType(ScaleType.FIT_CENTER);
+ }
+ }
+
+ /**
+ * The maximum width of this view.
+ *
+ * @return The maximum width of this view
+ *
+ * @see #setMaxWidth(int)
+ *
+ * @attr ref android.R.styleable#ImageView_maxWidth
+ */
+ public int getMaxWidth() {
+ return mMaxWidth;
+ }
+
+ /**
+ * An optional argument to supply a maximum width for this view. Only valid if
+ * {@link #setAdjustViewBounds(boolean)} 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(android.widget.ImageView.ScaleType)} to determine how to fit
+ * the image within the bounds.
+ * </p>
+ *
+ * @param maxWidth maximum width for this view
+ *
+ * @see #getMaxWidth()
+ *
+ * @attr ref android.R.styleable#ImageView_maxWidth
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxWidth(int maxWidth) {
+ mMaxWidth = maxWidth;
+ }
+
+ /**
+ * The maximum height of this view.
+ *
+ * @return The maximum height of this view
+ *
+ * @see #setMaxHeight(int)
+ *
+ * @attr ref android.R.styleable#ImageView_maxHeight
+ */
+ public int getMaxHeight() {
+ return mMaxHeight;
+ }
+
+ /**
+ * An optional argument to supply a maximum height for this view. Only valid if
+ * {@link #setAdjustViewBounds(boolean)} 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(android.widget.ImageView.ScaleType)} to determine how to fit
+ * the image within the bounds.
+ * </p>
+ *
+ * @param maxHeight maximum height for this view
+ *
+ * @see #getMaxHeight()
+ *
+ * @attr ref android.R.styleable#ImageView_maxHeight
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxHeight(int maxHeight) {
+ mMaxHeight = maxHeight;
+ }
+
+ /**
+ * Gets the current Drawable, or null if no Drawable has been
+ * assigned.
+ *
+ * @return the view's drawable, or null if no drawable has been
+ * assigned.
+ */
+ public Drawable getDrawable() {
+ if (mDrawable == mRecycleableBitmapDrawable) {
+ // Consider our cached version dirty since app code now has a reference to it
+ mRecycleableBitmapDrawable = null;
+ }
+ return mDrawable;
+ }
+
+ private class ImageDrawableCallback implements Runnable {
+
+ private final Drawable drawable;
+ private final Uri uri;
+ private final int resource;
+
+ ImageDrawableCallback(Drawable drawable, Uri uri, int resource) {
+ this.drawable = drawable;
+ this.uri = uri;
+ this.resource = resource;
+ }
+
+ @Override
+ public void run() {
+ setImageDrawable(drawable);
+ mUri = uri;
+ mResource = resource;
+ }
+ }
+
+ /**
+ * Sets a drawable as the content of this ImageView.
+ * <p class="note">This does Bitmap reading and decoding on the UI
+ * thread, which can cause a latency hiccup. If that's a concern,
+ * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or
+ * {@link #setImageBitmap(android.graphics.Bitmap)} and
+ * {@link android.graphics.BitmapFactory} instead.</p>
+ *
+ * @param resId the resource identifier of the drawable
+ *
+ * @attr ref android.R.styleable#ImageView_src
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setImageResourceAsync")
+ public void setImageResource(@DrawableRes int resId) {
+ // The resource configuration may have changed, so we should always
+ // try to load the resource even if the resId hasn't changed.
+ final int oldWidth = mDrawableWidth;
+ final int oldHeight = mDrawableHeight;
+
+ updateDrawable(null);
+ mResource = resId;
+ mUri = null;
+
+ resolveUri();
+
+ if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+ requestLayout();
+ }
+ invalidate();
+ }
+
+ /** @hide **/
+ public Runnable setImageResourceAsync(@DrawableRes int resId) {
+ Drawable d = null;
+ if (resId != 0) {
+ try {
+ d = getContext().getDrawable(resId);
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Unable to find resource: " + resId, e);
+ resId = 0;
+ }
+ }
+ return new ImageDrawableCallback(d, null, resId);
+ }
+
+ /**
+ * Sets the content of this ImageView to the specified Uri.
+ * Note that you use this method to load images from a local Uri only.
+ * <p/>
+ * To learn how to display images from a remote Uri see: <a href="https://developer.android.com/topic/performance/graphics/index.html">Handling Bitmaps</a>
+ * <p/>
+ * <p class="note">This does Bitmap reading and decoding on the UI
+ * thread, which can cause a latency hiccup. If that's a concern,
+ * consider using {@link #setImageDrawable(Drawable)} or
+ * {@link #setImageBitmap(android.graphics.Bitmap)} and
+ * {@link android.graphics.BitmapFactory} instead.</p>
+ *
+ * <p class="note">On devices running SDK < 24, this method will fail to
+ * apply correct density scaling to images loaded from
+ * {@link ContentResolver#SCHEME_CONTENT content} and
+ * {@link ContentResolver#SCHEME_FILE file} schemes. Applications running
+ * on devices with SDK >= 24 <strong>MUST</strong> specify the
+ * {@code targetSdkVersion} in their manifest as 24 or above for density
+ * scaling to be applied to images loaded from these schemes.</p>
+ *
+ * @param uri the Uri of an image, or {@code null} to clear the content
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setImageURIAsync")
+ public void setImageURI(@Nullable Uri uri) {
+ if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) {
+ updateDrawable(null);
+ mResource = 0;
+ mUri = uri;
+
+ final int oldWidth = mDrawableWidth;
+ final int oldHeight = mDrawableHeight;
+
+ resolveUri();
+
+ if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+ requestLayout();
+ }
+ invalidate();
+ }
+ }
+
+ /** @hide **/
+ public Runnable setImageURIAsync(@Nullable Uri uri) {
+ if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) {
+ Drawable d = uri == null ? null : getDrawableFromUri(uri);
+ if (d == null) {
+ // Do not set the URI if the drawable couldn't be loaded.
+ uri = null;
+ }
+ return new ImageDrawableCallback(d, uri, 0);
+ }
+ return null;
+ }
+
+ /**
+ * Sets a drawable as the content of this ImageView.
+ *
+ * @param drawable the Drawable to set, or {@code null} to clear the
+ * content
+ */
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ if (mDrawable != drawable) {
+ mResource = 0;
+ mUri = null;
+
+ final int oldWidth = mDrawableWidth;
+ final int oldHeight = mDrawableHeight;
+
+ updateDrawable(drawable);
+
+ if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+ requestLayout();
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets the content of this ImageView to the specified Icon.
+ *
+ * <p class="note">Depending on the Icon type, this may do Bitmap reading
+ * and decoding on the UI thread, which can cause UI jank. If that's a
+ * concern, consider using
+ * {@link Icon#loadDrawableAsync(Context, Icon.OnDrawableLoadedListener, Handler)}
+ * and then {@link #setImageDrawable(android.graphics.drawable.Drawable)}
+ * instead.</p>
+ *
+ * @param icon an Icon holding the desired image, or {@code null} to clear
+ * the content
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setImageIconAsync")
+ public void setImageIcon(@Nullable Icon icon) {
+ setImageDrawable(icon == null ? null : icon.loadDrawable(mContext));
+ }
+
+ /** @hide **/
+ public Runnable setImageIconAsync(@Nullable Icon icon) {
+ return new ImageDrawableCallback(icon == null ? null : icon.loadDrawable(mContext), null, 0);
+ }
+
+ /**
+ * Applies a tint to the image drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setImageDrawable(Drawable)} will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ * <p>
+ * <em>Note:</em> The default tint mode used by this setter is NOT
+ * consistent with the default tint mode used by the
+ * {@link android.R.styleable#ImageView_tint android:tint}
+ * attribute. If the {@code android:tint} attribute is specified, the
+ * default tint mode will be set to {@link PorterDuff.Mode#SRC_ATOP} to
+ * ensure consistency with earlier versions of the platform.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#ImageView_tint
+ * @see #getImageTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setImageTintList(@Nullable ColorStateList tint) {
+ mDrawableTintList = tint;
+ mHasDrawableTint = true;
+
+ applyImageTint();
+ }
+
+ /**
+ * Get the current {@link android.content.res.ColorStateList} used to tint the image Drawable,
+ * or null if no tint is applied.
+ *
+ * @return the tint applied to the image drawable
+ * @attr ref android.R.styleable#ImageView_tint
+ * @see #setImageTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getImageTintList() {
+ return mDrawableTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setImageTintList(ColorStateList)}} to the image drawable. The default
+ * mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#ImageView_tintMode
+ * @see #getImageTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mDrawableTintMode = tintMode;
+ mHasDrawableTintMode = true;
+
+ applyImageTint();
+ }
+
+ /**
+ * Gets the blending mode used to apply the tint to the image Drawable
+ * @return the blending mode used to apply the tint to the image Drawable
+ * @attr ref android.R.styleable#ImageView_tintMode
+ * @see #setImageTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getImageTintMode() {
+ return mDrawableTintMode;
+ }
+
+ private void applyImageTint() {
+ if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {
+ mDrawable = mDrawable.mutate();
+
+ if (mHasDrawableTint) {
+ mDrawable.setTintList(mDrawableTintList);
+ }
+
+ if (mHasDrawableTintMode) {
+ mDrawable.setTintMode(mDrawableTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mDrawable.isStateful()) {
+ mDrawable.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * Sets a Bitmap as the content of this ImageView.
+ *
+ * @param bm The bitmap to set
+ */
+ @android.view.RemotableViewMethod
+ public void setImageBitmap(Bitmap bm) {
+ // Hacky fix to force setImageDrawable to do a full setImageDrawable
+ // instead of doing an object reference comparison
+ mDrawable = null;
+ if (mRecycleableBitmapDrawable == null) {
+ mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm);
+ } else {
+ mRecycleableBitmapDrawable.setBitmap(bm);
+ }
+ setImageDrawable(mRecycleableBitmapDrawable);
+ }
+
+ /**
+ * Set the state of the current {@link android.graphics.drawable.StateListDrawable}.
+ * For more information about State List Drawables, see: <a href="https://developer.android.com/guide/topics/resources/drawable-resource.html#StateList">the Drawable Resource Guide</a>.
+ *
+ * @param state the state to set for the StateListDrawable
+ * @param merge if true, merges the state values for the state you specify into the current state
+ */
+ 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();
+ }
+
+ /**
+ * Sets the image level, when it is constructed from a
+ * {@link android.graphics.drawable.LevelListDrawable}.
+ *
+ * @param level The new level for the image.
+ */
+ @android.view.RemotableViewMethod
+ 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();
+ }
+ }
+
+ /**
+ * Returns the current ScaleType that is used to scale the bounds of an image to the bounds of the ImageView.
+ * @return The ScaleType used to scale the image.
+ * @see ImageView.ScaleType
+ * @attr ref android.R.styleable#ImageView_scaleType
+ */
+ public ScaleType getScaleType() {
+ return mScaleType;
+ }
+
+ /** Returns the view's optional matrix. This is applied to the
+ view's drawable when it is drawn. If there is no matrix,
+ this method will return an identity matrix.
+ Do not change this matrix in place but make a copy.
+ If you want a different matrix applied to the drawable,
+ be sure to call setImageMatrix().
+ */
+ public Matrix getImageMatrix() {
+ if (mDrawMatrix == null) {
+ return new Matrix(Matrix.IDENTITY_MATRIX);
+ }
+ return mDrawMatrix;
+ }
+
+ /**
+ * Adds a transformation {@link Matrix} that is applied
+ * to the view's drawable when it is drawn. Allows custom scaling,
+ * translation, and perspective distortion.
+ *
+ * @param matrix The transformation parameters in matrix form.
+ */
+ public void setImageMatrix(Matrix matrix) {
+ // collapse 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);
+ configureBounds();
+ invalidate();
+ }
+ }
+
+ /**
+ * Return whether this ImageView crops to padding.
+ *
+ * @return whether this ImageView crops to padding
+ *
+ * @see #setCropToPadding(boolean)
+ *
+ * @attr ref android.R.styleable#ImageView_cropToPadding
+ */
+ public boolean getCropToPadding() {
+ return mCropToPadding;
+ }
+
+ /**
+ * Sets whether this ImageView will crop to padding.
+ *
+ * @param cropToPadding whether this ImageView will crop to padding
+ *
+ * @see #getCropToPadding()
+ *
+ * @attr ref android.R.styleable#ImageView_cropToPadding
+ */
+ public void setCropToPadding(boolean cropToPadding) {
+ if (mCropToPadding != cropToPadding) {
+ mCropToPadding = cropToPadding;
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ private void resolveUri() {
+ if (mDrawable != null) {
+ return;
+ }
+
+ if (getResources() == null) {
+ return;
+ }
+
+ Drawable d = null;
+
+ if (mResource != 0) {
+ try {
+ d = mContext.getDrawable(mResource);
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
+ // Don't try again.
+ mResource = 0;
+ }
+ } else if (mUri != null) {
+ d = getDrawableFromUri(mUri);
+
+ if (d == null) {
+ Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
+ // Don't try again.
+ mUri = null;
+ }
+ } else {
+ return;
+ }
+
+ updateDrawable(d);
+ }
+
+ private Drawable getDrawableFromUri(Uri uri) {
+ final String scheme = uri.getScheme();
+ if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ try {
+ // Load drawable through Resources, to get the source density information
+ ContentResolver.OpenResourceIdResult r =
+ mContext.getContentResolver().getResourceId(uri);
+ return r.r.getDrawable(r.id, mContext.getTheme());
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Unable to open content: " + uri, e);
+ }
+ } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ InputStream stream = null;
+ try {
+ stream = mContext.getContentResolver().openInputStream(uri);
+ return Drawable.createFromResourceStream(sCompatUseCorrectStreamDensity
+ ? getResources() : null, null, stream, null);
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Unable to open content: " + uri, e);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Unable to close content: " + uri, e);
+ }
+ }
+ }
+ } else {
+ return Drawable.createFromPath(uri.toString());
+ }
+ return null;
+ }
+
+ @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 (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
+ mRecycleableBitmapDrawable.setBitmap(null);
+ }
+
+ boolean sameDrawable = false;
+
+ if (mDrawable != null) {
+ sameDrawable = mDrawable == d;
+ mDrawable.setCallback(null);
+ unscheduleDrawable(mDrawable);
+ if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
+ mDrawable.setVisible(false, false);
+ }
+ }
+
+ mDrawable = d;
+
+ if (d != null) {
+ d.setCallback(this);
+ d.setLayoutDirection(getLayoutDirection());
+ if (d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ if (!sameDrawable || sCompatDrawableVisibilityDispatch) {
+ final boolean visible = sCompatDrawableVisibilityDispatch
+ ? getVisibility() == VISIBLE
+ : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown();
+ d.setVisible(visible, true);
+ }
+ d.setLevel(mLevel);
+ mDrawableWidth = d.getIntrinsicWidth();
+ mDrawableHeight = d.getIntrinsicHeight();
+ applyImageTint();
+ applyColorMod();
+
+ configureBounds();
+ } else {
+ mDrawableWidth = mDrawableHeight = -1;
+ }
+ }
+
+ private void resizeFromDrawable() {
+ final 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();
+ }
+ }
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ if (mDrawable != null) {
+ mDrawable.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ 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;
+
+ final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ 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) {
+ resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
+ resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
+
+ desiredAspect = (float) w / (float) h;
+ }
+ }
+
+ final int pleft = mPaddingLeft;
+ final int pright = mPaddingRight;
+ final int ptop = mPaddingTop;
+ final 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
+ final 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;
+
+ // Allow the width to outgrow its original estimate if height is fixed.
+ if (!resizeHeight && !sCompatAdjustViewBounds) {
+ widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
+ }
+
+ 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;
+
+ // Allow the height to outgrow its original estimate if width is fixed.
+ if (!resizeWidth && !sCompatAdjustViewBounds) {
+ heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
+ heightMeasureSpec);
+ }
+
+ 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 = resolveSizeAndState(w, widthMeasureSpec, 0);
+ heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ private int resolveAdjustedSize(int desiredSize, int maxSize,
+ int measureSpec) {
+ int result = desiredSize;
+ final int specMode = MeasureSpec.getMode(measureSpec);
+ final 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) {
+ final boolean changed = super.setFrame(l, t, r, b);
+ mHaveFrame = true;
+ configureBounds();
+ return changed;
+ }
+
+ private void configureBounds() {
+ if (mDrawable == null || !mHaveFrame) {
+ return;
+ }
+
+ final int dwidth = mDrawableWidth;
+ final int dheight = mDrawableHeight;
+
+ final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
+ final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
+
+ final 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(Math.round((vwidth - dwidth) * 0.5f),
+ Math.round((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(Math.round(dx), Math.round(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 = Math.round((vwidth - dwidth * scale) * 0.5f);
+ dy = Math.round((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();
+
+ final Drawable drawable = mDrawable;
+ if (drawable != null && drawable.isStateful()
+ && drawable.setState(getDrawableState())) {
+ invalidateDrawable(drawable);
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mDrawable != null) {
+ mDrawable.setHotspot(x, y);
+ }
+ }
+
+ /** @hide */
+ public void animateTransform(Matrix matrix) {
+ if (mDrawable == null) {
+ return;
+ }
+ if (matrix == null) {
+ mDrawable.setBounds(0, 0, getWidth(), getHeight());
+ } else {
+ mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight);
+ if (mDrawMatrix == null) {
+ mDrawMatrix = new Matrix();
+ }
+ mDrawMatrix.set(matrix);
+ }
+ invalidate();
+ }
+
+ @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 {
+ final 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);
+ }
+ }
+
+ /**
+ * <p>Return the offset of the widget's text baseline from the widget's top
+ * boundary. </p>
+ *
+ * @return the offset of the baseline within the widget's bounds or -1
+ * if baseline alignment is not supported.
+ */
+ @Override
+ @ViewDebug.ExportedProperty(category = "layout")
+ public int getBaseline() {
+ if (mBaselineAlignBottom) {
+ return getMeasuredHeight();
+ } else {
+ return mBaseline;
+ }
+ }
+
+ /**
+ * <p>Set the offset of the widget's text baseline from the widget's top
+ * boundary. This value is overridden by the {@link #setBaselineAlignBottom(boolean)}
+ * property.</p>
+ *
+ * @param baseline The baseline to use, or -1 if none is to be provided.
+ *
+ * @see #setBaseline(int)
+ * @attr ref android.R.styleable#ImageView_baseline
+ */
+ public void setBaseline(int baseline) {
+ if (mBaseline != baseline) {
+ mBaseline = baseline;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets whether the baseline of this view to the bottom of the view.
+ * Setting this value overrides any calls to setBaseline.
+ *
+ * @param aligned If true, the image view will be baseline aligned by its bottom edge.
+ *
+ * @attr ref android.R.styleable#ImageView_baselineAlignBottom
+ */
+ public void setBaselineAlignBottom(boolean aligned) {
+ if (mBaselineAlignBottom != aligned) {
+ mBaselineAlignBottom = aligned;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Checks whether this view's baseline is considered the bottom of the view.
+ *
+ * @return True if the ImageView's baseline is considered the bottom of the view, false if otherwise.
+ * @see #setBaselineAlignBottom(boolean)
+ */
+ public boolean getBaselineAlignBottom() {
+ return mBaselineAlignBottom;
+ }
+
+ /**
+ * Sets 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));
+ }
+
+ /**
+ * Set a tinting option for the image. Assumes
+ * {@link PorterDuff.Mode#SRC_ATOP} blending mode.
+ *
+ * @param color Color tint to apply.
+ * @attr ref android.R.styleable#ImageView_tint
+ */
+ @RemotableViewMethod
+ public final void setColorFilter(int color) {
+ setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ /**
+ * Removes the image's {@link android.graphics.ColorFilter}.
+ *
+ * @see #setColorFilter(int)
+ * @see #getColorFilter()
+ */
+ public final void clearColorFilter() {
+ setColorFilter(null);
+ }
+
+ /**
+ * @hide Candidate for future API inclusion
+ */
+ public final void setXfermode(Xfermode mode) {
+ if (mXfermode != mode) {
+ mXfermode = mode;
+ mColorMod = true;
+ applyColorMod();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the active color filter for this ImageView.
+ *
+ * @return the active color filter for this ImageView
+ *
+ * @see #setColorFilter(android.graphics.ColorFilter)
+ */
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ /**
+ * Apply an arbitrary colorfilter to the image.
+ *
+ * @param cf the colorfilter to apply (may be null)
+ *
+ * @see #getColorFilter()
+ */
+ public void setColorFilter(ColorFilter cf) {
+ if (mColorFilter != cf) {
+ mColorFilter = cf;
+ mHasColorFilter = true;
+ mColorMod = true;
+ applyColorMod();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the alpha that will be applied to the drawable of this ImageView.
+ *
+ * @return the alpha value that will be applied to the drawable of this
+ * ImageView (between 0 and 255 inclusive, with 0 being transparent and
+ * 255 being opaque)
+ *
+ * @see #setImageAlpha(int)
+ */
+ public int getImageAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Sets the alpha value that should be applied to the image.
+ *
+ * @param alpha the alpha value that should be applied to the image (between
+ * 0 and 255 inclusive, with 0 being transparent and 255 being opaque)
+ *
+ * @see #getImageAlpha()
+ */
+ @RemotableViewMethod
+ public void setImageAlpha(int alpha) {
+ setAlpha(alpha);
+ }
+
+ /**
+ * Sets the alpha value that should be applied to the image.
+ *
+ * @param alpha the alpha value that should be applied to the image
+ *
+ * @deprecated use #setImageAlpha(int) instead
+ */
+ @Deprecated
+ @RemotableViewMethod
+ public void setAlpha(int alpha) {
+ alpha &= 0xFF; // keep it legal
+ if (mAlpha != alpha) {
+ mAlpha = alpha;
+ mColorMod = true;
+ applyColorMod();
+ invalidate();
+ }
+ }
+
+ private void applyColorMod() {
+ // Only mutate and apply when modifications have occurred. This should
+ // not reset the mColorMod flag, since these filters need to be
+ // re-applied if the Drawable is changed.
+ if (mDrawable != null && mColorMod) {
+ mDrawable = mDrawable.mutate();
+ if (mHasColorFilter) {
+ mDrawable.setColorFilter(mColorFilter);
+ }
+ mDrawable.setXfermode(mXfermode);
+ mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);
+ }
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return super.isOpaque() || mDrawable != null && mXfermode == null
+ && mDrawable.getOpacity() == PixelFormat.OPAQUE
+ && mAlpha * mViewAlphaScale >> 8 == 255
+ && isFilledByImage();
+ }
+
+ private boolean isFilledByImage() {
+ if (mDrawable == null) {
+ return false;
+ }
+
+ final Rect bounds = mDrawable.getBounds();
+ final Matrix matrix = mDrawMatrix;
+ if (matrix == null) {
+ return bounds.left <= 0 && bounds.top <= 0 && bounds.right >= getWidth()
+ && bounds.bottom >= getHeight();
+ } else if (matrix.rectStaysRect()) {
+ final RectF boundsSrc = mTempSrc;
+ final RectF boundsDst = mTempDst;
+ boundsSrc.set(bounds);
+ matrix.mapRect(boundsDst, boundsSrc);
+ return boundsDst.left <= 0 && boundsDst.top <= 0 && boundsDst.right >= getWidth()
+ && boundsDst.bottom >= getHeight();
+ } else {
+ // If the matrix doesn't map to a rectangle, assume the worst.
+ return false;
+ }
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+ // Only do this for new apps post-Nougat
+ if (mDrawable != null && !sCompatDrawableVisibilityDispatch) {
+ mDrawable.setVisible(isVisible, false);
+ }
+ }
+
+ @RemotableViewMethod
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+ if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+ mDrawable.setVisible(visibility == VISIBLE, false);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+ if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+ mDrawable.setVisible(getVisibility() == VISIBLE, false);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+ if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+ mDrawable.setVisible(false, false);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ImageView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+ stream.addProperty("layout:baseline", getBaseline());
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public boolean isDefaultFocusHighlightNeeded(Drawable background, Drawable foreground) {
+ final boolean lackFocusState = mDrawable == null || !mDrawable.isStateful()
+ || !mDrawable.hasFocusStateSpecified();
+ return super.isDefaultFocusHighlightNeeded(background, foreground) && lackFocusState;
+ }
+}
diff --git a/android/widget/LayoutPerfTest.java b/android/widget/LayoutPerfTest.java
new file mode 100644
index 0000000..d570ef3
--- /dev/null
+++ b/android/widget/LayoutPerfTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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.os.Looper;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.perftests.core.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static android.perftests.utils.LayoutUtils.gatherViewTree;
+import static android.perftests.utils.LayoutUtils.requestLayoutForAllNodes;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class LayoutPerfTest {
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection measureSpecs() {
+ return Arrays.asList(new Object[][] {
+ { "relative", R.layout.test_relative_layout, R.id.relative_layout_root },
+ { "linear", R.layout.test_linear_layout, R.id.linear_layout_root },
+ { "linear_weighted", R.layout.test_linear_layout_weighted,
+ R.id.linear_layout_weighted_root },
+ });
+ }
+
+ private int[] mMeasureSpecs = {EXACTLY, AT_MOST, UNSPECIFIED};
+
+ private int mLayoutId;
+ private int mViewId;
+
+ public LayoutPerfTest(String key, int layoutId, int viewId) {
+ // key is used in the final report automatically.
+ mLayoutId = layoutId;
+ mViewId = viewId;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule =
+ new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ @UiThreadTest
+ public void testLayoutPerf() throws Throwable {
+ mActivityRule.runOnUiThread(() -> {
+ assertTrue("We should be running on the main thread",
+ Looper.getMainLooper().getThread() == Thread.currentThread());
+ assertTrue("We should be running on the main thread",
+ Looper.myLooper() == Looper.getMainLooper());
+
+ Activity activity = mActivityRule.getActivity();
+ activity.setContentView(mLayoutId);
+
+ ViewGroup viewGroup = (ViewGroup) activity.findViewById(mViewId);
+
+ List<View> allNodes = gatherViewTree(viewGroup);
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ int length = mMeasureSpecs.length;
+ while (state.keepRunning()) {
+ for (int i = 0; i < length; i++) {
+ // The overhead of this call is ignorable, like within 1% difference.
+ requestLayoutForAllNodes(allNodes);
+
+ viewGroup.measure(mMeasureSpecs[i % length], mMeasureSpecs[i % length]);
+ viewGroup.layout(0, 0, viewGroup.getMeasuredWidth(), viewGroup.getMeasuredHeight());
+ }
+ }
+ });
+ }
+}
diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java
new file mode 100644
index 0000000..380bf7a
--- /dev/null
+++ b/android/widget/LinearLayout.java
@@ -0,0 +1,2051 @@
+/*
+ * 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.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * A layout that arranges other views either horizontally in a single column
+ * or vertically in a single row.
+ *
+ * <p>The following snippet shows how to include a linear layout in your layout XML file:</p>
+ *
+ * <pre><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent"
+ * android:paddingLeft="16dp"
+ * android:paddingRight="16dp"
+ * android:orientation="horizontal"
+ * android:gravity="center">
+ *
+ * <!-- Include other widget or layout tags here. These are considered
+ * "child views" or "children" of the linear layout -->
+ *
+ * </LinearLayout></pre>
+ *
+ * <p>Set {@link android.R.styleable#LinearLayout_orientation android:orientation} to specify
+ * whether child views are displayed in a row or column.</p>
+ *
+ * <p>To control how linear layout aligns all the views it contains, set a value for
+ * {@link android.R.styleable#LinearLayout_gravity android:gravity}. For example, the
+ * snippet above sets android:gravity to "center". The value you set affects
+ * both horizontal and vertical alignment of all child views within the single row or column.</p>
+ *
+ * <p>You can set
+ * {@link android.R.styleable#LinearLayout_Layout_layout_weight android:layout_weight}
+ * on individual child views to specify how linear layout divides remaining space amongst
+ * the views it contains. See the
+ * <a href="https://developer.android.com/guide/topics/ui/layout/linear.html">Linear Layout</a>
+ * guide for an example.</p>
+ *
+ * <p>See
+ * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
+ * to learn about other attributes you can set on a child view to affect its
+ * position and size in the containing linear layout.</p>
+ *
+ * @attr ref android.R.styleable#LinearLayout_baselineAligned
+ * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex
+ * @attr ref android.R.styleable#LinearLayout_gravity
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ * @attr ref android.R.styleable#LinearLayout_orientation
+ * @attr ref android.R.styleable#LinearLayout_weightSum
+ */
+@RemoteView
+public class LinearLayout extends ViewGroup {
+ /** @hide */
+ @IntDef({HORIZONTAL, VERTICAL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OrientationMode {}
+
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ SHOW_DIVIDER_NONE,
+ SHOW_DIVIDER_BEGINNING,
+ SHOW_DIVIDER_MIDDLE,
+ SHOW_DIVIDER_END
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DividerMode {}
+
+ /**
+ * Don't show any dividers.
+ */
+ public static final int SHOW_DIVIDER_NONE = 0;
+ /**
+ * Show a divider at the beginning of the group.
+ */
+ public static final int SHOW_DIVIDER_BEGINNING = 1;
+ /**
+ * Show dividers between each item in the group.
+ */
+ public static final int SHOW_DIVIDER_MIDDLE = 2;
+ /**
+ * Show a divider at the end of the group.
+ */
+ public static final int SHOW_DIVIDER_END = 4;
+
+ /**
+ * Compatibility check. Old versions of the platform would give different
+ * results from measurement passes using EXACTLY and non-EXACTLY modes,
+ * even when the resulting size was the same.
+ */
+ private final boolean mAllowInconsistentMeasurement;
+
+ /**
+ * Whether the children of this layout are baseline aligned. Only applicable
+ * if {@link #mOrientation} is horizontal.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ 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.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ private int mBaselineAlignedChildIndex = -1;
+
+ /**
+ * 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.
+ */
+ @ViewDebug.ExportedProperty(category = "measurement")
+ private int mBaselineChildTop = 0;
+
+ @ViewDebug.ExportedProperty(category = "measurement")
+ private int mOrientation;
+
+ @ViewDebug.ExportedProperty(category = "measurement", flagMapping = {
+ @ViewDebug.FlagToString(mask = -1,
+ equals = -1, name = "NONE"),
+ @ViewDebug.FlagToString(mask = Gravity.NO_GRAVITY,
+ equals = Gravity.NO_GRAVITY,name = "NONE"),
+ @ViewDebug.FlagToString(mask = Gravity.TOP,
+ equals = Gravity.TOP, name = "TOP"),
+ @ViewDebug.FlagToString(mask = Gravity.BOTTOM,
+ equals = Gravity.BOTTOM, name = "BOTTOM"),
+ @ViewDebug.FlagToString(mask = Gravity.LEFT,
+ equals = Gravity.LEFT, name = "LEFT"),
+ @ViewDebug.FlagToString(mask = Gravity.RIGHT,
+ equals = Gravity.RIGHT, name = "RIGHT"),
+ @ViewDebug.FlagToString(mask = Gravity.START,
+ equals = Gravity.START, name = "START"),
+ @ViewDebug.FlagToString(mask = Gravity.END,
+ equals = Gravity.END, name = "END"),
+ @ViewDebug.FlagToString(mask = Gravity.CENTER_VERTICAL,
+ equals = Gravity.CENTER_VERTICAL, name = "CENTER_VERTICAL"),
+ @ViewDebug.FlagToString(mask = Gravity.FILL_VERTICAL,
+ equals = Gravity.FILL_VERTICAL, name = "FILL_VERTICAL"),
+ @ViewDebug.FlagToString(mask = Gravity.CENTER_HORIZONTAL,
+ equals = Gravity.CENTER_HORIZONTAL, name = "CENTER_HORIZONTAL"),
+ @ViewDebug.FlagToString(mask = Gravity.FILL_HORIZONTAL,
+ equals = Gravity.FILL_HORIZONTAL, name = "FILL_HORIZONTAL"),
+ @ViewDebug.FlagToString(mask = Gravity.CENTER,
+ equals = Gravity.CENTER, name = "CENTER"),
+ @ViewDebug.FlagToString(mask = Gravity.FILL,
+ equals = Gravity.FILL, name = "FILL"),
+ @ViewDebug.FlagToString(mask = Gravity.RELATIVE_LAYOUT_DIRECTION,
+ equals = Gravity.RELATIVE_LAYOUT_DIRECTION, name = "RELATIVE")
+ }, formatToHexString = true)
+ private int mGravity = Gravity.START | Gravity.TOP;
+
+ @ViewDebug.ExportedProperty(category = "measurement")
+ private int mTotalLength;
+
+ @ViewDebug.ExportedProperty(category = "layout")
+ private float mWeightSum;
+
+ @ViewDebug.ExportedProperty(category = "layout")
+ private boolean mUseLargestChild;
+
+ 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;
+
+ private Drawable mDivider;
+ private int mDividerWidth;
+ private int mDividerHeight;
+ private int mShowDividers;
+ private int mDividerPadding;
+
+ private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
+
+ public LinearLayout(Context context) {
+ this(context, null);
+ }
+
+ public LinearLayout(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
+
+ 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);
+
+ mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false);
+
+ mShowDividers = a.getInt(R.styleable.LinearLayout_showDividers, SHOW_DIVIDER_NONE);
+ mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayout_dividerPadding, 0);
+ setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));
+
+ final int version = context.getApplicationInfo().targetSdkVersion;
+ mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;
+
+ a.recycle();
+ }
+
+ /**
+ * Returns <code>true</code> if this layout is currently configured to show at least one
+ * divider.
+ */
+ private boolean isShowingDividers() {
+ return (mShowDividers != SHOW_DIVIDER_NONE) && (mDivider != null);
+ }
+
+ /**
+ * Set how dividers should be shown between items in this layout
+ *
+ * @param showDividers One or more of {@link #SHOW_DIVIDER_BEGINNING},
+ * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}
+ * to show dividers, or {@link #SHOW_DIVIDER_NONE} to show no dividers.
+ */
+ public void setShowDividers(@DividerMode int showDividers) {
+ if (showDividers == mShowDividers) {
+ return;
+ }
+ mShowDividers = showDividers;
+
+ setWillNotDraw(!isShowingDividers());
+ requestLayout();
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ /**
+ * @return A flag set indicating how dividers should be shown around items.
+ * @see #setShowDividers(int)
+ */
+ @DividerMode
+ public int getShowDividers() {
+ return mShowDividers;
+ }
+
+ /**
+ * @return the divider Drawable that will divide each item.
+ *
+ * @see #setDividerDrawable(Drawable)
+ *
+ * @attr ref android.R.styleable#LinearLayout_divider
+ */
+ public Drawable getDividerDrawable() {
+ return mDivider;
+ }
+
+ /**
+ * Set a drawable to be used as a divider between items.
+ *
+ * @param divider Drawable that will divide each item.
+ *
+ * @see #setShowDividers(int)
+ *
+ * @attr ref android.R.styleable#LinearLayout_divider
+ */
+ public void setDividerDrawable(Drawable divider) {
+ if (divider == mDivider) {
+ return;
+ }
+ mDivider = divider;
+ if (divider != null) {
+ mDividerWidth = divider.getIntrinsicWidth();
+ mDividerHeight = divider.getIntrinsicHeight();
+ } else {
+ mDividerWidth = 0;
+ mDividerHeight = 0;
+ }
+
+ setWillNotDraw(!isShowingDividers());
+ requestLayout();
+ }
+
+ /**
+ * Set padding displayed on both ends of dividers. For a vertical layout, the padding is applied
+ * to left and right end of dividers. For a horizontal layout, the padding is applied to top and
+ * bottom end of dividers.
+ *
+ * @param padding Padding value in pixels that will be applied to each end
+ *
+ * @see #setShowDividers(int)
+ * @see #setDividerDrawable(Drawable)
+ * @see #getDividerPadding()
+ */
+ public void setDividerPadding(int padding) {
+ if (padding == mDividerPadding) {
+ return;
+ }
+ mDividerPadding = padding;
+
+ if (isShowingDividers()) {
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Get the padding size used to inset dividers in pixels
+ *
+ * @see #setShowDividers(int)
+ * @see #setDividerDrawable(Drawable)
+ * @see #setDividerPadding(int)
+ */
+ public int getDividerPadding() {
+ return mDividerPadding;
+ }
+
+ /**
+ * Get the width of the current divider drawable.
+ *
+ * @hide Used internally by framework.
+ */
+ public int getDividerWidth() {
+ return mDividerWidth;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDivider == null) {
+ return;
+ }
+
+ if (mOrientation == VERTICAL) {
+ drawDividersVertical(canvas);
+ } else {
+ drawDividersHorizontal(canvas);
+ }
+ }
+
+ void drawDividersVertical(Canvas canvas) {
+ final int count = getVirtualChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ if (hasDividerBeforeChildAt(i)) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int top = child.getTop() - lp.topMargin - mDividerHeight;
+ drawHorizontalDivider(canvas, top);
+ }
+ }
+ }
+
+ if (hasDividerBeforeChildAt(count)) {
+ final View child = getLastNonGoneChild();
+ int bottom = 0;
+ if (child == null) {
+ bottom = getHeight() - getPaddingBottom() - mDividerHeight;
+ } else {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ bottom = child.getBottom() + lp.bottomMargin;
+ }
+ drawHorizontalDivider(canvas, bottom);
+ }
+ }
+
+ /**
+ * Finds the last child that is not gone. The last child will be used as the reference for
+ * where the end divider should be drawn.
+ */
+ private View getLastNonGoneChild() {
+ for (int i = getVirtualChildCount() - 1; i >= 0; i--) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ void drawDividersHorizontal(Canvas canvas) {
+ final int count = getVirtualChildCount();
+ final boolean isLayoutRtl = isLayoutRtl();
+ for (int i = 0; i < count; i++) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ if (hasDividerBeforeChildAt(i)) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int position;
+ if (isLayoutRtl) {
+ position = child.getRight() + lp.rightMargin;
+ } else {
+ position = child.getLeft() - lp.leftMargin - mDividerWidth;
+ }
+ drawVerticalDivider(canvas, position);
+ }
+ }
+ }
+
+ if (hasDividerBeforeChildAt(count)) {
+ final View child = getLastNonGoneChild();
+ int position;
+ if (child == null) {
+ if (isLayoutRtl) {
+ position = getPaddingLeft();
+ } else {
+ position = getWidth() - getPaddingRight() - mDividerWidth;
+ }
+ } else {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (isLayoutRtl) {
+ position = child.getLeft() - lp.leftMargin - mDividerWidth;
+ } else {
+ position = child.getRight() + lp.rightMargin;
+ }
+ }
+ drawVerticalDivider(canvas, position);
+ }
+ }
+
+ void drawHorizontalDivider(Canvas canvas, int top) {
+ mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
+ getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
+ mDivider.draw(canvas);
+ }
+
+ void drawVerticalDivider(Canvas canvas, int left) {
+ mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
+ left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
+ mDivider.draw(canvas);
+ }
+
+ /**
+ * <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
+ */
+ @android.view.RemotableViewMethod
+ public void setBaselineAligned(boolean baselineAligned) {
+ mBaselineAligned = baselineAligned;
+ }
+
+ /**
+ * When true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * @return True to measure children with a weight using the minimum
+ * size of the largest child, false otherwise.
+ *
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ */
+ public boolean isMeasureWithLargestChildEnabled() {
+ return mUseLargestChild;
+ }
+
+ /**
+ * When set to true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * Disabled by default.
+ *
+ * @param enabled True to measure children with a weight using the
+ * minimum size of the largest child, false otherwise.
+ *
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ */
+ @android.view.RemotableViewMethod
+ public void setMeasureWithLargestChildEnabled(boolean enabled) {
+ mUseLargestChild = enabled;
+ }
+
+ @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
+ */
+ @android.view.RemotableViewMethod
+ 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, may be {@code null}
+ */
+ @Nullable
+ 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
+ */
+ @android.view.RemotableViewMethod
+ 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);
+ }
+ }
+
+ /**
+ * Determines where to position dividers between children.
+ *
+ * @param childIndex Index of child to check for preceding divider
+ * @return true if there should be a divider before the child at childIndex
+ * @hide Pending API consideration. Currently only used internally by the system.
+ */
+ protected boolean hasDividerBeforeChildAt(int childIndex) {
+ if (childIndex == getVirtualChildCount()) {
+ // Check whether the end divider should draw.
+ return (mShowDividers & SHOW_DIVIDER_END) != 0;
+ }
+ boolean allViewsAreGoneBefore = allViewsAreGoneBefore(childIndex);
+ if (allViewsAreGoneBefore) {
+ // This is the first view that's not gone, check if beginning divider is enabled.
+ return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
+ } else {
+ return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0;
+ }
+ }
+
+ /**
+ * Checks whether all (virtual) child views before the given index are gone.
+ */
+ private boolean allViewsAreGoneBefore(int childIndex) {
+ for (int i = childIndex - 1; i >= 0; i--) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 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 childState = 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;
+ boolean skippedMeasure = false;
+
+ final int baselineChildIndex = mBaselineAlignedChildIndex;
+ final boolean useLargestChild = mUseLargestChild;
+
+ int largestChildHeight = Integer.MIN_VALUE;
+ int consumedExcessSpace = 0;
+
+ int nonSkippedChildCount = 0;
+
+ // 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;
+ }
+
+ nonSkippedChildCount++;
+ if (hasDividerBeforeChildAt(i)) {
+ mTotalLength += mDividerHeight;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ totalWeight += lp.weight;
+
+ final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
+ if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
+ // Optimization: don't bother measuring children who are only
+ // laid out using excess space. These views will get measured
+ // later if we have space to distribute.
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
+ skippedMeasure = true;
+ } else {
+ if (useExcessSpace) {
+ // The heightMode is either UNSPECIFIED or AT_MOST, and
+ // this child is only laid out using excess space. Measure
+ // using WRAP_CONTENT so that we can find out the view's
+ // optimal height. We'll restore the original height of 0
+ // after measurement.
+ lp.height = 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).
+ final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
+ measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
+ heightMeasureSpec, usedHeight);
+
+ final int childHeight = child.getMeasuredHeight();
+ if (useExcessSpace) {
+ // Restore the original height and record how much space
+ // we've allocated to excess-only children so that we can
+ // match the behavior of EXACTLY measurement.
+ lp.height = 0;
+ consumedExcessSpace += childHeight;
+ }
+
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
+ lp.bottomMargin + getNextLocationOffset(child));
+
+ if (useLargestChild) {
+ largestChildHeight = Math.max(childHeight, largestChildHeight);
+ }
+ }
+
+ /**
+ * 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.MATCH_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);
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+ allFillParent = allFillParent && lp.width == LayoutParams.MATCH_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);
+ }
+
+ if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
+ mTotalLength += mDividerHeight;
+ }
+
+ if (useLargestChild &&
+ (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
+ mTotalLength = 0;
+
+ 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();
+ // Account for negative margins
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
+ lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
+ }
+ }
+
+ // 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
+ int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
+ heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
+ // Either expand children with weight to take up available space or
+ // shrink them if they extend beyond our current bounds. If we skipped
+ // measurement on any children, we need to measure them now.
+ int remainingExcess = heightSize - mTotalLength
+ + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
+ if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
+ float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
+
+ mTotalLength = 0;
+
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+ if (child == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final float childWeight = lp.weight;
+ if (childWeight > 0) {
+ final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
+ remainingExcess -= share;
+ remainingWeightSum -= childWeight;
+
+ final int childHeight;
+ if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
+ childHeight = largestChildHeight;
+ } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
+ || heightMode == MeasureSpec.EXACTLY)) {
+ // This child needs to be laid out from scratch using
+ // only its share of excess space.
+ childHeight = share;
+ } else {
+ // This child had some intrinsic height to which we
+ // need to add its share of excess space.
+ childHeight = child.getMeasuredHeight() + share;
+ }
+
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, childHeight), MeasureSpec.EXACTLY);
+ final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
+ lp.width);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ // Child may now not fit in vertical dimension.
+ childState = combineMeasuredStates(childState, child.getMeasuredState()
+ & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ 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.MATCH_PARENT;
+
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ matchWidthLocally ? margin : measuredWidth);
+
+ allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
+
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
+ lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
+ }
+
+ // Add in our padding
+ mTotalLength += mPaddingTop + mPaddingBottom;
+ // TODO: Should we recompute the heightSpec based on the new total length?
+ } else {
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ weightedMaxWidth);
+
+
+ // We have no limit, so make all weighted views as tall as the largest child.
+ // Children will have already been measured once.
+ if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
+ 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.measure(
+ MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
+ MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(largestChildHeight,
+ MeasureSpec.EXACTLY));
+ }
+ }
+ }
+ }
+
+ if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
+ maxWidth = alternativeMaxWidth;
+ }
+
+ maxWidth += mPaddingLeft + mPaddingRight;
+
+ // Check against our minimum width
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ heightSizeAndState);
+
+ 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 != null && child.getVisibility() != GONE) {
+ LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
+
+ if (lp.width == LayoutParams.MATCH_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 childState = 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;
+ boolean skippedMeasure = 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;
+ final boolean useLargestChild = mUseLargestChild;
+
+ final boolean isExactly = widthMode == MeasureSpec.EXACTLY;
+
+ int largestChildWidth = Integer.MIN_VALUE;
+ int usedExcessSpace = 0;
+
+ int nonSkippedChildCount = 0;
+
+ // 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;
+ }
+
+ nonSkippedChildCount++;
+ if (hasDividerBeforeChildAt(i)) {
+ mTotalLength += mDividerWidth;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ totalWeight += lp.weight;
+
+ final boolean useExcessSpace = lp.width == 0 && lp.weight > 0;
+ if (widthMode == MeasureSpec.EXACTLY && useExcessSpace) {
+ // Optimization: don't bother measuring children who are only
+ // laid out using excess space. These views will get measured
+ // later if we have space to distribute.
+ if (isExactly) {
+ mTotalLength += lp.leftMargin + lp.rightMargin;
+ } else {
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength +
+ 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 freeWidthSpec = MeasureSpec.makeSafeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
+ final int freeHeightSpec = MeasureSpec.makeSafeMeasureSpec(
+ MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
+ child.measure(freeWidthSpec, freeHeightSpec);
+ } else {
+ skippedMeasure = true;
+ }
+ } else {
+ if (useExcessSpace) {
+ // The widthMode is either UNSPECIFIED or AT_MOST, and
+ // this child is only laid out using excess space. Measure
+ // using WRAP_CONTENT so that we can find out the view's
+ // optimal width. We'll restore the original width of 0
+ // after measurement.
+ 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).
+ final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
+ measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
+ heightMeasureSpec, 0);
+
+ final int childWidth = child.getMeasuredWidth();
+ if (useExcessSpace) {
+ // Restore the original width and record how much space
+ // we've allocated to excess-only children so that we can
+ // match the behavior of EXACTLY measurement.
+ lp.width = 0;
+ usedExcessSpace += childWidth;
+ }
+
+ if (isExactly) {
+ mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
+ + getNextLocationOffset(child);
+ } else {
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
+ + lp.rightMargin + getNextLocationOffset(child));
+ }
+
+ if (useLargestChild) {
+ largestChildWidth = Math.max(childWidth, largestChildWidth);
+ }
+ }
+
+ boolean matchHeightLocally = false;
+ if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.MATCH_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;
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+ 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.MATCH_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);
+ }
+
+ if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
+ mTotalLength += mDividerWidth;
+ }
+
+ // 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);
+ }
+
+ if (useLargestChild &&
+ (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED)) {
+ mTotalLength = 0;
+
+ 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();
+ if (isExactly) {
+ mTotalLength += largestChildWidth + lp.leftMargin + lp.rightMargin +
+ getNextLocationOffset(child);
+ } else {
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + largestChildWidth +
+ lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
+ }
+ }
+ }
+
+ // 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
+ int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
+ widthSize = widthSizeAndState & MEASURED_SIZE_MASK;
+
+ // Either expand children with weight to take up available space or
+ // shrink them if they extend beyond our current bounds. If we skipped
+ // measurement on any children, we need to measure them now.
+ int remainingExcess = widthSize - mTotalLength
+ + (mAllowInconsistentMeasurement ? 0 : usedExcessSpace);
+ if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
+ float remainingWeightSum = 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 LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final float childWeight = lp.weight;
+ if (childWeight > 0) {
+ final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
+ remainingExcess -= share;
+ remainingWeightSum -= childWeight;
+
+ final int childWidth;
+ if (mUseLargestChild && widthMode != MeasureSpec.EXACTLY) {
+ childWidth = largestChildWidth;
+ } else if (lp.width == 0 && (!mAllowInconsistentMeasurement
+ || widthMode == MeasureSpec.EXACTLY)) {
+ // This child needs to be laid out from scratch using
+ // only its share of excess space.
+ childWidth = share;
+ } else {
+ // This child had some intrinsic width to which we
+ // need to add its share of excess space.
+ childWidth = child.getMeasuredWidth() + share;
+ }
+
+ final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, childWidth), MeasureSpec.EXACTLY);
+ final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
+ lp.height);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ // Child may now not fit in horizontal dimension.
+ childState = combineMeasuredStates(childState,
+ child.getMeasuredState() & MEASURED_STATE_MASK);
+ }
+
+ if (isExactly) {
+ mTotalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin +
+ getNextLocationOffset(child);
+ } else {
+ final int totalLength = mTotalLength;
+ mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredWidth() +
+ lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
+ }
+
+ boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY &&
+ lp.height == LayoutParams.MATCH_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.MATCH_PARENT;
+
+ 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;
+ // TODO: Should we update widthSize with the new total length?
+
+ // 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);
+
+ // We have no limit, so make all weighted views as wide as the largest child.
+ // Children will have already been measured once.
+ if (useLargestChild && widthMode != MeasureSpec.EXACTLY) {
+ 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.measure(
+ MeasureSpec.makeMeasureSpec(largestChildWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
+ MeasureSpec.EXACTLY));
+ }
+ }
+ }
+ }
+
+ if (!allFillParent && heightMode != MeasureSpec.EXACTLY) {
+ maxHeight = alternativeMaxHeight;
+ }
+
+ maxHeight += mPaddingTop + mPaddingBottom;
+
+ // Check against our minimum height
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+
+ setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ (childState<<MEASURED_HEIGHT_STATE_SHIFT)));
+
+ 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 accommodate the heightMeasureSpec from the parent
+ int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
+ MeasureSpec.EXACTLY);
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ if (lp.height == LayoutParams.MATCH_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(l, t, r, b);
+ } else {
+ layoutHorizontal(l, t, r, b);
+ }
+ }
+
+ /**
+ * 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)
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ */
+ void layoutVertical(int left, int top, int right, int bottom) {
+ final int paddingLeft = mPaddingLeft;
+
+ int childTop;
+ int childLeft;
+
+ // Where right end of child should go
+ final int width = right - left;
+ 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.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ // mTotalLength contains the padding already
+ childTop = mPaddingTop + bottom - top - mTotalLength;
+ break;
+
+ // mTotalLength contains the padding already
+ case Gravity.CENTER_VERTICAL:
+ childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
+ break;
+
+ case Gravity.TOP:
+ default:
+ childTop = mPaddingTop;
+ 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;
+ }
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ + lp.leftMargin - lp.rightMargin;
+ break;
+
+ case Gravity.RIGHT:
+ childLeft = childRight - childWidth - lp.rightMargin;
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = paddingLeft + lp.leftMargin;
+ break;
+ }
+
+ if (hasDividerBeforeChildAt(i)) {
+ childTop += mDividerHeight;
+ }
+
+ childTop += lp.topMargin;
+ setChildFrame(child, childLeft, childTop + getLocationOffset(child),
+ childWidth, childHeight);
+ childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
+
+ i += getChildrenSkipCount(child, i);
+ }
+ }
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ if (layoutDirection != mLayoutDirection) {
+ mLayoutDirection = layoutDirection;
+ if (mOrientation == HORIZONTAL) {
+ requestLayout();
+ }
+ }
+ }
+
+ /**
+ * 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)
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ */
+ void layoutHorizontal(int left, int top, int right, int bottom) {
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int paddingTop = mPaddingTop;
+
+ int childTop;
+ int childLeft;
+
+ // Where bottom of child should go
+ final int height = bottom - top;
+ int childBottom = height - mPaddingBottom;
+
+ // Space available for child
+ int childSpace = height - paddingTop - mPaddingBottom;
+
+ final int count = getVirtualChildCount();
+
+ final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ final boolean baselineAligned = mBaselineAligned;
+
+ final int[] maxAscent = mMaxAscent;
+ final int[] maxDescent = mMaxDescent;
+
+ final int layoutDirection = getLayoutDirection();
+ switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
+ case Gravity.RIGHT:
+ // mTotalLength contains the padding already
+ childLeft = mPaddingLeft + right - left - mTotalLength;
+ break;
+
+ case Gravity.CENTER_HORIZONTAL:
+ // mTotalLength contains the padding already
+ childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = mPaddingLeft;
+ break;
+ }
+
+ int start = 0;
+ int dir = 1;
+ //In case of RTL, start drawing from the last child.
+ if (isLayoutRtl) {
+ start = count - 1;
+ dir = -1;
+ }
+
+ for (int i = 0; i < count; i++) {
+ final int childIndex = start + dir * i;
+ final View child = getVirtualChildAt(childIndex);
+ if (child == null) {
+ childLeft += measureNullChild(childIndex);
+ } 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.MATCH_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 baseline 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;
+ default:
+ childTop = paddingTop;
+ break;
+ }
+
+ if (hasDividerBeforeChildAt(childIndex)) {
+ childLeft += mDividerWidth;
+ }
+
+ childLeft += lp.leftMargin;
+ setChildFrame(child, childLeft + getLocationOffset(child), childTop,
+ childWidth, childHeight);
+ childLeft += childWidth + lp.rightMargin +
+ getNextLocationOffset(child);
+
+ i += getChildrenSkipCount(child, childIndex);
+ }
+ }
+ }
+
+ 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 {@link #HORIZONTAL} or {@link #VERTICAL}. Default
+ * value is {@link #HORIZONTAL}.
+ *
+ * @attr ref android.R.styleable#LinearLayout_orientation
+ */
+ public void setOrientation(@OrientationMode int orientation) {
+ if (mOrientation != orientation) {
+ mOrientation = orientation;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the current orientation.
+ *
+ * @return either {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ @OrientationMode
+ 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
+ */
+ @android.view.RemotableViewMethod
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.START;
+ }
+
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the current gravity. See {@link android.view.Gravity}
+ *
+ * @return the current gravity.
+ * @see #setGravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ @android.view.RemotableViewMethod
+ public void setHorizontalGravity(int horizontalGravity) {
+ final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ 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#MATCH_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.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+ return null;
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (sPreserveMarginParamsInLayoutParamConversion) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ }
+ }
+ return new LayoutParams(lp);
+ }
+
+
+ // Override to allow type-checking of LayoutParams.
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LinearLayout.LayoutParams;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return LinearLayout.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("layout:baselineAligned", mBaselineAligned);
+ encoder.addProperty("layout:baselineAlignedChildIndex", mBaselineAlignedChildIndex);
+ encoder.addProperty("measurement:baselineChildTop", mBaselineChildTop);
+ encoder.addProperty("measurement:orientation", mOrientation);
+ encoder.addProperty("measurement:gravity", mGravity);
+ encoder.addProperty("measurement:totalLength", mTotalLength);
+ encoder.addProperty("layout:totalLength", mTotalLength);
+ encoder.addProperty("layout:useLargestChild", mUseLargestChild);
+ }
+
+ /**
+ * 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(category = "layout")
+ public float weight;
+
+ /**
+ * Gravity for the view associated with these LayoutParams.
+ *
+ * @see android.view.Gravity
+ */
+ @ViewDebug.ExportedProperty(category = "layout", 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.START, to = "START"),
+ @ViewDebug.IntToString(from = Gravity.END, to = "END"),
+ @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 #MATCH_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #MATCH_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(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height, margin values, weight,
+ * and gravity of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ super(source);
+
+ this.weight = source.weight;
+ this.gravity = source.gravity;
+ }
+
+ @Override
+ public String debug(String output) {
+ return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
+ ", height=" + sizeToString(height) + " weight=" + weight + "}";
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("layout:weight", weight);
+ encoder.addProperty("layout:gravity", gravity);
+ }
+ }
+}
diff --git a/android/widget/ListAdapter.java b/android/widget/ListAdapter.java
new file mode 100644
index 0000000..d8fd1c9
--- /dev/null
+++ b/android/widget/ListAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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 {
+
+ /**
+ * Indicates whether all the items in this adapter are enabled. If the
+ * value returned by this method changes over time, there is no guarantee
+ * it will take effect. If true, it means all items are selectable and
+ * clickable (there is no separator.)
+ *
+ * @return True if all items are enabled, false otherwise.
+ *
+ * @see #isEnabled(int)
+ */
+ 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).
+ *
+ * The result is unspecified if position is invalid. An {@link ArrayIndexOutOfBoundsException}
+ * should be thrown in that case for fast failure.
+ *
+ * @param position Index of the item
+ *
+ * @return True if the item is not a separator
+ *
+ * @see #areAllItemsEnabled()
+ */
+ boolean isEnabled(int position);
+}
diff --git a/android/widget/ListPopupWindow.java b/android/widget/ListPopupWindow.java
new file mode 100644
index 0000000..0d67615
--- /dev/null
+++ b/android/widget/ListPopupWindow.java
@@ -0,0 +1,1339 @@
+/*
+ * Copyright (C) 2010 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.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * A ListPopupWindow anchors itself to a host view and displays a
+ * list of choices.
+ *
+ * <p>ListPopupWindow contains a number of tricky behaviors surrounding
+ * positioning, scrolling parents to fit the dropdown, interacting
+ * sanely with the IME if present, and others.
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class ListPopupWindow implements ShowableListMenu {
+ private static final String TAG = "ListPopupWindow";
+ private static final boolean DEBUG = false;
+
+ /**
+ * This value controls the length of time that the user
+ * must leave a pointer down without scrolling to expand
+ * the autocomplete dropdown list to cover the IME.
+ */
+ private static final int EXPAND_LIST_TIMEOUT = 250;
+
+ private Context mContext;
+ private ListAdapter mAdapter;
+ private DropDownListView mDropDownList;
+
+ private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownHorizontalOffset;
+ private int mDropDownVerticalOffset;
+ private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+ private boolean mDropDownVerticalOffsetSet;
+ private boolean mIsAnimatedFromAnchor = true;
+ private boolean mOverlapAnchor;
+ private boolean mOverlapAnchorSet;
+
+ private int mDropDownGravity = Gravity.NO_GRAVITY;
+
+ private boolean mDropDownAlwaysVisible = false;
+ private boolean mForceIgnoreOutsideTouch = false;
+ int mListItemExpandMaximum = Integer.MAX_VALUE;
+
+ private View mPromptView;
+ private int mPromptPosition = POSITION_PROMPT_ABOVE;
+
+ private DataSetObserver mObserver;
+
+ private View mDropDownAnchorView;
+
+ private Drawable mDropDownListHighlight;
+
+ private AdapterView.OnItemClickListener mItemClickListener;
+ private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+ private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
+ private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
+ private final PopupScrollListener mScrollListener = new PopupScrollListener();
+ private final ListSelectorHider mHideSelector = new ListSelectorHider();
+ private Runnable mShowDropDownRunnable;
+
+ private final Handler mHandler;
+
+ private final Rect mTempRect = new Rect();
+
+ /**
+ * Optional anchor-relative bounds to be used as the transition epicenter.
+ * When {@code null}, the anchor bounds are used as the epicenter.
+ */
+ private Rect mEpicenterBounds;
+
+ private boolean mModal;
+
+ PopupWindow mPopup;
+
+ /**
+ * The provided prompt view should appear above list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_ABOVE = 0;
+
+ /**
+ * The provided prompt view should appear below list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_BELOW = 1;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
+ * If used to specify a popup width, the popup will match the width of the anchor view.
+ * If used to specify a popup height, the popup will fill available space.
+ */
+ public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * If used to specify a popup width, the popup will use the width of its content.
+ */
+ public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: the requirements for the
+ * input method should be based on the focusability of the popup. That is
+ * if it is focusable than it needs to work with the input method, else
+ * it doesn't.
+ */
+ public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed so that the user can also operate
+ * the input method while it is shown.
+ */
+ public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed to use as much space on the
+ * screen as needed, regardless of whether this covers the input method.
+ */
+ public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ */
+ public ListPopupWindow(@NonNull Context context) {
+ this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ */
+ public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Default style attribute to use for popup content.
+ */
+ public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Style attribute to read for default styling of popup content.
+ * @param defStyleRes Style resource ID to use for default styling of popup content.
+ */
+ public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ mContext = context;
+ mHandler = new Handler(context.getMainLooper());
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,
+ defStyleAttr, defStyleRes);
+ mDropDownHorizontalOffset = a.getDimensionPixelOffset(
+ R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0);
+ mDropDownVerticalOffset = a.getDimensionPixelOffset(
+ R.styleable.ListPopupWindow_dropDownVerticalOffset, 0);
+ if (mDropDownVerticalOffset != 0) {
+ mDropDownVerticalOffsetSet = true;
+ }
+ a.recycle();
+
+ mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ }
+
+ /**
+ * Sets the adapter that provides the data and the views to represent the data
+ * in this popup window.
+ *
+ * @param adapter The adapter to use to create this window's content.
+ */
+ public void setAdapter(@Nullable ListAdapter adapter) {
+ if (mObserver == null) {
+ mObserver = new PopupDataSetObserver();
+ } else if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ mAdapter = adapter;
+ if (mAdapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ }
+
+ if (mDropDownList != null) {
+ mDropDownList.setAdapter(mAdapter);
+ }
+ }
+
+ /**
+ * Set where the optional prompt view should appear. The default is
+ * {@link #POSITION_PROMPT_ABOVE}.
+ *
+ * @param position A position constant declaring where the prompt should be displayed.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public void setPromptPosition(int position) {
+ mPromptPosition = position;
+ }
+
+ /**
+ * @return Where the optional prompt view should appear.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public int getPromptPosition() {
+ return mPromptPosition;
+ }
+
+ /**
+ * Set whether this window should be modal when shown.
+ *
+ * <p>If a popup window is modal, it will receive all touch and key input.
+ * If the user touches outside the popup window's content area the popup window
+ * will be dismissed.
+ *
+ * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
+ */
+ public void setModal(boolean modal) {
+ mModal = modal;
+ mPopup.setFocusable(modal);
+ }
+
+ /**
+ * Returns whether the popup window will be modal when shown.
+ *
+ * @return {@code true} if the popup window will be modal, {@code false} otherwise.
+ */
+ public boolean isModal() {
+ return mModal;
+ }
+
+ /**
+ * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+ * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+ * ignore outside touch even when the drop down is not set to always visible.
+ *
+ * @hide Used only by AutoCompleteTextView to handle some internal special cases.
+ */
+ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+ mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
+ }
+
+ /**
+ * Sets whether the drop-down should remain visible under certain conditions.
+ *
+ * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
+ * of the size or content of the list. {@link #getBackground()} will fill any space
+ * that is not used by the list.
+ *
+ * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+ mDropDownAlwaysVisible = dropDownAlwaysVisible;
+ }
+
+ /**
+ * @return Whether the drop-down is visible under special conditions.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public boolean isDropDownAlwaysVisible() {
+ return mDropDownAlwaysVisible;
+ }
+
+ /**
+ * Sets the operating mode for the soft input area.
+ *
+ * @param mode The desired mode, see
+ * {@link android.view.WindowManager.LayoutParams#softInputMode}
+ * for the full list
+ *
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ * @see #getSoftInputMode()
+ */
+ public void setSoftInputMode(int mode) {
+ mPopup.setSoftInputMode(mode);
+ }
+
+ /**
+ * Returns the current value in {@link #setSoftInputMode(int)}.
+ *
+ * @see #setSoftInputMode(int)
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ */
+ public int getSoftInputMode() {
+ return mPopup.getSoftInputMode();
+ }
+
+ /**
+ * Sets a drawable to use as the list item selector.
+ *
+ * @param selector List selector drawable to use in the popup.
+ */
+ public void setListSelector(Drawable selector) {
+ mDropDownListHighlight = selector;
+ }
+
+ /**
+ * @return The background drawable for the popup window.
+ */
+ public @Nullable Drawable getBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * Sets a drawable to be the background for the popup window.
+ *
+ * @param d A drawable to set as the background.
+ */
+ public void setBackgroundDrawable(@Nullable Drawable d) {
+ mPopup.setBackgroundDrawable(d);
+ }
+
+ /**
+ * Set an animation style to use when the popup window is shown or dismissed.
+ *
+ * @param animationStyle Animation style to use.
+ */
+ public void setAnimationStyle(@StyleRes int animationStyle) {
+ mPopup.setAnimationStyle(animationStyle);
+ }
+
+ /**
+ * Returns the animation style that will be used when the popup window is
+ * shown or dismissed.
+ *
+ * @return Animation style that will be used.
+ */
+ public @StyleRes int getAnimationStyle() {
+ return mPopup.getAnimationStyle();
+ }
+
+ /**
+ * Returns the view that will be used to anchor this popup.
+ *
+ * @return The popup's anchor view
+ */
+ public @Nullable View getAnchorView() {
+ return mDropDownAnchorView;
+ }
+
+ /**
+ * Sets the popup's anchor view. This popup will always be positioned relative to
+ * the anchor view when shown.
+ *
+ * @param anchor The view to use as an anchor.
+ */
+ public void setAnchorView(@Nullable View anchor) {
+ mDropDownAnchorView = anchor;
+ }
+
+ /**
+ * @return The horizontal offset of the popup from its anchor in pixels.
+ */
+ public int getHorizontalOffset() {
+ return mDropDownHorizontalOffset;
+ }
+
+ /**
+ * Set the horizontal offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The horizontal offset of the popup from its anchor.
+ */
+ public void setHorizontalOffset(int offset) {
+ mDropDownHorizontalOffset = offset;
+ }
+
+ /**
+ * @return The vertical offset of the popup from its anchor in pixels.
+ */
+ public int getVerticalOffset() {
+ if (!mDropDownVerticalOffsetSet) {
+ return 0;
+ }
+ return mDropDownVerticalOffset;
+ }
+
+ /**
+ * Set the vertical offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The vertical offset of the popup from its anchor.
+ */
+ public void setVerticalOffset(int offset) {
+ mDropDownVerticalOffset = offset;
+ mDropDownVerticalOffsetSet = true;
+ }
+
+ /**
+ * Specifies the anchor-relative bounds of the popup's transition
+ * epicenter.
+ *
+ * @param bounds anchor-relative bounds
+ * @hide
+ */
+ public void setEpicenterBounds(Rect bounds) {
+ mEpicenterBounds = bounds;
+ }
+
+ /**
+ * Set the gravity of the dropdown list. This is commonly used to
+ * set gravity to START or END for alignment with the anchor.
+ *
+ * @param gravity Gravity value to use
+ */
+ public void setDropDownGravity(int gravity) {
+ mDropDownGravity = gravity;
+ }
+
+ /**
+ * @return The width of the popup window in pixels.
+ */
+ public int getWidth() {
+ return mDropDownWidth;
+ }
+
+ /**
+ * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
+ * or {@link #WRAP_CONTENT}.
+ *
+ * @param width Width of the popup window.
+ */
+ public void setWidth(int width) {
+ mDropDownWidth = width;
+ }
+
+ /**
+ * Sets the width of the popup window by the size of its content. The final width may be
+ * larger to accommodate styled window dressing.
+ *
+ * @param width Desired width of content in pixels.
+ */
+ public void setContentWidth(int width) {
+ Drawable popupBackground = mPopup.getBackground();
+ if (popupBackground != null) {
+ popupBackground.getPadding(mTempRect);
+ mDropDownWidth = mTempRect.left + mTempRect.right + width;
+ } else {
+ setWidth(width);
+ }
+ }
+
+ /**
+ * @return The height of the popup window in pixels.
+ */
+ public int getHeight() {
+ return mDropDownHeight;
+ }
+
+ /**
+ * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
+ *
+ * @param height Height of the popup window must be a positive value,
+ * {@link #MATCH_PARENT}, or {@link #WRAP_CONTENT}.
+ *
+ * @throws IllegalArgumentException if height is set to negative value
+ */
+ public void setHeight(int height) {
+ if (height < 0 && ViewGroup.LayoutParams.WRAP_CONTENT != height
+ && ViewGroup.LayoutParams.MATCH_PARENT != height) {
+ if (mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.O) {
+ Log.e(TAG, "Negative value " + height + " passed to ListPopupWindow#setHeight"
+ + " produces undefined results");
+ } else {
+ throw new IllegalArgumentException(
+ "Invalid height. Must be a positive value, MATCH_PARENT, or WRAP_CONTENT.");
+ }
+ }
+ mDropDownHeight = height;
+ }
+
+ /**
+ * Set the layout type for this popup window.
+ * <p>
+ * See {@link WindowManager.LayoutParams#type} for possible values.
+ *
+ * @param layoutType Layout type for this window.
+ *
+ * @see WindowManager.LayoutParams#type
+ */
+ public void setWindowLayoutType(int layoutType) {
+ mDropDownWindowLayoutType = layoutType;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is clicked.
+ *
+ * @param clickListener Listener to register
+ *
+ * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
+ */
+ public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) {
+ mItemClickListener = clickListener;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is selected.
+ *
+ * @param selectedListener Listener to register.
+ *
+ * @see ListView#setOnItemSelectedListener(OnItemSelectedListener)
+ */
+ public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) {
+ mItemSelectedListener = selectedListener;
+ }
+
+ /**
+ * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
+ * is controlled by {@link #setPromptPosition(int)}.
+ *
+ * @param prompt View to use as an informational prompt.
+ */
+ public void setPromptView(@Nullable View prompt) {
+ boolean showing = isShowing();
+ if (showing) {
+ removePromptView();
+ }
+ mPromptView = prompt;
+ if (showing) {
+ show();
+ }
+ }
+
+ /**
+ * Post a {@link #show()} call to the UI thread.
+ */
+ public void postShow() {
+ mHandler.post(mShowDropDownRunnable);
+ }
+
+ /**
+ * Show the popup list. If the list is already showing, this method
+ * will recalculate the popup's size and position.
+ */
+ @Override
+ public void show() {
+ int height = buildDropDown();
+
+ final boolean noInputMethod = isInputMethodNotNeeded();
+ mPopup.setAllowScrollingAnchorParent(!noInputMethod);
+ mPopup.setWindowLayoutType(mDropDownWindowLayoutType);
+
+ if (mPopup.isShowing()) {
+ if (!getAnchorView().isAttachedToWindow()) {
+ //Don't update position if the anchor view is detached from window.
+ return;
+ }
+ final int widthSpec;
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ // The call to PopupWindow's update method below can accept -1 for any
+ // value you do not want to update.
+ widthSpec = -1;
+ } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthSpec = getAnchorView().getWidth();
+ } else {
+ widthSpec = mDropDownWidth;
+ }
+
+ final int heightSpec;
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ // The call to PopupWindow's update method below can accept -1 for any
+ // value you do not want to update.
+ heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
+ if (noInputMethod) {
+ mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0);
+ mPopup.setHeight(0);
+ } else {
+ mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0);
+ mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightSpec = height;
+ } else {
+ heightSpec = mDropDownHeight;
+ }
+
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+
+ mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
+ mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec,
+ (heightSpec < 0)? -1 : heightSpec);
+ } else {
+ final int widthSpec;
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthSpec = getAnchorView().getWidth();
+ } else {
+ widthSpec = mDropDownWidth;
+ }
+ }
+
+ final int heightSpec;
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightSpec = height;
+ } else {
+ heightSpec = mDropDownHeight;
+ }
+ }
+
+ mPopup.setWidth(widthSpec);
+ mPopup.setHeight(heightSpec);
+ mPopup.setClipToScreenEnabled(true);
+
+ // use outside touchable to dismiss drop down when touching outside of it, so
+ // only set this if the dropdown is not always visible
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+ mPopup.setTouchInterceptor(mTouchInterceptor);
+ mPopup.setEpicenterBounds(mEpicenterBounds);
+ if (mOverlapAnchorSet) {
+ mPopup.setOverlapAnchor(mOverlapAnchor);
+ }
+ mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset,
+ mDropDownVerticalOffset, mDropDownGravity);
+ mDropDownList.setSelection(ListView.INVALID_POSITION);
+
+ if (!mModal || mDropDownList.isInTouchMode()) {
+ clearListSelection();
+ }
+ if (!mModal) {
+ mHandler.post(mHideSelector);
+ }
+ }
+ }
+
+ /**
+ * Dismiss the popup window.
+ */
+ @Override
+ public void dismiss() {
+ mPopup.dismiss();
+ removePromptView();
+ mPopup.setContentView(null);
+ mDropDownList = null;
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ }
+
+ /**
+ * Set a listener to receive a callback when the popup is dismissed.
+ *
+ * @param listener Listener that will be notified when the popup is dismissed.
+ */
+ public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) {
+ mPopup.setOnDismissListener(listener);
+ }
+
+ private void removePromptView() {
+ if (mPromptView != null) {
+ final ViewParent parent = mPromptView.getParent();
+ if (parent instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) parent;
+ group.removeView(mPromptView);
+ }
+ }
+ }
+
+ /**
+ * Control how the popup operates with an input method: one of
+ * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+ * or {@link #INPUT_METHOD_NOT_NEEDED}.
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to the {@link #show()}
+ * method.</p>
+ *
+ * @see #getInputMethodMode()
+ * @see #show()
+ */
+ public void setInputMethodMode(int mode) {
+ mPopup.setInputMethodMode(mode);
+ }
+
+ /**
+ * Return the current value in {@link #setInputMethodMode(int)}.
+ *
+ * @see #setInputMethodMode(int)
+ */
+ public int getInputMethodMode() {
+ return mPopup.getInputMethodMode();
+ }
+
+ /**
+ * Set the selected position of the list.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ *
+ * @param position List position to set as selected.
+ */
+ public void setSelection(int position) {
+ DropDownListView list = mDropDownList;
+ if (isShowing() && list != null) {
+ list.setListSelectionHidden(false);
+ list.setSelection(position);
+ if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
+ list.setItemChecked(position, true);
+ }
+ }
+ }
+
+ /**
+ * Clear any current list selection.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ public void clearListSelection() {
+ final DropDownListView list = mDropDownList;
+ if (list != null) {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ list.setListSelectionHidden(true);
+ list.hideSelector();
+ list.requestLayout();
+ }
+ }
+
+ /**
+ * @return {@code true} if the popup is currently showing, {@code false} otherwise.
+ */
+ @Override
+ public boolean isShowing() {
+ return mPopup.isShowing();
+ }
+
+ /**
+ * @return {@code true} if this popup is configured to assume the user does not need
+ * to interact with the IME while it is showing, {@code false} otherwise.
+ */
+ public boolean isInputMethodNotNeeded() {
+ return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
+ }
+
+ /**
+ * Perform an item click operation on the specified list adapter position.
+ *
+ * @param position Adapter position for performing the click
+ * @return true if the click action could be performed, false if not.
+ * (e.g. if the popup was not showing, this method would return false.)
+ */
+ public boolean performItemClick(int position) {
+ if (isShowing()) {
+ if (mItemClickListener != null) {
+ final DropDownListView list = mDropDownList;
+ final View child = list.getChildAt(position - list.getFirstVisiblePosition());
+ final ListAdapter adapter = list.getAdapter();
+ mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return The currently selected item or null if the popup is not showing.
+ */
+ public @Nullable Object getSelectedItem() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedItem();
+ }
+
+ /**
+ * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getSelectedItemPosition() {
+ if (!isShowing()) {
+ return ListView.INVALID_POSITION;
+ }
+ return mDropDownList.getSelectedItemPosition();
+ }
+
+ /**
+ * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemId()
+ */
+ public long getSelectedItemId() {
+ if (!isShowing()) {
+ return ListView.INVALID_ROW_ID;
+ }
+ return mDropDownList.getSelectedItemId();
+ }
+
+ /**
+ * @return The View for the currently selected item or null if
+ * {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedView()
+ */
+ public @Nullable View getSelectedView() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedView();
+ }
+
+ /**
+ * @return The {@link ListView} displayed within the popup window.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ @Override
+ public @Nullable ListView getListView() {
+ return mDropDownList;
+ }
+
+ @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
+ return new DropDownListView(context, hijackFocus);
+ }
+
+ /**
+ * The maximum number of list items that can be visible and still have
+ * the list expand when touched.
+ *
+ * @param max Max number of items that can be visible and still allow the list to expand.
+ */
+ void setListItemExpandMax(int max) {
+ mListItemExpandMaximum = max;
+ }
+
+ /**
+ * Filter key down events. By forwarding key down events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyDown
+ * @param event event param passed to the host view's onKeyDown
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ * @see #onKeyUp(int, KeyEvent)
+ */
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ // when the drop down is shown, we drive it directly
+ if (isShowing()) {
+ // 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
+ // also if selection is not currently in the drop down, then don't
+ // let center or enter presses go there since that would cause it
+ // to select one of its items
+ if (keyCode != KeyEvent.KEYCODE_SPACE
+ && (mDropDownList.getSelectedItemPosition() >= 0
+ || !KeyEvent.isConfirmKey(keyCode))) {
+ int curIndex = mDropDownList.getSelectedItemPosition();
+ boolean consumed;
+
+ final boolean below = !mPopup.isAboveAnchor();
+
+ final ListAdapter adapter = mAdapter;
+
+ boolean allEnabled;
+ int firstItem = Integer.MAX_VALUE;
+ int lastItem = Integer.MIN_VALUE;
+
+ if (adapter != null) {
+ allEnabled = adapter.areAllItemsEnabled();
+ firstItem = allEnabled ? 0 :
+ mDropDownList.lookForSelectablePosition(0, true);
+ lastItem = allEnabled ? adapter.getCount() - 1 :
+ mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
+ }
+
+ if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
+ (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
+ // When the selection is at the top, we block the key
+ // event to prevent focus from moving.
+ clearListSelection();
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ show();
+ return true;
+ } else {
+ // WARNING: Please read the comment where mListSelectionHidden
+ // is declared
+ mDropDownList.setListSelectionHidden(false);
+ }
+
+ consumed = mDropDownList.onKeyDown(keyCode, event);
+ if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
+
+ if (consumed) {
+ // If it handled the key event, then the user is
+ // navigating in the list, so we should put it in front.
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ // Here's a little trick we need to do to make sure that
+ // the list view is actually showing its focus indicator,
+ // by ensuring it has focus and getting its window out
+ // of touch mode.
+ mDropDownList.requestFocusFromTouch();
+ show();
+
+ 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 {
+ if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // when the selection is at the bottom, we block the
+ // event to avoid going to the next focusable widget
+ if (curIndex == lastItem) {
+ return true;
+ }
+ } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
+ curIndex == firstItem) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Filter key up events. By forwarding key up events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyUp
+ * @param event event param passed to the host view's onKeyUp
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ * @see #onKeyDown(int, KeyEvent)
+ */
+ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+ if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
+ boolean consumed = mDropDownList.onKeyUp(keyCode, event);
+ if (consumed && KeyEvent.isConfirmKey(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
+ dismiss();
+ }
+ return consumed;
+ }
+ return false;
+ }
+
+ /**
+ * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
+ * events to this function, views using ListPopupWindow can have it dismiss the popup
+ * when the back key is pressed.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyPreIme
+ * @param event event param passed to the host view's onKeyPreIme
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
+ // special case for the back key, we do not even try to send it
+ // to the drop down list but instead, consume it immediately
+ final View anchorView = mDropDownAnchorView;
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+ if (state != null) {
+ state.handleUpEvent(event);
+ }
+ if (event.isTracking() && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns an {@link OnTouchListener} that can be added to the source view
+ * to implement drag-to-open behavior. Generally, the source view should be
+ * the same view that was passed to {@link #setAnchorView}.
+ * <p>
+ * When the listener is set on a view, touching that view and dragging
+ * outside of its bounds will open the popup window. Lifting will select the
+ * currently touched list item.
+ * <p>
+ * Example usage:
+ * <pre>
+ * ListPopupWindow myPopup = new ListPopupWindow(context);
+ * myPopup.setAnchor(myAnchor);
+ * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
+ * myAnchor.setOnTouchListener(dragListener);
+ * </pre>
+ *
+ * @param src the view on which the resulting listener will be set
+ * @return a touch listener that controls drag-to-open behavior
+ */
+ public OnTouchListener createDragToOpenListener(View src) {
+ return new ForwardingListener(src) {
+ @Override
+ public ShowableListMenu getPopup() {
+ return ListPopupWindow.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 = mContext;
+
+ /**
+ * This Runnable exists for the sole purpose of checking if the view layout has got
+ * completed and if so call showDropDown to display the drop down. This is used to show
+ * the drop down as soon as possible after user opens up the search dialog, without
+ * waiting for the normal UI pipeline to do it's job which is slower than this method.
+ */
+ mShowDropDownRunnable = new Runnable() {
+ public void run() {
+ // View layout should be all done before displaying the drop down.
+ View view = getAnchorView();
+ if (view != null && view.getWindowToken() != null) {
+ show();
+ }
+ }
+ };
+
+ mDropDownList = createDropDownListView(context, !mModal);
+ if (mDropDownListHighlight != null) {
+ mDropDownList.setSelector(mDropDownListHighlight);
+ }
+ mDropDownList.setAdapter(mAdapter);
+ mDropDownList.setOnItemClickListener(mItemClickListener);
+ mDropDownList.setFocusable(true);
+ mDropDownList.setFocusableInTouchMode(true);
+ mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> parent, View view,
+ int position, long id) {
+
+ if (position != -1) {
+ DropDownListView dropDownList = mDropDownList;
+
+ if (dropDownList != null) {
+ dropDownList.setListSelectionHidden(false);
+ }
+ }
+ }
+
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ mDropDownList.setOnScrollListener(mScrollListener);
+
+ if (mItemSelectedListener != null) {
+ mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
+ }
+
+ dropDownView = mDropDownList;
+
+ View hintView = mPromptView;
+ if (hintView != null) {
+ // if a 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.MATCH_PARENT, 0, 1.0f
+ );
+
+ switch (mPromptPosition) {
+ case POSITION_PROMPT_BELOW:
+ hintContainer.addView(dropDownView, hintParams);
+ hintContainer.addView(hintView);
+ break;
+
+ case POSITION_PROMPT_ABOVE:
+ hintContainer.addView(hintView);
+ hintContainer.addView(dropDownView, hintParams);
+ break;
+
+ default:
+ Log.e(TAG, "Invalid hint position " + mPromptPosition);
+ break;
+ }
+
+ // Measure the hint's height to find how much more vertical
+ // space we need to add to the drop down's height.
+ final int widthSize;
+ final int widthMode;
+ if (mDropDownWidth >= 0) {
+ widthMode = MeasureSpec.AT_MOST;
+ widthSize = mDropDownWidth;
+ } else {
+ widthMode = MeasureSpec.UNSPECIFIED;
+ widthSize = 0;
+ }
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+ final 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 {
+ final View view = mPromptView;
+ if (view != null) {
+ LinearLayout.LayoutParams hintParams =
+ (LinearLayout.LayoutParams) view.getLayoutParams();
+ otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+ }
+ }
+
+ // getMaxAvailableHeight() subtracts the padding, so we put it back
+ // to get the available height for the whole window.
+ final int padding;
+ final Drawable background = mPopup.getBackground();
+ if (background != null) {
+ background.getPadding(mTempRect);
+ padding = mTempRect.top + mTempRect.bottom;
+
+ // If we don't have an explicit vertical offset, determine one from
+ // the window background so that content will line up.
+ if (!mDropDownVerticalOffsetSet) {
+ mDropDownVerticalOffset = -mTempRect.top;
+ }
+ } else {
+ mTempRect.setEmpty();
+ padding = 0;
+ }
+
+ // Max height available on the screen for a popup.
+ final boolean ignoreBottomDecorations =
+ mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
+ final int maxHeight = mPopup.getMaxAvailableHeight(
+ getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
+ if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ return maxHeight + padding;
+ }
+
+ final int childWidthSpec;
+ switch (mDropDownWidth) {
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(
+ mContext.getResources().getDisplayMetrics().widthPixels
+ - (mTempRect.left + mTempRect.right),
+ MeasureSpec.AT_MOST);
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(
+ mContext.getResources().getDisplayMetrics().widthPixels
+ - (mTempRect.left + mTempRect.right),
+ MeasureSpec.EXACTLY);
+ break;
+ default:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
+ break;
+ }
+
+ // Add padding only if the list has items in it, that way we don't show
+ // the popup if it is not needed.
+ final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
+ 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1);
+ if (listContent > 0) {
+ final int listPadding = mDropDownList.getPaddingTop()
+ + mDropDownList.getPaddingBottom();
+ otherHeights += padding + listPadding;
+ }
+
+ return listContent + otherHeights;
+ }
+
+ /**
+ * @hide
+ */
+ public void setOverlapAnchor(boolean overlap) {
+ mOverlapAnchorSet = true;
+ mOverlapAnchor = overlap;
+ }
+
+ private class PopupDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ if (isShowing()) {
+ // Resize the popup to fit new content
+ show();
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ dismiss();
+ }
+ }
+
+ private class ListSelectorHider implements Runnable {
+ public void run() {
+ clearListSelection();
+ }
+ }
+
+ private class ResizePopupRunnable implements Runnable {
+ public void run() {
+ if (mDropDownList != null && mDropDownList.isAttachedToWindow()
+ && mDropDownList.getCount() > mDropDownList.getChildCount()
+ && mDropDownList.getChildCount() <= mListItemExpandMaximum) {
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ show();
+ }
+ }
+ }
+
+ private class PopupTouchInterceptor implements OnTouchListener {
+ public boolean onTouch(View v, MotionEvent event) {
+ final int action = event.getAction();
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ if (action == MotionEvent.ACTION_DOWN &&
+ mPopup != null && mPopup.isShowing() &&
+ (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
+ mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
+ } else if (action == MotionEvent.ACTION_UP) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ }
+ return false;
+ }
+ }
+
+ private class PopupScrollListener implements ListView.OnScrollListener {
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
+ !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ mResizePopupRunnable.run();
+ }
+ }
+ }
+}
diff --git a/android/widget/ListView.java b/android/widget/ListView.java
new file mode 100644
index 0000000..fc9e8e7
--- /dev/null
+++ b/android/widget/ListView.java
@@ -0,0 +1,4155 @@
+/*
+ * 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.annotation.IdRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.SparseBooleanArray;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/*
+ * Implementation Notes:
+ *
+ * Some terminology:
+ *
+ * index - index of the items that are currently visible
+ * position - index of the items in the cursor
+ */
+
+
+/**
+ * <p>Displays a vertically-scrollable collection of views, where each view is positioned
+ * immediatelybelow the previous view in the list. For a more modern, flexible, and performant
+ * approach to displaying lists, use {@link android.support.v7.widget.RecyclerView}.</p>
+ *
+ * <p>To display a list, you can include a list view in your layout XML file:</p>
+ *
+ * <pre><ListView
+ * android:id="@+id/list_view"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent" /></pre>
+ *
+ * <p>A list view is an <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews">
+ * adapter view</a> that does not know the details, such as type and contents, of the views it
+ * contains. Instead list view requests views on demand from a {@link ListAdapter} as needed,
+ * such as to display new views as the user scrolls up or down.</p>
+ *
+ * <p>In order to display items in the list, call {@link #setAdapter(ListAdapter adapter)}
+ * to associate an adapter with the list. For a simple example, see the discussion of filling an
+ * adapter view with text in the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#FillingTheLayout">
+ * Layouts</a> guide.</p>
+ *
+ * <p>To display a more custom view for each item in your dataset, implement a ListAdapter.
+ * For example, extend {@link BaseAdapter} and create and configure the view for each data item in
+ * {@code getView(...)}:</p>
+ *
+ * <pre>private class MyAdapter extends BaseAdapter {
+ *
+ * // override other abstract methods here
+ *
+ * @Override
+ * public View getView(int position, View convertView, ViewGroup container) {
+ * if (convertView == null) {
+ * convertView = getLayoutInflater().inflate(R.layout.list_item, container, false);
+ * }
+ *
+ * ((TextView) convertView.findViewById(android.R.id.text1))
+ * .setText(getItem(position));
+ * return convertView;
+ * }
+ * }</pre>
+ *
+ * <p class="note">ListView attempts to reuse view objects in order to improve performance and
+ * avoid a lag in response to user scrolls. To take advantage of this feature, check if the
+ * {@code convertView} provided to {@code getView(...)} is null before creating or inflating a new
+ * view object. See
+ * <a href="{@docRoot}training/improving-layouts/smooth-scrolling.html">
+ * Making ListView Scrolling Smooth</a> for more ways to ensure a smooth user experience.</p>
+ *
+ * <p>For a more complete example of creating a custom adapter, see the
+ * <a href="{@docRoot}samples/CustomChoiceList/index.html">
+ * Custom Choice List</a> sample app.</p>
+ *
+ * <p>To specify an action when a user clicks or taps on a single list item, see
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#HandlingUserSelections">
+ * Handling click events</a>.</p>
+ *
+ * <p>To learn how to populate a list view with a CursorAdapter, see the discussion of filling an
+ * adapter view with text in the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#FillingTheLayout">
+ * Layouts</a> guide.
+ * See <a href="{@docRoot}guide/topics/ui/layout/listview.html">
+ * Using a Loader</a>
+ * to learn how to avoid blocking the main thread when using a cursor.</p>
+ *
+ * <p class="note">Note, many examples use {@link android.app.ListActivity ListActivity}
+ * or {@link android.app.ListFragment ListFragment}
+ * to display a list view. Instead, favor the more flexible approach when writing your own app:
+ * use a more generic Activity subclass or Fragment subclass and add a list view to the layout
+ * or view hierarchy directly. This approach gives you more direct control of the
+ * list view and adapter.</p>
+ *
+ * @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_headerDividersEnabled
+ * @attr ref android.R.styleable#ListView_footerDividersEnabled
+ */
+@RemoteView
+public class ListView extends AbsListView {
+ static final String TAG = "ListView";
+
+ /**
+ * Used to indicate a no preference for a position type.
+ */
+ static final int NO_POSITION = -1;
+
+ /**
+ * When arrow scrolling, ListView will never scroll more than this factor
+ * times the height of the list.
+ */
+ private static final float MAX_SCROLL_FACTOR = 0.33f;
+
+ /**
+ * 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;
+
+ /**
+ * A class that represents a fixed view in a list, for example a header at the top
+ * or a footer at the bottom.
+ */
+ public class FixedViewInfo {
+ /** The view to add to the list */
+ public View view;
+ /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+ public Object data;
+ /** <code>true</code> if the fixed view should be selectable in the list */
+ public boolean isSelectable;
+ }
+
+ ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
+ ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
+
+ Drawable mDivider;
+ int mDividerHeight;
+
+ Drawable mOverScrollHeader;
+ Drawable mOverScrollFooter;
+
+ private boolean mIsCacheColorOpaque;
+ private boolean mDividerIsOpaque;
+
+ private boolean mHeaderDividersEnabled;
+ private boolean mFooterDividersEnabled;
+
+ private boolean mAreAllItemsSelectable = true;
+
+ private boolean mItemsCanFocus = false;
+
+ // used for temporary calculations.
+ private final Rect mTempRect = new Rect();
+ private Paint mDividerPaint;
+
+ // the single allocated result per list view; kinda cheesey but avoids
+ // allocating these thingies too often.
+ private final ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult();
+
+ // Keeps focused children visible through resizes
+ private FocusSelector mFocusSelector;
+
+ public ListView(Context context) {
+ this(context, null);
+ }
+
+ public ListView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.listViewStyle);
+ }
+
+ public ListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ListView, defStyleAttr, defStyleRes);
+
+ final CharSequence[] entries = a.getTextArray(R.styleable.ListView_entries);
+ if (entries != null) {
+ setAdapter(new ArrayAdapter<>(context, R.layout.simple_list_item_1, entries));
+ }
+
+ final Drawable d = a.getDrawable(R.styleable.ListView_divider);
+ if (d != null) {
+ // Use an implicit divider height which may be explicitly
+ // overridden by android:dividerHeight further down.
+ setDivider(d);
+ }
+
+ final Drawable osHeader = a.getDrawable(R.styleable.ListView_overScrollHeader);
+ if (osHeader != null) {
+ setOverscrollHeader(osHeader);
+ }
+
+ final Drawable osFooter = a.getDrawable(R.styleable.ListView_overScrollFooter);
+ if (osFooter != null) {
+ setOverscrollFooter(osFooter);
+ }
+
+ // Use an explicit divider height, if specified.
+ if (a.hasValueOrEmpty(R.styleable.ListView_dividerHeight)) {
+ final int dividerHeight = a.getDimensionPixelSize(
+ R.styleable.ListView_dividerHeight, 0);
+ if (dividerHeight != 0) {
+ setDividerHeight(dividerHeight);
+ }
+ }
+
+ mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true);
+ mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true);
+
+ a.recycle();
+ }
+
+ /**
+ * @return The maximum amount a list view will scroll in response to
+ * an arrow event.
+ */
+ public int getMaxScrollAmount() {
+ return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
+ }
+
+ /**
+ * 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 this method 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: When first introduced, this method could only be called before
+ * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+ * called at any time. If the ListView's adapter does not extend
+ * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+ * instance of {@link WrapperListAdapter}.
+ *
+ * @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 (v.getParent() != null && v.getParent() != this) {
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "The specified child already has a parent. "
+ + "You must call removeView() on the child's parent first.");
+ }
+ }
+ final FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mHeaderViewInfos.add(info);
+ mAreAllItemsSelectable &= isSelectable;
+
+ // Wrap the adapter if it wasn't already wrapped.
+ if (mAdapter != null) {
+ if (!(mAdapter instanceof HeaderViewListAdapter)) {
+ wrapHeaderListAdapterInternal();
+ }
+
+ // In the case of re-adding a header 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 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: When first introduced, this method could only be called before
+ * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+ * called at any time. If the ListView's adapter does not extend
+ * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+ * instance of {@link WrapperListAdapter}.
+ *
+ * @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 (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
+ if (mDataSetObserver != null) {
+ 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: When first introduced, this method could only be called before
+ * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+ * called at any time. If the ListView's adapter does not extend
+ * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+ * instance of {@link WrapperListAdapter}.
+ *
+ * @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) {
+ if (v.getParent() != null && v.getParent() != this) {
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "The specified child already has a parent. "
+ + "You must call removeView() on the child's parent first.");
+ }
+ }
+
+ final FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mFooterViewInfos.add(info);
+ mAreAllItemsSelectable &= isSelectable;
+
+ // Wrap the adapter if it wasn't already wrapped.
+ if (mAdapter != null) {
+ if (!(mAdapter instanceof HeaderViewListAdapter)) {
+ wrapHeaderListAdapterInternal();
+ }
+
+ // 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: When first introduced, this method could only be called before
+ * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+ * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+ * called at any time. If the ListView's adapter does not extend
+ * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+ * instance of {@link WrapperListAdapter}.
+ *
+ * @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 (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
+ if (mDataSetObserver != null) {
+ 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 up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+ * through the specified intent.
+ * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+ */
+ @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+ public void setRemoteViewsAdapter(Intent intent) {
+ super.setRemoteViewsAdapter(intent);
+ }
+
+ /**
+ * 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 (mAdapter != null && mDataSetObserver != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ resetList();
+ mRecycler.clear();
+
+ if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
+ mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
+ } else {
+ mAdapter = adapter;
+ }
+
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+
+ // AbsListView#setAdapter will update choice mode states.
+ super.setAdapter(adapter);
+
+ if (mAdapter != null) {
+ mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ checkFocus();
+
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
+
+ int position;
+ if (mStackFromBottom) {
+ position = lookForSelectablePosition(mItemCount - 1, false);
+ } else {
+ position = lookForSelectablePosition(0, true);
+ }
+ setSelectedPositionInt(position);
+ setNextSelectedPositionInt(position);
+
+ if (mItemCount == 0) {
+ // Nothing selected
+ checkSelectionChanged();
+ }
+ } else {
+ mAreAllItemsSelectable = true;
+ checkFocus();
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ requestLayout();
+ }
+
+ /**
+ * The list is empty. Clear everything out.
+ */
+ @Override
+ void resetList() {
+ // The parent's resetList() will remove all views from the layout so we need to
+ // cleanup the state of our footers and headers
+ clearRecycledState(mHeaderViewInfos);
+ clearRecycledState(mFooterViewInfos);
+
+ super.resetList();
+
+ mLayoutMode = LAYOUT_NORMAL;
+ }
+
+ private void clearRecycledState(ArrayList<FixedViewInfo> infos) {
+ if (infos != null) {
+ final int count = infos.size();
+
+ for (int i = 0; i < count; i++) {
+ final View child = infos.get(i).view;
+ final ViewGroup.LayoutParams params = child.getLayoutParams();
+ if (checkLayoutParams(params)) {
+ ((LayoutParams) params).recycledHeaderFooter = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * @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(INVALID_POSITION, child);
+ mSelectedTop = child.getTop();
+ invalidate();
+ }
+ return scroll;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void fillGap(boolean down) {
+ final int count = getChildCount();
+ if (down) {
+ int paddingTop = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ paddingTop = getListPaddingTop();
+ }
+ final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
+ paddingTop;
+ fillDown(mFirstPosition + count, startOffset);
+ correctTooHigh(getChildCount());
+ } else {
+ int paddingBottom = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ paddingBottom = getListPaddingBottom();
+ }
+ final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
+ getHeight() - paddingBottom;
+ 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);
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ end -= 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++;
+ }
+
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+ 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 = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ 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;
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 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;
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position. The view will
+ * scroll such that the indicated position is displayed.
+ * @param position Scroll to this adapter position.
+ */
+ @android.view.RemotableViewMethod
+ public void smoothScrollToPosition(int position) {
+ super.smoothScrollToPosition(position);
+ }
+
+ /**
+ * Smoothly scroll to the specified adapter position offset. The view will
+ * scroll such that the indicated position is displayed.
+ * @param offset The amount to offset from the adapter position to scroll to.
+ */
+ @android.view.RemotableViewMethod
+ public void smoothScrollByOffset(int offset) {
+ super.smoothScrollByOffset(offset);
+ }
+
+ /**
+ * 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;
+ }
+
+ private class FocusSelector implements Runnable {
+ // the selector is waiting to set selection on the list view
+ private static final int STATE_SET_SELECTION = 1;
+ // the selector set the selection on the list view, waiting for a layoutChildren pass
+ private static final int STATE_WAIT_FOR_LAYOUT = 2;
+ // the selector's selection has been honored and it is waiting to request focus on the
+ // target child.
+ private static final int STATE_REQUEST_FOCUS = 3;
+
+ private int mAction;
+ private int mPosition;
+ private int mPositionTop;
+
+ FocusSelector setupForSetSelection(int position, int top) {
+ mPosition = position;
+ mPositionTop = top;
+ mAction = STATE_SET_SELECTION;
+ return this;
+ }
+
+ public void run() {
+ if (mAction == STATE_SET_SELECTION) {
+ setSelectionFromTop(mPosition, mPositionTop);
+ mAction = STATE_WAIT_FOR_LAYOUT;
+ } else if (mAction == STATE_REQUEST_FOCUS) {
+ final int childIndex = mPosition - mFirstPosition;
+ final View child = getChildAt(childIndex);
+ if (child != null) {
+ child.requestFocus();
+ }
+ mAction = -1;
+ }
+ }
+
+ @Nullable Runnable setupFocusIfValid(int position) {
+ if (mAction != STATE_WAIT_FOR_LAYOUT || position != mPosition) {
+ return null;
+ }
+ mAction = STATE_REQUEST_FOCUS;
+ return this;
+ }
+
+ void onLayoutComplete() {
+ if (mAction == STATE_WAIT_FOR_LAYOUT) {
+ mAction = -1;
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mFocusSelector != null) {
+ removeCallbacks(mFocusSelector);
+ mFocusSelector = null;
+ }
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (getChildCount() > 0) {
+ View focusedChild = getFocusedChild();
+ if (focusedChild != null) {
+ final int childPosition = mFirstPosition + indexOfChild(focusedChild);
+ final int childBottom = focusedChild.getBottom();
+ final int offset = Math.max(0, childBottom - (h - mPaddingTop));
+ final int top = focusedChild.getTop() - offset;
+ if (mFocusSelector == null) {
+ mFocusSelector = new FocusSelector();
+ }
+ post(mFocusSelector.setupForSetSelection(childPosition, top));
+ }
+ }
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Sets up mListPadding
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int childWidth = 0;
+ int childHeight = 0;
+ int childState = 0;
+
+ mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+ if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
+ || heightMode == MeasureSpec.UNSPECIFIED)) {
+ final View child = obtainView(0, mIsScrap);
+
+ // Lay out child directly against the parent measure spec so that
+ // we can obtain exected minimum width and height.
+ measureScrapChild(child, 0, widthMeasureSpec, heightSize);
+
+ childWidth = child.getMeasuredWidth();
+ childHeight = child.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+ if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
+ ((LayoutParams) child.getLayoutParams()).viewType)) {
+ mRecycler.addScrapView(child, 0);
+ }
+ }
+
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ widthSize = mListPadding.left + mListPadding.right + childWidth +
+ getVerticalScrollbarWidth();
+ } else {
+ widthSize |= (childState & MEASURED_STATE_MASK);
+ }
+
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightSize = mListPadding.top + mListPadding.bottom + childHeight +
+ getVerticalFadingEdgeLength() * 2;
+ }
+
+ if (heightMode == MeasureSpec.AT_MOST) {
+ // TODO: after first layout we should maybe start at the first visible position, not 0
+ heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
+ LayoutParams p = (LayoutParams) child.getLayoutParams();
+ if (p == null) {
+ p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+ child.setLayoutParams(p);
+ }
+ p.viewType = mAdapter.getItemViewType(position);
+ p.isEnabled = mAdapter.isEnabled(position);
+ p.forceAdd = true;
+
+ final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+ mListPadding.left + mListPadding.right, p.width);
+ final int lpHeight = p.height;
+ final int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+
+ // Since this view was measured directly aginst the parent measure
+ // spec, we must measure it again before reuse.
+ child.forceLayout();
+ }
+
+ /**
+ * @return True to recycle the views used to measure this ListView in
+ * UNSPECIFIED/AT_MOST modes, false otherwise.
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "list")
+ protected boolean recycleOnMeasure() {
+ return true;
+ }
+
+ /**
+ * Measures the height of the given range of children (inclusive) and
+ * returns the height with this ListView's padding and divider heights
+ * included. If maxHeight is provided, the measuring will stop when the
+ * current height reaches maxHeight.
+ *
+ * @param widthMeasureSpec The width measure spec to be given to a child's
+ * {@link View#measure(int, int)}.
+ * @param startPosition The position of the first child to be shown.
+ * @param endPosition The (inclusive) position of the last child to be
+ * shown. Specify {@link #NO_POSITION} if the last child should be
+ * the last available child from the adapter.
+ * @param maxHeight The maximum height that will be returned (if all the
+ * children don't fit in this value, this value will be
+ * returned).
+ * @param disallowPartialChildPosition In general, whether the returned
+ * height should only contain entire children. This is more
+ * powerful--it is the first inclusive position at which partial
+ * children will not be allowed. Example: it looks nice to have
+ * at least 3 completely visible children, and in portrait this
+ * will most likely fit; but in landscape there could be times
+ * when even 2 children can not be completely shown, so a value
+ * of 2 (remember, inclusive) would be good (assuming
+ * startPosition is 0).
+ * @return The height of this ListView with the given children.
+ */
+ final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
+ 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;
+ // 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;
+ final boolean recyle = recycleOnMeasure();
+ final boolean[] isScrap = mIsScrap;
+
+ for (i = startPosition; i <= endPosition; ++i) {
+ child = obtainView(i, isScrap);
+
+ measureScrapChild(child, i, widthMeasureSpec, maxHeight);
+
+ if (i > 0) {
+ // Count the divider for all but one child
+ returnedHeight += dividerHeight;
+ }
+
+ // Recycle the view before we possibly return from the method
+ if (recyle && recycleBin.shouldRecycleViewType(
+ ((LayoutParams) child.getLayoutParams()).viewType)) {
+ recycleBin.addScrapView(child, -1);
+ }
+
+ returnedHeight += child.getMeasuredHeight();
+
+ if (returnedHeight >= maxHeight) {
+ // We went over, figure out which height to return. If returnedHeight > maxHeight,
+ // then the i'th position did not fit completely.
+ return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
+ && (i > disallowPartialChildPosition) // We've past the min pos
+ && (prevHeightWithoutPartialChild > 0) // We have a prev height
+ && (returnedHeight != maxHeight) // i'th child did not fit completely
+ ? prevHeightWithoutPartialChild
+ : maxHeight;
+ }
+
+ if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
+ prevHeightWithoutPartialChild = returnedHeight;
+ }
+ }
+
+ // At this point, we went through the range of children, and they each
+ // completely fit, so return the returnedHeight
+ return returnedHeight;
+ }
+
+ @Override
+ int findMotionRow(int y) {
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ if (!mStackFromBottom) {
+ for (int i = 0; i < childCount; i++) {
+ View v = getChildAt(i);
+ if (y <= v.getBottom()) {
+ return mFirstPosition + i;
+ }
+ }
+ } else {
+ for (int i = childCount - 1; i >= 0; i--) {
+ View v = getChildAt(i);
+ if (y >= v.getTop()) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Put a specific item at a specific location on the screen and then build
+ * up and down from there.
+ *
+ * @param position The reference view to use as the starting point
+ * @param top Pixel offset from the top of this view to the top of the
+ * reference view.
+ *
+ * @return The selected view, or null if the selected view is outside the
+ * visible area.
+ */
+ private View fillSpecific(int position, int top) {
+ boolean tempIsSelected = position == mSelectedPosition;
+ View temp = makeAndAddView(position, top, true, 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) {
+ if (lastPosition < mItemCount - 1 || lastBottom > end) {
+ if (lastPosition == mItemCount - 1) {
+ // Don't pull the bottom too far up
+ topOffset = Math.min(topOffset, lastBottom - end);
+ }
+ // Move everything up
+ offsetChildrenTopAndBottom(-topOffset);
+ if (lastPosition < mItemCount - 1) {
+ // Fill the gap that was opened below the last position with more rows, if
+ // possible
+ fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight);
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+ } else if (lastPosition == mItemCount - 1) {
+ adjustViewsUpOrDown();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ final boolean blockLayoutRequests = mBlockLayoutRequests;
+ if (blockLayoutRequests) {
+ return;
+ }
+
+ mBlockLayoutRequests = true;
+
+ try {
+ super.layoutChildren();
+
+ invalidate();
+
+ if (mAdapter == null) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ final int childrenTop = mListPadding.top;
+ final int childrenBottom = mBottom - mTop - mListPadding.bottom;
+ final int childCount = getChildCount();
+
+ int index = 0;
+ 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:
+ default:
+ // Remember the previously selected view
+ index = mSelectedPosition - mFirstPosition;
+ if (index >= 0 && index < childCount) {
+ oldSel = getChildAt(index);
+ }
+
+ // Remember the previous first child
+ oldFirst = getChildAt(0);
+
+ if (mNextSelectedPosition >= 0) {
+ delta = mNextSelectedPosition - mSelectedPosition;
+ }
+
+ // Caution: newSel might be null
+ newSel = getChildAt(index + delta);
+ }
+
+
+ boolean dataChanged = mDataChanged;
+ if (dataChanged) {
+ handleDataChanged();
+ }
+
+ // Handle the empty set by removing all views that are visible
+ // and calling it a day
+ if (mItemCount == 0) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ } else if (mItemCount != mAdapter.getCount()) {
+ throw new IllegalStateException("The content of the adapter has changed but "
+ + "ListView did not receive a notification. Make sure the content of "
+ + "your adapter is not modified from a background thread, but only from "
+ + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ + "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ + ") with Adapter(" + mAdapter.getClass() + ")]");
+ }
+
+ setSelectedPositionInt(mNextSelectedPosition);
+
+ AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
+ View accessibilityFocusLayoutRestoreView = null;
+ int accessibilityFocusPosition = INVALID_POSITION;
+
+ // Remember which child, if any, had accessibility focus. This must
+ // occur before recycling any views, since that will clear
+ // accessibility focus.
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
+ if (focusHost != null) {
+ final View focusChild = getAccessibilityFocusedChild(focusHost);
+ if (focusChild != null) {
+ if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)
+ || (focusChild.hasTransientState() && mAdapterHasStableIds)) {
+ // The views won't be changing, so try to maintain
+ // focus on the current host and virtual view.
+ accessibilityFocusLayoutRestoreView = focusHost;
+ accessibilityFocusLayoutRestoreNode = viewRootImpl
+ .getAccessibilityFocusedVirtualView();
+ }
+
+ // If all else fails, maintain focus at the same
+ // position.
+ accessibilityFocusPosition = getPositionForView(focusChild);
+ }
+ }
+ }
+
+ View focusLayoutRestoreDirectChild = null;
+ View focusLayoutRestoreView = null;
+
+ // 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 ViewAncestor 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 re-layout
+ // if the data hasn't changed, or if the focused position is a
+ // header or footer.
+ if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)
+ || focusedChild.hasTransientState() || mAdapterHasStableIds) {
+ focusLayoutRestoreDirectChild = focusedChild;
+ // Remember the specific view that had focus.
+ focusLayoutRestoreView = findFocus();
+ if (focusLayoutRestoreView != null) {
+ // Tell it we are going to mess with it.
+ focusLayoutRestoreView.dispatchStartTemporaryDetach();
+ }
+ }
+ requestFocus();
+ }
+
+ // 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), firstPosition+i);
+ }
+ } else {
+ recycleBin.fillActiveViews(childCount, firstPosition);
+ }
+
+ // Clear out old views
+ detachAllViewsFromParent();
+ recycleBin.removeSkippedScrap();
+
+ 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:
+ final int selectedPosition = reconcileSelectedPosition();
+ sel = fillSpecific(selectedPosition, mSpecificTop);
+ /**
+ * When ListView is resized, FocusSelector requests an async selection for the
+ * previously focused item to make sure it is still visible. If the item is not
+ * selectable, it won't regain focus so instead we call FocusSelector
+ * to directly request focus on the view after it is visible.
+ */
+ if (sel == null && mFocusSelector != null) {
+ final Runnable focusRunnable = mFocusSelector
+ .setupFocusIfValid(selectedPosition);
+ if (focusRunnable != null) {
+ post(focusRunnable);
+ }
+ }
+ 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();
+
+ // remove any header/footer that has been temp detached and not re-attached
+ removeUnusedFixedViews(mHeaderViewInfos);
+ removeUnusedFixedViews(mFooterViewInfos);
+
+ 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 != null &&
+ focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
+ if (!focusWasTaken) {
+ // Selected item didn't take focus, but we still want to
+ // make sure something else outside of the selected view
+ // has focus.
+ final View focused = getFocusedChild();
+ if (focused != null) {
+ focused.clearFocus();
+ }
+ positionSelector(INVALID_POSITION, sel);
+ } else {
+ sel.setSelected(false);
+ mSelectorRect.setEmpty();
+ }
+ } else {
+ positionSelector(INVALID_POSITION, sel);
+ }
+ mSelectedTop = sel.getTop();
+ } else {
+ final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
+ || mTouchMode == TOUCH_MODE_DONE_WAITING;
+ if (inTouchMode) {
+ // If the user's finger is down, select the motion position.
+ final View child = getChildAt(mMotionPosition - mFirstPosition);
+ if (child != null) {
+ positionSelector(mMotionPosition, child);
+ }
+ } else if (mSelectorPosition != INVALID_POSITION) {
+ // If we had previously positioned the selector somewhere,
+ // put it back there. It might not match up with the data,
+ // but it's transitioning out so it's not a big deal.
+ final View child = getChildAt(mSelectorPosition - mFirstPosition);
+ if (child != null) {
+ positionSelector(mSelectorPosition, child);
+ }
+ } else {
+ // Otherwise, clear selection.
+ 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();
+ }
+ }
+
+ // Attempt to restore accessibility focus, if necessary.
+ if (viewRootImpl != null) {
+ final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
+ if (newAccessibilityFocusedView == null) {
+ if (accessibilityFocusLayoutRestoreView != null
+ && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
+ final AccessibilityNodeProvider provider =
+ accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
+ if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
+ final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
+ accessibilityFocusLayoutRestoreNode.getSourceNodeId());
+ provider.performAction(virtualViewId,
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ } else {
+ accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
+ }
+ } else if (accessibilityFocusPosition != INVALID_POSITION) {
+ // Bound the position within the visible children.
+ final int position = MathUtils.constrain(
+ accessibilityFocusPosition - mFirstPosition, 0,
+ getChildCount() - 1);
+ final View restoreView = getChildAt(position);
+ if (restoreView != null) {
+ restoreView.requestAccessibilityFocus();
+ }
+ }
+ }
+ }
+
+ // Tell focus view we are done mucking with it, if it is still in
+ // our view hierarchy.
+ if (focusLayoutRestoreView != null
+ && focusLayoutRestoreView.getWindowToken() != null) {
+ focusLayoutRestoreView.dispatchFinishTemporaryDetach();
+ }
+
+ mLayoutMode = LAYOUT_NORMAL;
+ mDataChanged = false;
+ if (mPositionScrollAfterLayout != null) {
+ post(mPositionScrollAfterLayout);
+ mPositionScrollAfterLayout = null;
+ }
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+
+ updateScrollIndicators();
+
+ if (mItemCount > 0) {
+ checkSelectionChanged();
+ }
+
+ invokeOnItemScrollListener();
+ } finally {
+ if (mFocusSelector != null) {
+ mFocusSelector.onLayoutComplete();
+ }
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = false;
+ }
+ }
+ }
+
+ @Override
+ boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
+ final boolean result = super.trackMotionScroll(deltaY, incrementalDeltaY);
+ removeUnusedFixedViews(mHeaderViewInfos);
+ removeUnusedFixedViews(mFooterViewInfos);
+ return result;
+ }
+
+ /**
+ * Header and Footer views are not scrapped / recycled like other views but they are still
+ * detached from the ViewGroup. After a layout operation, call this method to remove such views.
+ *
+ * @param infoList The info list to be traversed
+ */
+ private void removeUnusedFixedViews(@Nullable List<FixedViewInfo> infoList) {
+ if (infoList == null) {
+ return;
+ }
+ for (int i = infoList.size() - 1; i >= 0; i--) {
+ final FixedViewInfo fixedViewInfo = infoList.get(i);
+ final View view = fixedViewInfo.view;
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (view.getParent() == null && lp != null && lp.recycledHeaderFooter) {
+ removeDetachedView(view, false);
+ lp.recycledHeaderFooter = 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;
+ }
+
+ /**
+ * Obtains the view and adds 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 {@code true} to align top edge to y, {@code false} to align
+ * bottom edge to y
+ * @param childrenLeft left edge where children should be positioned
+ * @param selected {@code true} if the position is selected, {@code false}
+ * otherwise
+ * @return the view that was added
+ */
+ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
+ boolean selected) {
+ if (!mDataChanged) {
+ // Try to use an existing view for this position.
+ final View activeView = mRecycler.getActiveView(position);
+ if (activeView != null) {
+ // Found it. We're reusing an existing child, so it just needs
+ // to be positioned like a scrap view.
+ setupChild(activeView, position, y, flow, childrenLeft, selected, true);
+ return activeView;
+ }
+ }
+
+ // Make a new view for this position, or convert an unused view if
+ // possible.
+ final View child = obtainView(position, mIsScrap);
+
+ // This needs to be positioned and measured.
+ setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
+
+ return child;
+ }
+
+ /**
+ * Adds 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 {@code true} to align top edge to y, {@code false} to
+ * align bottom edge to y
+ * @param childrenLeft left edge where children should be positioned
+ * @param selected {@code true} if the position is selected, {@code false}
+ * otherwise
+ * @param isAttachedToWindow {@code true} if the view is already attached
+ * to the window, e.g. whether it was reused, or
+ * {@code false} otherwise
+ */
+ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
+ boolean selected, boolean isAttachedToWindow) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
+
+ final boolean isSelected = selected && shouldShowSelector();
+ final boolean updateChildSelected = isSelected != child.isSelected();
+ final int mode = mTouchMode;
+ final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
+ && mMotionPosition == position;
+ final boolean updateChildPressed = isPressed != child.isPressed();
+ final boolean needToMeasure = !isAttachedToWindow || 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 = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+ }
+ p.viewType = mAdapter.getItemViewType(position);
+ p.isEnabled = mAdapter.isEnabled(position);
+
+ // Set up view state before attaching the view, since we may need to
+ // rely on the jumpDrawablesToCurrentState() call that occurs as part
+ // of view attachment.
+ if (updateChildSelected) {
+ child.setSelected(isSelected);
+ }
+
+ if (updateChildPressed) {
+ child.setPressed(isPressed);
+ }
+
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(mCheckStates.get(position));
+ } else if (getContext().getApplicationInfo().targetSdkVersion
+ >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ child.setActivated(mCheckStates.get(position));
+ }
+ }
+
+ if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
+ && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
+ attachViewToParent(child, flowDown ? -1 : 0, p);
+
+ // If the view was previously attached for a different position,
+ // then manually jump the drawables.
+ if (isAttachedToWindow
+ && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
+ != position) {
+ child.jumpDrawablesToCurrentState();
+ }
+ } else {
+ p.forceAdd = false;
+ if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ p.recycledHeaderFooter = true;
+ }
+ addViewInLayout(child, flowDown ? -1 : 0, p, true);
+ // add view in layout will reset the RTL properties. We have to re-resolve them
+ child.resolveRtlPropertiesIfNeeded();
+ }
+
+ if (needToMeasure) {
+ final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ mListPadding.left + mListPadding.right, p.width);
+ final int lpHeight = p.height;
+ final int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
+ 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);
+ }
+
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+
+ @Override
+ protected boolean canAnimate() {
+ return super.canAnimate() && mItemCount > 0;
+ }
+
+ /**
+ * Sets the currently selected item. If in touch mode, the item will not be selected
+ * but it will still be positioned appropriately. If the specified selection position
+ * is less than 0, then the item at position 0 will be selected.
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ */
+ @Override
+ public void setSelection(int position) {
+ setSelectionFromTop(position, 0);
+ }
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position the position of the item to select
+ */
+ @Override
+ void setSelectionInt(int position) {
+ setNextSelectedPositionInt(position);
+ boolean awakeScrollbars = false;
+
+ final int selectedPosition = mSelectedPosition;
+
+ if (selectedPosition >= 0) {
+ if (position == selectedPosition - 1) {
+ awakeScrollbars = true;
+ } else if (position == selectedPosition + 1) {
+ awakeScrollbars = true;
+ }
+ }
+
+ if (mPositionScroller != null) {
+ mPositionScroller.stop();
+ }
+
+ layoutChildren();
+
+ if (awakeScrollbars) {
+ awakenScrollBars();
+ }
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator).
+ *
+ * @param position The starting position to look at.
+ * @param lookDown Whether to look down for other positions.
+ * @return The next selectable position starting at position and then searching either up or
+ * down. Returns {@link #INVALID_POSITION} if nothing can be found.
+ */
+ @Override
+ int lookForSelectablePosition(int position, boolean lookDown) {
+ final ListAdapter adapter = mAdapter;
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ final int count = adapter.getCount();
+ if (!mAreAllItemsSelectable) {
+ if (lookDown) {
+ position = Math.max(0, position);
+ while (position < count && !adapter.isEnabled(position)) {
+ position++;
+ }
+ } else {
+ position = Math.min(position, count - 1);
+ while (position >= 0 && !adapter.isEnabled(position)) {
+ position--;
+ }
+ }
+ }
+
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+
+ return position;
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator). If there
+ * are no selectable positions in the specified direction from the starting
+ * position, searches in the opposite direction from the starting position
+ * to the current position.
+ *
+ * @param current the current position
+ * @param position the starting position
+ * @param lookDown whether to look down for other positions
+ * @return the next selectable position, or {@link #INVALID_POSITION} if
+ * nothing can be found
+ */
+ int lookForSelectablePositionAfter(int current, int position, boolean lookDown) {
+ final ListAdapter adapter = mAdapter;
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ // First check after the starting position in the specified direction.
+ final int after = lookForSelectablePosition(position, lookDown);
+ if (after != INVALID_POSITION) {
+ return after;
+ }
+
+ // Then check between the starting position and the current position.
+ final int count = adapter.getCount();
+ current = MathUtils.constrain(current, -1, count - 1);
+ if (lookDown) {
+ position = Math.min(position - 1, count - 1);
+ while ((position > current) && !adapter.isEnabled(position)) {
+ position--;
+ }
+ if (position <= current) {
+ return INVALID_POSITION;
+ }
+ } else {
+ position = Math.max(0, position + 1);
+ while ((position < current) && !adapter.isEnabled(position)) {
+ position++;
+ }
+ if (position >= current) {
+ 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 = getHeaderViewsCount();
+ 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 || !isAttachedToWindow()) {
+ return false;
+ }
+
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ boolean handled = false;
+ int action = event.getAction();
+ if (KeyEvent.isConfirmKey(keyCode)
+ && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
+ handled = resurrectSelectionIfNeeded();
+ if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
+ keyPressed();
+ handled = true;
+ }
+ }
+
+
+ if (!handled && action != KeyEvent.ACTION_UP) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded();
+ if (!handled) {
+ while (count-- > 0) {
+ if (arrowScroll(FOCUS_UP)) {
+ handled = true;
+ } else {
+ break;
+ }
+ }
+ }
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded();
+ if (!handled) {
+ while (count-- > 0) {
+ if (arrowScroll(FOCUS_DOWN)) {
+ handled = true;
+ } else {
+ break;
+ }
+ }
+ }
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (event.hasNoModifiers()) {
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (event.hasNoModifiers()) {
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_UP:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_END:
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ // This creates an asymmetry in TAB navigation order. At some
+ // point in the future we may decide that it's preferable to
+ // force the list selection to the top or bottom when receiving
+ // TAB focus from another widget, but for now this is adequate.
+ if (event.hasNoModifiers()) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
+ }
+ break;
+ }
+ }
+
+ if (handled) {
+ return true;
+ }
+
+ if (sendToTextFilter(keyCode, count, event)) {
+ return true;
+ }
+
+ 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) {
+ final int nextPage;
+ final boolean down;
+
+ if (direction == FOCUS_UP) {
+ nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
+ down = false;
+ } else if (direction == FOCUS_DOWN) {
+ nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
+ down = true;
+ } else {
+ return false;
+ }
+
+ if (nextPage >= 0) {
+ final int position = lookForSelectablePositionAfter(mSelectedPosition, 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);
+ invokeOnItemScrollListener();
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Go to the last or first item if possible (not worrying about panning
+ * across or navigating within the internal focus of the currently selected
+ * item.)
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ * @return whether selection was moved
+ */
+ boolean fullScroll(int direction) {
+ boolean moved = false;
+ if (direction == FOCUS_UP) {
+ if (mSelectedPosition != 0) {
+ final int position = lookForSelectablePositionAfter(mSelectedPosition, 0, true);
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_FORCE_TOP;
+ setSelectionInt(position);
+ invokeOnItemScrollListener();
+ }
+ moved = true;
+ }
+ } else if (direction == FOCUS_DOWN) {
+ final int lastItem = (mItemCount - 1);
+ if (mSelectedPosition < lastItem) {
+ final int position = lookForSelectablePositionAfter(
+ mSelectedPosition, lastItem, false);
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ setSelectionInt(position);
+ invokeOnItemScrollListener();
+ }
+ moved = true;
+ }
+ }
+
+ if (moved && !awakenScrollBars()) {
+ awakenScrollBars();
+ 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 != null && 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
+ Rect focusedRect = mTempRect;
+ if (currentFocus != null) {
+ currentFocus.getFocusedRect(focusedRect);
+ offsetDescendantRectToMyCoords(currentFocus, focusedRect);
+ offsetRectIntoDescendantCoords(nextFocus, focusedRect);
+ } else {
+ focusedRect = null;
+ }
+ if (nextFocus.requestFocus(direction, focusedRect)) {
+ 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;
+ }
+ }
+
+ /**
+ * Used by {@link #arrowScrollImpl(int)} to help determine the next selected position
+ * to move to. This return a position in the direction given if the selected item
+ * is fully visible.
+ *
+ * @param selectedView Current selected view to move from
+ * @param selectedPos Current selected position to move from
+ * @param direction Direction to move in
+ * @return Desired selected position after moving in the given direction
+ */
+ private final int nextSelectedPositionForDirection(
+ View selectedView, int selectedPos, int direction) {
+ int nextSelected;
+
+ if (direction == View.FOCUS_DOWN) {
+ final int listBottom = getHeight() - mListPadding.bottom;
+ if (selectedView != null && selectedView.getBottom() <= listBottom) {
+ nextSelected = selectedPos != INVALID_POSITION && selectedPos >= mFirstPosition ?
+ selectedPos + 1 :
+ mFirstPosition;
+ } else {
+ return INVALID_POSITION;
+ }
+ } else {
+ final int listTop = mListPadding.top;
+ if (selectedView != null && selectedView.getTop() >= listTop) {
+ final int lastPos = mFirstPosition + getChildCount() - 1;
+ nextSelected = selectedPos != INVALID_POSITION && selectedPos <= lastPos ?
+ selectedPos - 1 :
+ lastPos;
+ } else {
+ return INVALID_POSITION;
+ }
+ }
+
+ if (nextSelected < 0 || nextSelected >= mAdapter.getCount()) {
+ return INVALID_POSITION;
+ }
+ return lookForSelectablePosition(nextSelected, direction == View.FOCUS_DOWN);
+ }
+
+ /**
+ * 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 selectedPos = mSelectedPosition;
+
+ int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, 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();
+ selectedPos = nextSelectedPosition;
+ 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 (focused != null) {
+ if (!isViewAncestorOf(focused, this) || 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();
+
+ // but we don't want to set the ressurect position (that would make subsequent
+ // unhandled key events bring back the item we just scrolled off!)
+ mResurrectToPosition = INVALID_POSITION;
+ }
+
+ if (needToRedraw) {
+ if (selectedView != null) {
+ positionSelectorLikeFocus(selectedPos, selectedView);
+ mSelectedTop = selectedView.getTop();
+ }
+ if (!awakenScrollBars()) {
+ 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.MATCH_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.makeSafeMeasureSpec(getMeasuredHeight(),
+ 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;
+
+ int numChildren = getChildCount();
+
+ if (direction == View.FOCUS_DOWN) {
+ int indexToMakeVisible = numChildren - 1;
+ if (nextSelectedPosition != INVALID_POSITION) {
+ indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+ }
+ while (numChildren <= indexToMakeVisible) {
+ // Child to view is not attached yet.
+ addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1);
+ numChildren++;
+ }
+ 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;
+ }
+ while (indexToMakeVisible < 0) {
+ // Child to view is not attached yet.
+ addViewAbove(getChildAt(0), mFirstPosition);
+ mFirstPosition--;
+ 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 || startPos >= mAdapter.getCount()) {
+ 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
+ * direction.
+ *
+ * @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;
+ final AbsListView.RecycleBin recycleBin = mRecycler;
+
+ 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) {
+ AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
+ if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
+ recycleBin.addScrapView(first, mFirstPosition);
+ }
+ detachViewFromParent(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) {
+ AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
+ if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
+ recycleBin.addScrapView(last, mFirstPosition+lastIndex);
+ }
+ detachViewFromParent(last);
+ last = getChildAt(--lastIndex);
+ }
+ }
+ recycleBin.fullyDetachScrapViews();
+ removeUnusedFixedViews(mHeaderViewInfos);
+ removeUnusedFixedViews(mFooterViewInfos);
+ }
+
+ private View addViewAbove(View theView, int position) {
+ int abovePosition = position - 1;
+ View view = obtainView(abovePosition, mIsScrap);
+ int edgeOfNewChild = theView.getTop() - mDividerHeight;
+ setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
+ false, mIsScrap[0]);
+ return view;
+ }
+
+ private View addViewBelow(View theView, int position) {
+ int belowPosition = position + 1;
+ View view = obtainView(belowPosition, mIsScrap);
+ int edgeOfNewChild = theView.getBottom() + mDividerHeight;
+ setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
+ false, mIsScrap[0]);
+ return view;
+ }
+
+ /**
+ * Indicates that the views created by the ListAdapter can contain focusable
+ * items.
+ *
+ * @param itemsCanFocus true if items can get focus, false otherwise
+ */
+ public void setItemsCanFocus(boolean itemsCanFocus) {
+ mItemsCanFocus = itemsCanFocus;
+ if (!itemsCanFocus) {
+ setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+ }
+
+ /**
+ * @return Whether the views created by the ListAdapter can contain focusable
+ * items.
+ */
+ public boolean getItemsCanFocus() {
+ return mItemsCanFocus;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque &&
+ hasOpaqueScrollbars()) || super.isOpaque();
+ if (retValue) {
+ // only return true if the list items cover the entire area of the view
+ final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop;
+ View first = getChildAt(0);
+ if (first == null || first.getTop() > listTop) {
+ return false;
+ }
+ final int listBottom = getHeight() -
+ (mListPadding != null ? mListPadding.bottom : mPaddingBottom);
+ View last = getChildAt(getChildCount() - 1);
+ if (last == null || last.getBottom() < listBottom) {
+ return false;
+ }
+ }
+ return retValue;
+ }
+
+ @Override
+ public void setCacheColorHint(int color) {
+ final boolean opaque = (color >>> 24) == 0xFF;
+ mIsCacheColorOpaque = opaque;
+ if (opaque) {
+ if (mDividerPaint == null) {
+ mDividerPaint = new Paint();
+ }
+ mDividerPaint.setColor(color);
+ }
+ super.setCacheColorHint(color);
+ }
+
+ void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) {
+ final int height = drawable.getMinimumHeight();
+
+ canvas.save();
+ canvas.clipRect(bounds);
+
+ final int span = bounds.bottom - bounds.top;
+ if (span < height) {
+ bounds.top = bounds.bottom - height;
+ }
+
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
+
+ canvas.restore();
+ }
+
+ void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) {
+ final int height = drawable.getMinimumHeight();
+
+ canvas.save();
+ canvas.clipRect(bounds);
+
+ final int span = bounds.bottom - bounds.top;
+ if (span < height) {
+ bounds.bottom = bounds.top + height;
+ }
+
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
+
+ canvas.restore();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mCachingStarted) {
+ mCachingActive = true;
+ }
+
+ // Draw the dividers
+ final int dividerHeight = mDividerHeight;
+ final Drawable overscrollHeader = mOverScrollHeader;
+ final Drawable overscrollFooter = mOverScrollFooter;
+ final boolean drawOverscrollHeader = overscrollHeader != null;
+ final boolean drawOverscrollFooter = overscrollFooter != null;
+ final boolean drawDividers = dividerHeight > 0 && mDivider != null;
+
+ if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
+ // Only modify the top and bottom in the loop, we set the left and right here
+ final Rect bounds = mTempRect;
+ bounds.left = mPaddingLeft;
+ bounds.right = mRight - mLeft - mPaddingRight;
+
+ final int count = getChildCount();
+ final int headerCount = getHeaderViewsCount();
+ final int itemCount = mItemCount;
+ final int footerLimit = (itemCount - mFooterViewInfos.size());
+ final boolean headerDividers = mHeaderDividersEnabled;
+ final boolean footerDividers = mFooterDividersEnabled;
+ final int first = mFirstPosition;
+ final boolean areAllItemsSelectable = mAreAllItemsSelectable;
+ final ListAdapter adapter = mAdapter;
+ // If the list is opaque *and* the background is not, we want to
+ // fill a rect where the dividers would be for non-selectable items
+ // If the list is opaque and the background is also opaque, we don't
+ // need to draw anything since the background will do it for us
+ final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
+
+ if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
+ mDividerPaint = new Paint();
+ mDividerPaint.setColor(getCacheColorHint());
+ }
+ final Paint paint = mDividerPaint;
+
+ int effectivePaddingTop = 0;
+ int effectivePaddingBottom = 0;
+ if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+ effectivePaddingTop = mListPadding.top;
+ effectivePaddingBottom = mListPadding.bottom;
+ }
+
+ final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
+ if (!mStackFromBottom) {
+ int bottom = 0;
+
+ // Draw top divider or header for overscroll
+ final int scrollY = mScrollY;
+ if (count > 0 && scrollY < 0) {
+ if (drawOverscrollHeader) {
+ bounds.bottom = 0;
+ bounds.top = scrollY;
+ drawOverscrollHeader(canvas, overscrollHeader, bounds);
+ } else if (drawDividers) {
+ bounds.bottom = 0;
+ bounds.top = -dividerHeight;
+ drawDivider(canvas, bounds, -1);
+ }
+ }
+
+ for (int i = 0; i < count; i++) {
+ final int itemIndex = (first + i);
+ final boolean isHeader = (itemIndex < headerCount);
+ final boolean isFooter = (itemIndex >= footerLimit);
+ if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+ final View child = getChildAt(i);
+ bottom = child.getBottom();
+ final boolean isLastItem = (i == (count - 1));
+
+ if (drawDividers && (bottom < listBottom)
+ && !(drawOverscrollFooter && isLastItem)) {
+ final int nextIndex = (itemIndex + 1);
+ // Draw dividers between enabled items, headers
+ // and/or footers when enabled and requested, and
+ // after the last enabled item.
+ if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+ && (nextIndex >= headerCount)) && (isLastItem
+ || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
+ && (nextIndex < footerLimit)))) {
+ bounds.top = bottom;
+ bounds.bottom = bottom + dividerHeight;
+ drawDivider(canvas, bounds, i);
+ } else if (fillForMissingDividers) {
+ bounds.top = bottom;
+ bounds.bottom = bottom + dividerHeight;
+ canvas.drawRect(bounds, paint);
+ }
+ }
+ }
+ }
+
+ final int overFooterBottom = mBottom + mScrollY;
+ if (drawOverscrollFooter && first + count == itemCount &&
+ overFooterBottom > bottom) {
+ bounds.top = bottom;
+ bounds.bottom = overFooterBottom;
+ drawOverscrollFooter(canvas, overscrollFooter, bounds);
+ }
+ } else {
+ int top;
+
+ final int scrollY = mScrollY;
+
+ if (count > 0 && drawOverscrollHeader) {
+ bounds.top = scrollY;
+ bounds.bottom = getChildAt(0).getTop();
+ drawOverscrollHeader(canvas, overscrollHeader, bounds);
+ }
+
+ final int start = drawOverscrollHeader ? 1 : 0;
+ for (int i = start; i < count; i++) {
+ final int itemIndex = (first + i);
+ final boolean isHeader = (itemIndex < headerCount);
+ final boolean isFooter = (itemIndex >= footerLimit);
+ if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+ final View child = getChildAt(i);
+ top = child.getTop();
+ if (drawDividers && (top > effectivePaddingTop)) {
+ final boolean isFirstItem = (i == start);
+ final int previousIndex = (itemIndex - 1);
+ // Draw dividers between enabled items, headers
+ // and/or footers when enabled and requested, and
+ // before the first enabled item.
+ if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+ && (previousIndex >= headerCount)) && (isFirstItem ||
+ adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
+ && (previousIndex < footerLimit)))) {
+ bounds.top = top - dividerHeight;
+ bounds.bottom = top;
+ // Give the method the child ABOVE the divider,
+ // so we subtract one from our child position.
+ // Give -1 when there is no child above the
+ // divider.
+ drawDivider(canvas, bounds, i - 1);
+ } else if (fillForMissingDividers) {
+ bounds.top = top - dividerHeight;
+ bounds.bottom = top;
+ canvas.drawRect(bounds, paint);
+ }
+ }
+ }
+ }
+
+ if (count > 0 && scrollY > 0) {
+ if (drawOverscrollFooter) {
+ final int absListBottom = mBottom;
+ bounds.top = absListBottom;
+ bounds.bottom = absListBottom + scrollY;
+ drawOverscrollFooter(canvas, overscrollFooter, bounds);
+ } else if (drawDividers) {
+ bounds.top = listBottom;
+ bounds.bottom = listBottom + dividerHeight;
+ drawDivider(canvas, bounds, -1);
+ }
+ }
+ }
+ }
+
+ // Draw the indicators (these should be drawn above the dividers) and children
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ boolean more = super.drawChild(canvas, child, drawingTime);
+ if (mCachingActive && child.mCachingFailed) {
+ mCachingActive = false;
+ }
+ return more;
+ }
+
+ /**
+ * Draws a divider for the given child in the given bounds.
+ *
+ * @param canvas The canvas to draw to.
+ * @param bounds The bounds of the divider.
+ * @param childIndex The index of child (of the View) above the divider.
+ * This will be -1 if there is no child above the divider to be
+ * drawn.
+ */
+ void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
+ // This widget draws the same divider for all children
+ final Drawable divider = mDivider;
+
+ divider.setBounds(bounds);
+ divider.draw(canvas);
+ }
+
+ /**
+ * Returns the drawable that will be drawn between each item in the list.
+ *
+ * @return the current drawable drawn between list elements
+ * @attr ref R.styleable#ListView_divider
+ */
+ @Nullable
+ public Drawable getDivider() {
+ return mDivider;
+ }
+
+ /**
+ * Sets the drawable that will be drawn between each item in the list.
+ * <p>
+ * <strong>Note:</strong> If the drawable does not have an intrinsic
+ * height, you should also call {@link #setDividerHeight(int)}.
+ *
+ * @param divider the drawable to use
+ * @attr ref R.styleable#ListView_divider
+ */
+ public void setDivider(@Nullable Drawable divider) {
+ if (divider != null) {
+ mDividerHeight = divider.getIntrinsicHeight();
+ } else {
+ mDividerHeight = 0;
+ }
+ mDivider = divider;
+ mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * @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;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Enables or disables the drawing of the divider for header views.
+ *
+ * @param headerDividersEnabled True to draw the headers, false otherwise.
+ *
+ * @see #setFooterDividersEnabled(boolean)
+ * @see #areHeaderDividersEnabled()
+ * @see #addHeaderView(android.view.View)
+ */
+ public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
+ mHeaderDividersEnabled = headerDividersEnabled;
+ invalidate();
+ }
+
+ /**
+ * @return Whether the drawing of the divider for header views is enabled
+ *
+ * @see #setHeaderDividersEnabled(boolean)
+ */
+ public boolean areHeaderDividersEnabled() {
+ return mHeaderDividersEnabled;
+ }
+
+ /**
+ * Enables or disables the drawing of the divider for footer views.
+ *
+ * @param footerDividersEnabled True to draw the footers, false otherwise.
+ *
+ * @see #setHeaderDividersEnabled(boolean)
+ * @see #areFooterDividersEnabled()
+ * @see #addFooterView(android.view.View)
+ */
+ public void setFooterDividersEnabled(boolean footerDividersEnabled) {
+ mFooterDividersEnabled = footerDividersEnabled;
+ invalidate();
+ }
+
+ /**
+ * @return Whether the drawing of the divider for footer views is enabled
+ *
+ * @see #setFooterDividersEnabled(boolean)
+ */
+ public boolean areFooterDividersEnabled() {
+ return mFooterDividersEnabled;
+ }
+
+ /**
+ * Sets the drawable that will be drawn above all other list content.
+ * This area can become visible when the user overscrolls the list.
+ *
+ * @param header The drawable to use
+ */
+ public void setOverscrollHeader(Drawable header) {
+ mOverScrollHeader = header;
+ if (mScrollY < 0) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return The drawable that will be drawn above all other list content
+ */
+ public Drawable getOverscrollHeader() {
+ return mOverScrollHeader;
+ }
+
+ /**
+ * Sets the drawable that will be drawn below all other list content.
+ * This area can become visible when the user overscrolls the list,
+ * or when the list's content does not fully fill the container area.
+ *
+ * @param footer The drawable to use
+ */
+ public void setOverscrollFooter(Drawable footer) {
+ mOverScrollFooter = footer;
+ invalidate();
+ }
+
+ /**
+ * @return The drawable that will be drawn below all other list content
+ */
+ public Drawable getOverscrollFooter() {
+ return mOverScrollFooter;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+ final ListAdapter adapter = mAdapter;
+ int closetChildIndex = -1;
+ int closestChildTop = 0;
+ if (adapter != null && gainFocus && previouslyFocusedRect != null) {
+ previouslyFocusedRect.offset(mScrollX, mScrollY);
+
+ // Don't cache the result of getChildCount or mFirstPosition here,
+ // it could change in layoutChildren.
+ if (adapter.getCount() < getChildCount() + mFirstPosition) {
+ mLayoutMode = LAYOUT_NORMAL;
+ layoutChildren();
+ }
+
+ // figure out which item should be selected based on previously
+ // focused rect
+ Rect otherRect = mTempRect;
+ int minDistance = Integer.MAX_VALUE;
+ final int childCount = getChildCount();
+ final int firstPosition = mFirstPosition;
+
+ for (int i = 0; i < childCount; i++) {
+ // only consider selectable views
+ if (!adapter.isEnabled(firstPosition + i)) {
+ continue;
+ }
+
+ View other = getChildAt(i);
+ other.getDrawingRect(otherRect);
+ offsetDescendantRectToMyCoords(other, otherRect);
+ int distance = getDistance(previouslyFocusedRect, otherRect, direction);
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ closetChildIndex = i;
+ closestChildTop = other.getTop();
+ }
+ }
+ }
+
+ if (closetChildIndex >= 0) {
+ setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop);
+ } 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();
+ }
+ }
+
+ /**
+ * @see android.view.View#findViewById(int)
+ * @removed For internal use only. This should have been hidden.
+ */
+ @Override
+ protected <T extends View> T findViewTraversal(@IdRes int id) {
+ // First look in our children, then in any header and footer views that
+ // may be scrolled off.
+ View v = super.findViewTraversal(id);
+ if (v == null) {
+ v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
+ if (v != null) {
+ return (T) v;
+ }
+ v = findViewInHeadersOrFooters(mFooterViewInfos, id);
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ return (T) v;
+ }
+
+ View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) {
+ // Look in the passed in list of headers or footers for the view.
+ 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;
+ }
+
+ /**
+ * @see android.view.View#findViewWithTag(Object)
+ * @removed For internal use only. This should have been hidden.
+ */
+ @Override
+ protected <T extends View> T findViewWithTagTraversal(Object tag) {
+ // First look in our children, then in any header and footer views that
+ // may be scrolled off.
+ View v = super.findViewWithTagTraversal(tag);
+ if (v == null) {
+ v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag);
+ if (v != null) {
+ return (T) v;
+ }
+
+ v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag);
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ return (T) v;
+ }
+
+ View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
+ // Look in the passed in list of headers or footers for the view with
+ // the 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;
+ }
+
+ /**
+ * First look in our children, then in any header and footer views that may
+ * be scrolled off.
+ *
+ * @see android.view.View#findViewByPredicate(Predicate)
+ * @hide
+ */
+ @Override
+ protected <T extends View> T findViewByPredicateTraversal(
+ Predicate<View> predicate, View childToSkip) {
+ View v = super.findViewByPredicateTraversal(predicate, childToSkip);
+ if (v == null) {
+ v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate, childToSkip);
+ if (v != null) {
+ return (T) v;
+ }
+
+ v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate, childToSkip);
+ if (v != null) {
+ return (T) v;
+ }
+ }
+ return (T) v;
+ }
+
+ /**
+ * Look in the passed in list of headers or footers for the first view that
+ * matches the predicate.
+ */
+ View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where,
+ Predicate<View> predicate, View childToSkip) {
+ if (where != null) {
+ int len = where.size();
+ View v;
+
+ for (int i = 0; i < len; i++) {
+ v = where.get(i).view;
+
+ if (v != childToSkip && !v.isRootNamespace()) {
+ v = v.findViewByPredicate(predicate);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the set of checked items ids. The result is only valid if the
+ * choice mode has not been set to {@link #CHOICE_MODE_NONE}.
+ *
+ * @return A new array which contains the id of each checked item in the
+ * list.
+ *
+ * @deprecated Use {@link #getCheckedItemIds()} instead.
+ */
+ @Deprecated
+ public long[] getCheckItemIds() {
+ // Use new behavior that correctly handles stable ID mapping.
+ if (mAdapter != null && mAdapter.hasStableIds()) {
+ return getCheckedItemIds();
+ }
+
+ // Old behavior was buggy, but would sort of work for adapters without stable IDs.
+ // Fall back to it to support legacy apps.
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) {
+ final SparseBooleanArray states = mCheckStates;
+ final int count = states.size();
+ final long[] ids = new long[count];
+ final ListAdapter adapter = mAdapter;
+
+ int checkedCount = 0;
+ for (int i = 0; i < count; i++) {
+ if (states.valueAt(i)) {
+ ids[checkedCount++] = adapter.getItemId(states.keyAt(i));
+ }
+ }
+
+ // Trim array if needed. mCheckStates may contain false values
+ // resulting in checkedCount being smaller than count.
+ if (checkedCount == count) {
+ return ids;
+ } else {
+ final long[] result = new long[checkedCount];
+ System.arraycopy(ids, 0, result, 0, checkedCount);
+
+ return result;
+ }
+ }
+ return new long[0];
+ }
+
+ @Override
+ int getHeightForPosition(int position) {
+ final int height = super.getHeightForPosition(position);
+ if (shouldAdjustHeightForDivider(position)) {
+ return height + mDividerHeight;
+ }
+ return height;
+ }
+
+ private boolean shouldAdjustHeightForDivider(int itemIndex) {
+ final int dividerHeight = mDividerHeight;
+ final Drawable overscrollHeader = mOverScrollHeader;
+ final Drawable overscrollFooter = mOverScrollFooter;
+ final boolean drawOverscrollHeader = overscrollHeader != null;
+ final boolean drawOverscrollFooter = overscrollFooter != null;
+ final boolean drawDividers = dividerHeight > 0 && mDivider != null;
+
+ if (drawDividers) {
+ final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
+ final int itemCount = mItemCount;
+ final int headerCount = getHeaderViewsCount();
+ final int footerLimit = (itemCount - mFooterViewInfos.size());
+ final boolean isHeader = (itemIndex < headerCount);
+ final boolean isFooter = (itemIndex >= footerLimit);
+ final boolean headerDividers = mHeaderDividersEnabled;
+ final boolean footerDividers = mFooterDividersEnabled;
+ if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+ final ListAdapter adapter = mAdapter;
+ if (!mStackFromBottom) {
+ final boolean isLastItem = (itemIndex == (itemCount - 1));
+ if (!drawOverscrollFooter || !isLastItem) {
+ final int nextIndex = itemIndex + 1;
+ // Draw dividers between enabled items, headers
+ // and/or footers when enabled and requested, and
+ // after the last enabled item.
+ if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+ && (nextIndex >= headerCount)) && (isLastItem
+ || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
+ && (nextIndex < footerLimit)))) {
+ return true;
+ } else if (fillForMissingDividers) {
+ return true;
+ }
+ }
+ } else {
+ final int start = drawOverscrollHeader ? 1 : 0;
+ final boolean isFirstItem = (itemIndex == start);
+ if (!isFirstItem) {
+ final int previousIndex = (itemIndex - 1);
+ // Draw dividers between enabled items, headers
+ // and/or footers when enabled and requested, and
+ // before the first enabled item.
+ if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+ && (previousIndex >= headerCount)) && (isFirstItem ||
+ adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
+ && (previousIndex < footerLimit)))) {
+ return true;
+ } else if (fillForMissingDividers) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ListView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ final int rowsCount = getCount();
+ final int selectionMode = getSelectionModeForAccessibility();
+ final CollectionInfo collectionInfo = CollectionInfo.obtain(
+ rowsCount, 1, false, selectionMode);
+ info.setCollectionInfo(collectionInfo);
+
+ if (rowsCount > 0) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ switch (action) {
+ case R.id.accessibilityActionScrollToPosition: {
+ final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+ final int position = Math.min(row, getCount() - 1);
+ if (row >= 0) {
+ // The accessibility service gets data asynchronously, so
+ // we'll be a little lenient by clamping the last position.
+ smoothScrollToPosition(position);
+ return true;
+ }
+ } break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoForItem(
+ View view, int position, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoForItem(view, position, info);
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ final boolean isSelected = isItemChecked(position);
+ final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
+ position, 1, 0, 1, isHeading, isSelected);
+ info.setCollectionItemInfo(itemInfo);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ encoder.addProperty("recycleOnMeasure", recycleOnMeasure());
+ }
+
+ /** @hide */
+ protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+ ArrayList<ListView.FixedViewInfo> headerViewInfos,
+ ArrayList<ListView.FixedViewInfo> footerViewInfos,
+ ListAdapter adapter) {
+ return new HeaderViewListAdapter(headerViewInfos, footerViewInfos, adapter);
+ }
+
+ /** @hide */
+ protected void wrapHeaderListAdapterInternal() {
+ mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
+ }
+
+ /** @hide */
+ protected void dispatchDataSetObserverOnChangedInternal() {
+ if (mDataSetObserver != null) {
+ mDataSetObserver.onChanged();
+ }
+ }
+}
diff --git a/android/widget/MediaController.java b/android/widget/MediaController.java
new file mode 100644
index 0000000..8e04f1c
--- /dev/null
+++ b/android/widget/MediaController.java
@@ -0,0 +1,716 @@
+/*
+ * 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.graphics.PixelFormat;
+import android.media.AudioManager;
+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.view.accessibility.AccessibilityManager;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.internal.policy.PhoneWindow;
+
+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 programmatically.
+ * 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 final Context mContext;
+ private View mAnchor;
+ private View mRoot;
+ private WindowManager mWindowManager;
+ private Window mWindow;
+ private View mDecor;
+ private WindowManager.LayoutParams mDecorLayoutParams;
+ private ProgressBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private boolean mShowing;
+ private boolean mDragging;
+ private static final int sDefaultTimeout = 3000;
+ private final 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;
+ private CharSequence mPlayDescription;
+ private CharSequence mPauseDescription;
+ private final AccessibilityManager mAccessibilityManager;
+
+ public MediaController(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRoot = this;
+ mContext = context;
+ mUseFastForward = true;
+ mFromXml = true;
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ if (mRoot != null)
+ initControllerView(mRoot);
+ }
+
+ public MediaController(Context context, boolean useFastForward) {
+ super(context);
+ mContext = context;
+ mUseFastForward = useFastForward;
+ initFloatingWindowLayout();
+ initFloatingWindow();
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+ }
+
+ public MediaController(Context context) {
+ this(context, true);
+ }
+
+ private void initFloatingWindow() {
+ mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+ mWindow = new PhoneWindow(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();
+ }
+
+ // Allocate and initialize the static parts of mDecorLayoutParams. Must
+ // also call updateFloatingWindowLayout() to fill in the dynamic parts
+ // (y and width) before mDecorLayoutParams can be used.
+ private void initFloatingWindowLayout() {
+ mDecorLayoutParams = new WindowManager.LayoutParams();
+ WindowManager.LayoutParams p = mDecorLayoutParams;
+ p.gravity = Gravity.TOP | Gravity.LEFT;
+ p.height = LayoutParams.WRAP_CONTENT;
+ p.x = 0;
+ p.format = PixelFormat.TRANSLUCENT;
+ p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+ p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+ p.token = null;
+ p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
+ }
+
+ // Update the dynamic parts of mDecorLayoutParams
+ // Must be called with mAnchor != NULL.
+ private void updateFloatingWindowLayout() {
+ int [] anchorPos = new int[2];
+ mAnchor.getLocationOnScreen(anchorPos);
+
+ // we need to know the size of the controller so we can properly position it
+ // within its space
+ mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST));
+
+ WindowManager.LayoutParams p = mDecorLayoutParams;
+ p.width = mAnchor.getWidth();
+ p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2;
+ p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight();
+ }
+
+ // This is called whenever mAnchor's layout bound changes
+ private final OnLayoutChangeListener mLayoutChangeListener =
+ new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight,
+ int oldBottom) {
+ updateFloatingWindowLayout();
+ if (mShowing) {
+ mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
+ }
+ }
+ };
+
+ private final OnTouchListener mTouchListener = new OnTouchListener() {
+ @Override
+ 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.
+ * When VideoView calls this method, it will use the VideoView's parent
+ * as the anchor.
+ * @param view The view to which to anchor the controller when it is visible.
+ */
+ public void setAnchorView(View view) {
+ if (mAnchor != null) {
+ mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
+ }
+ mAnchor = view;
+ if (mAnchor != null) {
+ mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
+ }
+
+ FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_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) {
+ Resources res = mContext.getResources();
+ mPlayDescription = res
+ .getText(com.android.internal.R.string.lockscreen_transport_play_description);
+ mPauseDescription = res
+ .getText(com.android.internal.R.string.lockscreen_transport_pause_description);
+ mPauseButton = v.findViewById(com.android.internal.R.id.pause);
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ mPauseButton.setOnClickListener(mPauseListener);
+ }
+
+ mFfwdButton = v.findViewById(com.android.internal.R.id.ffwd);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(mFfwdListener);
+ if (!mFromXml) {
+ mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ mRewButton = 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 = v.findViewById(com.android.internal.R.id.next);
+ if (mNextButton != null && !mFromXml && !mListenersSet) {
+ mNextButton.setVisibility(View.GONE);
+ }
+ mPrevButton = v.findViewById(com.android.internal.R.id.prev);
+ if (mPrevButton != null && !mFromXml && !mListenersSet) {
+ mPrevButton.setVisibility(View.GONE);
+ }
+
+ mProgress = 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 = v.findViewById(com.android.internal.R.id.time);
+ mCurrentTime = 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);
+ }
+
+ /**
+ * Disable pause or seek buttons if the stream cannot be paused or seeked.
+ * This requires the control interface to be a MediaPlayerControlExt
+ */
+ private void disableUnsupportedButtons() {
+ try {
+ if (mPauseButton != null && !mPlayer.canPause()) {
+ mPauseButton.setEnabled(false);
+ }
+ if (mRewButton != null && !mPlayer.canSeekBackward()) {
+ mRewButton.setEnabled(false);
+ }
+ if (mFfwdButton != null && !mPlayer.canSeekForward()) {
+ mFfwdButton.setEnabled(false);
+ }
+ // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+ // this scheme can break the case when applications want to allow seek through the
+ // progress bar but disable forward/backward buttons.
+ //
+ // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+ // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+ // shouldn't arise in existing applications.
+ if (mProgress != null && !mPlayer.canSeekBackward() && !mPlayer.canSeekForward()) {
+ mProgress.setEnabled(false);
+ }
+ } catch (IncompatibleClassChangeError ex) {
+ // We were given an old version of the interface, that doesn't have
+ // the canPause/canSeekXYZ methods. This is OK, it just means we
+ // assume the media can be paused and seeked, and so we don't disable
+ // the buttons.
+ }
+ }
+
+ /**
+ * 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();
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ }
+ disableUnsupportedButtons();
+ updateFloatingWindowLayout();
+ mWindowManager.addView(mDecor, mDecorLayoutParams);
+ 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.
+ post(mShowProgress);
+
+ if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ removeCallbacks(mFadeOut);
+ postDelayed(mFadeOut, timeout);
+ }
+ }
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Remove the controller from the screen.
+ */
+ public void hide() {
+ if (mAnchor == null)
+ return;
+
+ if (mShowing) {
+ try {
+ removeCallbacks(mShowProgress);
+ mWindowManager.removeView(mDecor);
+ } catch (IllegalArgumentException ex) {
+ Log.w("MediaController", "already removed");
+ }
+ mShowing = false;
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ };
+
+ private final Runnable mShowProgress = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ if (!mDragging && mShowing && mPlayer.isPlaying()) {
+ postDelayed(mShowProgress, 1000 - (pos % 1000));
+ }
+ }
+ };
+
+ 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) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ show(0); // show until hide is called
+ break;
+ case MotionEvent.ACTION_UP:
+ show(sDefaultTimeout); // start timeout
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ hide();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ show(sDefaultTimeout);
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ final boolean uniqueDown = event.getRepeatCount() == 0
+ && event.getAction() == KeyEvent.ACTION_DOWN;
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_SPACE) {
+ if (uniqueDown) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ }
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (uniqueDown && !mPlayer.isPlaying()) {
+ mPlayer.start();
+ updatePausePlay();
+ show(sDefaultTimeout);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (uniqueDown && mPlayer.isPlaying()) {
+ mPlayer.pause();
+ updatePausePlay();
+ show(sDefaultTimeout);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP
+ || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+ || keyCode == KeyEvent.KEYCODE_CAMERA) {
+ // don't show the controls for volume adjustment
+ return super.dispatchKeyEvent(event);
+ } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+ if (uniqueDown) {
+ hide();
+ }
+ return true;
+ }
+
+ show(sDefaultTimeout);
+ return super.dispatchKeyEvent(event);
+ }
+
+ private final View.OnClickListener mPauseListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ }
+ };
+
+ private void updatePausePlay() {
+ if (mRoot == null || mPauseButton == null)
+ return;
+
+ if (mPlayer.isPlaying()) {
+ mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause);
+ mPauseButton.setContentDescription(mPauseDescription);
+ } else {
+ mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play);
+ mPauseButton.setContentDescription(mPlayDescription);
+ }
+ }
+
+ private void doPauseResume() {
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ } else {
+ mPlayer.start();
+ }
+ updatePausePlay();
+ }
+
+ // There are two scenarios that can trigger the seekbar listener to trigger:
+ //
+ // The first is the user using the touchpad to adjust the posititon of the
+ // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+ // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+ // We're setting the field "mDragging" to true for the duration of the dragging
+ // session to avoid jumps in the position in case of ongoing playback.
+ //
+ // The second scenario involves the user operating the scroll ball, in this
+ // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+ // we will simply apply the updated position without suspending regular updates.
+ private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar bar) {
+ show(3600000);
+
+ mDragging = true;
+
+ // By removing these pending progress messages we make sure
+ // that a) we won't update the progress while the user adjusts
+ // the seekbar and b) once the user is done dragging the thumb
+ // we will post one of these messages to the queue again and
+ // this ensures that there will be exactly one message queued up.
+ removeCallbacks(mShowProgress);
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
+ if (!fromuser) {
+ // We're not interested in programmatically generated changes to
+ // the progress bar's position.
+ return;
+ }
+
+ long duration = mPlayer.getDuration();
+ long newposition = (duration * progress) / 1000L;
+ mPlayer.seekTo( (int) newposition);
+ if (mCurrentTime != null)
+ mCurrentTime.setText(stringForTime( (int) newposition));
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar bar) {
+ mDragging = false;
+ setProgress();
+ updatePausePlay();
+ show(sDefaultTimeout);
+
+ // Ensure that progress is properly updated in the future,
+ // the call to show() does not guarantee this because it is a
+ // no-op if we are already showing.
+ post(mShowProgress);
+ }
+ };
+
+ @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);
+ }
+ disableUnsupportedButtons();
+ super.setEnabled(enabled);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return MediaController.class.getName();
+ }
+
+ private final View.OnClickListener mRewListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = mPlayer.getCurrentPosition();
+ pos -= 5000; // milliseconds
+ mPlayer.seekTo(pos);
+ setProgress();
+
+ show(sDefaultTimeout);
+ }
+ };
+
+ private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ @Override
+ 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();
+ boolean canPause();
+ boolean canSeekBackward();
+ boolean canSeekForward();
+
+ /**
+ * Get the audio session id for the player used by this VideoView. This can be used to
+ * apply audio effects to the audio track of a video.
+ * @return The audio session, or 0 if there was an error.
+ */
+ int getAudioSessionId();
+ }
+}
diff --git a/android/widget/MenuItemHoverListener.java b/android/widget/MenuItemHoverListener.java
new file mode 100644
index 0000000..835e4cd
--- /dev/null
+++ b/android/widget/MenuItemHoverListener.java
@@ -0,0 +1,33 @@
+package android.widget;
+
+import android.annotation.NonNull;
+import android.view.MenuItem;
+
+import com.android.internal.view.menu.MenuBuilder;
+
+/**
+ * An interface notified when a menu item is hovered. Useful for cases when hover should trigger
+ * some behavior at a higher level, like managing the opening and closing of submenus.
+ *
+ * @hide
+ */
+public interface MenuItemHoverListener {
+ /**
+ * Called when hover exits a menu item.
+ * <p>
+ * If hover is moving to another item, this method will be called before
+ * {@link #onItemHoverEnter(MenuBuilder, MenuItem)} for the newly-hovered item.
+ *
+ * @param menu the item's parent menu
+ * @param item the hovered menu item
+ */
+ void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item);
+
+ /**
+ * Called when hover enters a menu item.
+ *
+ * @param menu the item's parent menu
+ * @param item the hovered menu item
+ */
+ void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item);
+}
diff --git a/android/widget/MenuPopupWindow.java b/android/widget/MenuPopupWindow.java
new file mode 100644
index 0000000..85e26d0
--- /dev/null
+++ b/android/widget/MenuPopupWindow.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 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.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.transition.Transition;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.view.menu.ListMenuItemView;
+import com.android.internal.view.menu.MenuAdapter;
+import com.android.internal.view.menu.MenuBuilder;
+
+/**
+ * A MenuPopupWindow represents the popup window for menu.
+ *
+ * MenuPopupWindow is mostly same as ListPopupWindow, but it has customized
+ * behaviors specific to menus,
+ *
+ * @hide
+ */
+public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener {
+ private MenuItemHoverListener mHoverListener;
+
+ public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
+ MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus);
+ view.setHoverListener(this);
+ return view;
+ }
+
+ public void setEnterTransition(Transition enterTransition) {
+ mPopup.setEnterTransition(enterTransition);
+ }
+
+ public void setExitTransition(Transition exitTransition) {
+ mPopup.setExitTransition(exitTransition);
+ }
+
+ public void setHoverListener(MenuItemHoverListener hoverListener) {
+ mHoverListener = hoverListener;
+ }
+
+ /**
+ * Set whether this window is touch modal or if outside touches will be sent to
+ * other windows behind it.
+ */
+ public void setTouchModal(boolean touchModal) {
+ mPopup.setTouchModal(touchModal);
+ }
+
+ @Override
+ public void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
+ // Forward up the chain
+ if (mHoverListener != null) {
+ mHoverListener.onItemHoverEnter(menu, item);
+ }
+ }
+
+ @Override
+ public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
+ // Forward up the chain
+ if (mHoverListener != null) {
+ mHoverListener.onItemHoverExit(menu, item);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class MenuDropDownListView extends DropDownListView {
+ final int mAdvanceKey;
+ final int mRetreatKey;
+
+ private MenuItemHoverListener mHoverListener;
+ private MenuItem mHoveredMenuItem;
+
+ public MenuDropDownListView(Context context, boolean hijackFocus) {
+ super(context, hijackFocus);
+
+ final Resources res = context.getResources();
+ final Configuration config = res.getConfiguration();
+ if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ mAdvanceKey = KeyEvent.KEYCODE_DPAD_LEFT;
+ mRetreatKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ mAdvanceKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+ mRetreatKey = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ }
+
+ public void setHoverListener(MenuItemHoverListener hoverListener) {
+ mHoverListener = hoverListener;
+ }
+
+ public void clearSelection() {
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView();
+ if (selectedItem != null && keyCode == mAdvanceKey) {
+ if (selectedItem.isEnabled() && selectedItem.getItemData().hasSubMenu()) {
+ performItemClick(
+ selectedItem,
+ getSelectedItemPosition(),
+ getSelectedItemId());
+ }
+ return true;
+ } else if (selectedItem != null && keyCode == mRetreatKey) {
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+
+ // Close only the top-level menu.
+ ((MenuAdapter) getAdapter()).getAdapterMenu().close(false /* closeAllMenus */);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent ev) {
+ // Dispatch any changes in hovered item index to the listener.
+ if (mHoverListener != null) {
+ // The adapter may be wrapped. Adjust the index if necessary.
+ final int headersCount;
+ final MenuAdapter menuAdapter;
+ final ListAdapter adapter = getAdapter();
+ if (adapter instanceof HeaderViewListAdapter) {
+ final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) adapter;
+ headersCount = headerAdapter.getHeadersCount();
+ menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter();
+ } else {
+ headersCount = 0;
+ menuAdapter = (MenuAdapter) adapter;
+ }
+
+ // Find the menu item for the view at the event coordinates.
+ MenuItem menuItem = null;
+ if (ev.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
+ final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
+ if (position != INVALID_POSITION) {
+ final int itemPosition = position - headersCount;
+ if (itemPosition >= 0 && itemPosition < menuAdapter.getCount()) {
+ menuItem = menuAdapter.getItem(itemPosition);
+ }
+ }
+ }
+
+ final MenuItem oldMenuItem = mHoveredMenuItem;
+ if (oldMenuItem != menuItem) {
+ final MenuBuilder menu = menuAdapter.getAdapterMenu();
+ if (oldMenuItem != null) {
+ mHoverListener.onItemHoverExit(menu, oldMenuItem);
+ }
+
+ mHoveredMenuItem = menuItem;
+
+ if (menuItem != null) {
+ mHoverListener.onItemHoverEnter(menu, menuItem);
+ }
+ }
+ }
+
+ return super.onHoverEvent(ev);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/widget/MultiAutoCompleteTextView.java b/android/widget/MultiAutoCompleteTextView.java
new file mode 100644
index 0000000..f348d73
--- /dev/null
+++ b/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.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.QwertyKeyListener;
+import android.util.AttributeSet;
+
+/**
+ * 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 provide a {@link Tokenizer} to distinguish the
+ * various substrings.
+ *
+ * <p>The following code snippet shows how to create a text view which suggests
+ * various countries names while the user is typing:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CountriesActivity extends Activity {
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * setContentView(R.layout.autocomplete_7);
+ *
+ * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ * android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ * MultiAutoCompleteTextView textView = 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MultiAutoCompleteTextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /* 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 || mTokenizer == null) {
+ 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 || mTokenizer == 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) {
+ clearComposingText();
+
+ 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));
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return MultiAutoCompleteTextView.class.getName();
+ }
+
+ 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/android/widget/NumberPicker.java b/android/widget/NumberPicker.java
new file mode 100644
index 0000000..4d3189e
--- /dev/null
+++ b/android/widget/NumberPicker.java
@@ -0,0 +1,2817 @@
+/*
+ * 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.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.NumberKeyListener;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.LayoutInflater.Filter;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.animation.DecelerateInterpolator;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A widget that enables the user to select a number from a predefined range.
+ * There are two flavors of this widget and which one is presented to the user
+ * depends on the current theme.
+ * <ul>
+ * <li>
+ * If the current theme is derived from {@link android.R.style#Theme} the widget
+ * presents the current value as an editable input field with an increment button
+ * above and a decrement button below. Long pressing the buttons allows for a quick
+ * change of the current value. Tapping on the input field allows to type in
+ * a desired value.
+ * </li>
+ * <li>
+ * If the current theme is derived from {@link android.R.style#Theme_Holo} or
+ * {@link android.R.style#Theme_Holo_Light} the widget presents the current
+ * value as an editable input field with a lesser value above and a greater
+ * value below. Tapping on the lesser or greater value selects it by animating
+ * the number axis up or down to make the chosen value current. Flinging up
+ * or down allows for multiple increments or decrements of the current value.
+ * Long pressing on the lesser and greater values also allows for a quick change
+ * of the current value. Tapping on the current value allows to type in a
+ * desired value.
+ * </li>
+ * </ul>
+ * <p>
+ * For an example of using this widget, see {@link android.widget.TimePicker}.
+ * </p>
+ */
+@Widget
+public class NumberPicker extends LinearLayout {
+
+ /**
+ * The number of items show in the selector wheel.
+ */
+ private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
+
+ /**
+ * The default update interval during long press.
+ */
+ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
+
+ /**
+ * The index of the middle selector item.
+ */
+ private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
+
+ /**
+ * The coefficient by which to adjust (divide) the max fling velocity.
+ */
+ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
+
+ /**
+ * The the duration for adjusting the selector wheel.
+ */
+ private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
+
+ /**
+ * The duration of scrolling while snapping to a given position.
+ */
+ private static final int SNAP_SCROLL_DURATION = 300;
+
+ /**
+ * The strength of fading in the top and bottom while drawing the selector.
+ */
+ private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
+
+ /**
+ * The default unscaled height of the selection divider.
+ */
+ private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
+
+ /**
+ * The default unscaled distance between the selection dividers.
+ */
+ private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
+
+ /**
+ * The resource id for the default layout.
+ */
+ private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker;
+
+ /**
+ * Constant for unspecified size.
+ */
+ private static final int SIZE_UNSPECIFIED = -1;
+
+ /**
+ * User choice on whether the selector wheel should be wrapped.
+ */
+ private boolean mWrapSelectorWheelPreferred = true;
+
+ /**
+ * Use a custom NumberPicker formatting callback to use two-digit minutes
+ * strings like "01". Keeping a static formatter etc. is the most efficient
+ * way to do this; it avoids creating temporary objects on every call to
+ * format().
+ */
+ private static class TwoDigitFormatter implements NumberPicker.Formatter {
+ final StringBuilder mBuilder = new StringBuilder();
+
+ char mZeroDigit;
+ java.util.Formatter mFmt;
+
+ final Object[] mArgs = new Object[1];
+
+ TwoDigitFormatter() {
+ final Locale locale = Locale.getDefault();
+ init(locale);
+ }
+
+ private void init(Locale locale) {
+ mFmt = createFormatter(locale);
+ mZeroDigit = getZeroDigit(locale);
+ }
+
+ public String format(int value) {
+ final Locale currentLocale = Locale.getDefault();
+ if (mZeroDigit != getZeroDigit(currentLocale)) {
+ init(currentLocale);
+ }
+ mArgs[0] = value;
+ mBuilder.delete(0, mBuilder.length());
+ mFmt.format("%02d", mArgs);
+ return mFmt.toString();
+ }
+
+ private static char getZeroDigit(Locale locale) {
+ return LocaleData.get(locale).zeroDigit;
+ }
+
+ private java.util.Formatter createFormatter(Locale locale) {
+ return new java.util.Formatter(mBuilder, locale);
+ }
+ }
+
+ private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter();
+
+ /**
+ * @hide
+ */
+ public static final Formatter getTwoDigitFormatter() {
+ return sTwoDigitFormatter;
+ }
+
+ /**
+ * The increment button.
+ */
+ private final ImageButton mIncrementButton;
+
+ /**
+ * The decrement button.
+ */
+ private final ImageButton mDecrementButton;
+
+ /**
+ * The text for showing the current value.
+ */
+ private final EditText mInputText;
+
+ /**
+ * The distance between the two selection dividers.
+ */
+ private final int mSelectionDividersDistance;
+
+ /**
+ * The min height of this widget.
+ */
+ private final int mMinHeight;
+
+ /**
+ * The max height of this widget.
+ */
+ private final int mMaxHeight;
+
+ /**
+ * The max width of this widget.
+ */
+ private final int mMinWidth;
+
+ /**
+ * The max width of this widget.
+ */
+ private int mMaxWidth;
+
+ /**
+ * Flag whether to compute the max width.
+ */
+ private final boolean mComputeMaxWidth;
+
+ /**
+ * The height of the text.
+ */
+ private final int mTextSize;
+
+ /**
+ * The height of the gap between text elements if the selector wheel.
+ */
+ private int mSelectorTextGapHeight;
+
+ /**
+ * The values to be displayed instead the indices.
+ */
+ private String[] mDisplayedValues;
+
+ /**
+ * Lower value of the range of numbers allowed for the NumberPicker
+ */
+ private int mMinValue;
+
+ /**
+ * Upper value of the range of numbers allowed for the NumberPicker
+ */
+ private int mMaxValue;
+
+ /**
+ * Current value of this NumberPicker
+ */
+ private int mValue;
+
+ /**
+ * Listener to be notified upon current value change.
+ */
+ private OnValueChangeListener mOnValueChangeListener;
+
+ /**
+ * Listener to be notified upon scroll state change.
+ */
+ private OnScrollListener mOnScrollListener;
+
+ /**
+ * Formatter for for displaying the current value.
+ */
+ private Formatter mFormatter;
+
+ /**
+ * The speed for updating the value form long press.
+ */
+ private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
+
+ /**
+ * Cache for the string representation of selector indices.
+ */
+ private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
+
+ /**
+ * The selector indices whose value are show by the selector.
+ */
+ private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
+
+ /**
+ * The {@link Paint} for drawing the selector.
+ */
+ private final Paint mSelectorWheelPaint;
+
+ /**
+ * The {@link Drawable} for pressed virtual (increment/decrement) buttons.
+ */
+ private final Drawable mVirtualButtonPressedDrawable;
+
+ /**
+ * The height of a selector element (text + gap).
+ */
+ private int mSelectorElementHeight;
+
+ /**
+ * The initial offset of the scroll selector.
+ */
+ private int mInitialScrollOffset = Integer.MIN_VALUE;
+
+ /**
+ * The current offset of the scroll selector.
+ */
+ private int mCurrentScrollOffset;
+
+ /**
+ * The {@link Scroller} responsible for flinging the selector.
+ */
+ private final Scroller mFlingScroller;
+
+ /**
+ * The {@link Scroller} responsible for adjusting the selector.
+ */
+ private final Scroller mAdjustScroller;
+
+ /**
+ * The previous Y coordinate while scrolling the selector.
+ */
+ private int mPreviousScrollerY;
+
+ /**
+ * Handle to the reusable command for setting the input text selection.
+ */
+ private SetSelectionCommand mSetSelectionCommand;
+
+ /**
+ * Handle to the reusable command for changing the current value from long
+ * press by one.
+ */
+ private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
+
+ /**
+ * Command for beginning an edit of the current value via IME on long press.
+ */
+ private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
+
+ /**
+ * The Y position of the last down event.
+ */
+ private float mLastDownEventY;
+
+ /**
+ * The time of the last down event.
+ */
+ private long mLastDownEventTime;
+
+ /**
+ * The Y position of the last down or move event.
+ */
+ private float mLastDownOrMoveEventY;
+
+ /**
+ * Determines speed during touch scrolling.
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * @see ViewConfiguration#getScaledTouchSlop()
+ */
+ private int mTouchSlop;
+
+ /**
+ * @see ViewConfiguration#getScaledMinimumFlingVelocity()
+ */
+ private int mMinimumFlingVelocity;
+
+ /**
+ * @see ViewConfiguration#getScaledMaximumFlingVelocity()
+ */
+ private int mMaximumFlingVelocity;
+
+ /**
+ * Flag whether the selector should wrap around.
+ */
+ private boolean mWrapSelectorWheel;
+
+ /**
+ * The back ground color used to optimize scroller fading.
+ */
+ private final int mSolidColor;
+
+ /**
+ * Flag whether this widget has a selector wheel.
+ */
+ private final boolean mHasSelectorWheel;
+
+ /**
+ * Divider for showing item to be selected while scrolling
+ */
+ private final Drawable mSelectionDivider;
+
+ /**
+ * The height of the selection divider.
+ */
+ private final int mSelectionDividerHeight;
+
+ /**
+ * The current scroll state of the number picker.
+ */
+ private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+ /**
+ * Flag whether to ignore move events - we ignore such when we show in IME
+ * to prevent the content from scrolling.
+ */
+ private boolean mIgnoreMoveEvents;
+
+ /**
+ * Flag whether to perform a click on tap.
+ */
+ private boolean mPerformClickOnTap;
+
+ /**
+ * The top of the top selection divider.
+ */
+ private int mTopSelectionDividerTop;
+
+ /**
+ * The bottom of the bottom selection divider.
+ */
+ private int mBottomSelectionDividerBottom;
+
+ /**
+ * The virtual id of the last hovered child.
+ */
+ private int mLastHoveredChildVirtualViewId;
+
+ /**
+ * Whether the increment virtual button is pressed.
+ */
+ private boolean mIncrementVirtualButtonPressed;
+
+ /**
+ * Whether the decrement virtual button is pressed.
+ */
+ private boolean mDecrementVirtualButtonPressed;
+
+ /**
+ * Provider to report to clients the semantic structure of this widget.
+ */
+ private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
+
+ /**
+ * Helper class for managing pressed state of the virtual buttons.
+ */
+ private final PressedStateHelper mPressedStateHelper;
+
+ /**
+ * The keycode of the last handled DPAD down event.
+ */
+ private int mLastHandledDownDpadKeyCode = -1;
+
+ /**
+ * If true then the selector wheel is hidden until the picker has focus.
+ */
+ private boolean mHideWheelUntilFocused;
+
+ /**
+ * Interface to listen for changes of the current value.
+ */
+ public interface OnValueChangeListener {
+
+ /**
+ * Called upon a change of the current value.
+ *
+ * @param picker The NumberPicker associated with this listener.
+ * @param oldVal The previous value.
+ * @param newVal The new value.
+ */
+ void onValueChange(NumberPicker picker, int oldVal, int newVal);
+ }
+
+ /**
+ * Interface to listen for the picker scroll state.
+ */
+ public interface OnScrollListener {
+ /** @hide */
+ @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScrollState {}
+
+ /**
+ * The view is not scrolling.
+ */
+ public static int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * The user is scrolling using touch, and his finger is still on the screen.
+ */
+ public static int SCROLL_STATE_TOUCH_SCROLL = 1;
+
+ /**
+ * The user had previously been scrolling using touch and performed a fling.
+ */
+ public static int SCROLL_STATE_FLING = 2;
+
+ /**
+ * Callback invoked while the number picker scroll state has changed.
+ *
+ * @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 onScrollStateChange(NumberPicker view, @ScrollState int scrollState);
+ }
+
+ /**
+ * Interface used to format current value into a string for presentation.
+ */
+ public interface Formatter {
+
+ /**
+ * Formats a string representation of the current value.
+ *
+ * @param value The currently selected value.
+ * @return A formatted string representation.
+ */
+ public String format(int value);
+ }
+
+ /**
+ * Create a new number picker.
+ *
+ * @param context The application environment.
+ */
+ public NumberPicker(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Create a new number picker.
+ *
+ * @param context The application environment.
+ * @param attrs A collection of attributes.
+ */
+ public NumberPicker(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.numberPickerStyle);
+ }
+
+ /**
+ * Create a new number picker
+ *
+ * @param context the application environment.
+ * @param attrs a collection of attributes.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Create a new number picker
+ *
+ * @param context the application environment.
+ * @param attrs a collection of attributes.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
+ */
+ public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // process style attributes
+ final TypedArray attributesArray = context.obtainStyledAttributes(
+ attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes);
+ final int layoutResId = attributesArray.getResourceId(
+ R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID);
+
+ mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
+
+ mHideWheelUntilFocused = attributesArray.getBoolean(
+ R.styleable.NumberPicker_hideWheelUntilFocused, false);
+
+ mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
+
+ final Drawable selectionDivider = attributesArray.getDrawable(
+ R.styleable.NumberPicker_selectionDivider);
+ if (selectionDivider != null) {
+ selectionDivider.setCallback(this);
+ selectionDivider.setLayoutDirection(getLayoutDirection());
+ if (selectionDivider.isStateful()) {
+ selectionDivider.setState(getDrawableState());
+ }
+ }
+ mSelectionDivider = selectionDivider;
+
+ final int defSelectionDividerHeight = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
+ getResources().getDisplayMetrics());
+ mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
+
+ final int defSelectionDividerDistance = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
+ getResources().getDisplayMetrics());
+ mSelectionDividersDistance = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance);
+
+ mMinHeight = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED);
+
+ mMaxHeight = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED);
+ if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
+ && mMinHeight > mMaxHeight) {
+ throw new IllegalArgumentException("minHeight > maxHeight");
+ }
+
+ mMinWidth = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED);
+
+ mMaxWidth = attributesArray.getDimensionPixelSize(
+ R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED);
+ if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
+ && mMinWidth > mMaxWidth) {
+ throw new IllegalArgumentException("minWidth > maxWidth");
+ }
+
+ mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
+
+ mVirtualButtonPressedDrawable = attributesArray.getDrawable(
+ R.styleable.NumberPicker_virtualButtonPressedDrawable);
+
+ attributesArray.recycle();
+
+ mPressedStateHelper = new PressedStateHelper();
+
+ // By default Linearlayout that we extend is not drawn. This is
+ // its draw() method is not called but dispatchDraw() is called
+ // directly (see ViewGroup.drawChild()). However, this class uses
+ // the fading edge effect implemented by View and we need our
+ // draw() method to be called. Therefore, we declare we will draw.
+ setWillNotDraw(!mHasSelectorWheel);
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(layoutResId, this, true);
+
+ OnClickListener onClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ hideSoftInput();
+ mInputText.clearFocus();
+ if (v.getId() == R.id.increment) {
+ changeValueByOne(true);
+ } else {
+ changeValueByOne(false);
+ }
+ }
+ };
+
+ OnLongClickListener onLongClickListener = new OnLongClickListener() {
+ public boolean onLongClick(View v) {
+ hideSoftInput();
+ mInputText.clearFocus();
+ if (v.getId() == R.id.increment) {
+ postChangeCurrentByOneFromLongPress(true, 0);
+ } else {
+ postChangeCurrentByOneFromLongPress(false, 0);
+ }
+ return true;
+ }
+ };
+
+ // increment button
+ if (!mHasSelectorWheel) {
+ mIncrementButton = findViewById(R.id.increment);
+ mIncrementButton.setOnClickListener(onClickListener);
+ mIncrementButton.setOnLongClickListener(onLongClickListener);
+ } else {
+ mIncrementButton = null;
+ }
+
+ // decrement button
+ if (!mHasSelectorWheel) {
+ mDecrementButton = findViewById(R.id.decrement);
+ mDecrementButton.setOnClickListener(onClickListener);
+ mDecrementButton.setOnLongClickListener(onLongClickListener);
+ } else {
+ mDecrementButton = null;
+ }
+
+ // input text
+ mInputText = findViewById(R.id.numberpicker_input);
+ mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ mInputText.selectAll();
+ } else {
+ mInputText.setSelection(0, 0);
+ validateInputTextView(v);
+ }
+ }
+ });
+ mInputText.setFilters(new InputFilter[] {
+ new InputTextFilter()
+ });
+ mInputText.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+
+ mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+ mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+
+ // initialize constants
+ ViewConfiguration configuration = ViewConfiguration.get(context);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
+ / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
+ mTextSize = (int) mInputText.getTextSize();
+
+ // create the selector wheel paint
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTextAlign(Align.CENTER);
+ paint.setTextSize(mTextSize);
+ paint.setTypeface(mInputText.getTypeface());
+ ColorStateList colors = mInputText.getTextColors();
+ int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
+ paint.setColor(color);
+ mSelectorWheelPaint = paint;
+
+ // create the fling and adjust scrollers
+ mFlingScroller = new Scroller(getContext(), null, true);
+ mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
+
+ updateInputTextView();
+
+ // If not explicitly specified this view is important for accessibility.
+ if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ // Should be focusable by default, as the text view whose visibility changes is focusable
+ if (getFocusable() == View.FOCUSABLE_AUTO) {
+ setFocusable(View.FOCUSABLE);
+ setFocusableInTouchMode(true);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (!mHasSelectorWheel) {
+ super.onLayout(changed, left, top, right, bottom);
+ return;
+ }
+ final int msrdWdth = getMeasuredWidth();
+ final int msrdHght = getMeasuredHeight();
+
+ // Input text centered horizontally.
+ final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
+ final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
+ final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
+ final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
+ final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
+ final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
+ mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
+
+ if (changed) {
+ // need to do all this when we know our size
+ initializeSelectorWheel();
+ initializeFadingEdges();
+ mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
+ - mSelectionDividerHeight;
+ mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
+ + mSelectionDividersDistance;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!mHasSelectorWheel) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ // Try greedily to fit the max width and height.
+ final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
+ final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
+ super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
+ // Flag if we are measured with width or height less than the respective min.
+ final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
+ widthMeasureSpec);
+ final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
+ heightMeasureSpec);
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ /**
+ * Move to the final position of a scroller. Ensures to force finish the scroller
+ * and if it is not at its final position a scroll of the selector wheel is
+ * performed to fast forward to the final position.
+ *
+ * @param scroller The scroller to whose final position to get.
+ * @return True of the a move was performed, i.e. the scroller was not in final position.
+ */
+ private boolean moveToFinalScrollerPosition(Scroller scroller) {
+ scroller.forceFinished(true);
+ int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
+ int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
+ int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
+ if (overshootAdjustment != 0) {
+ if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
+ if (overshootAdjustment > 0) {
+ overshootAdjustment -= mSelectorElementHeight;
+ } else {
+ overshootAdjustment += mSelectorElementHeight;
+ }
+ }
+ amountToScroll += overshootAdjustment;
+ scrollBy(0, amountToScroll);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (!mHasSelectorWheel || !isEnabled()) {
+ return false;
+ }
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ removeAllCallbacks();
+ hideSoftInput();
+ mLastDownOrMoveEventY = mLastDownEventY = event.getY();
+ mLastDownEventTime = event.getEventTime();
+ mIgnoreMoveEvents = false;
+ mPerformClickOnTap = false;
+ // Handle pressed state before any state change.
+ if (mLastDownEventY < mTopSelectionDividerTop) {
+ if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+ mPressedStateHelper.buttonPressDelayed(
+ PressedStateHelper.BUTTON_DECREMENT);
+ }
+ } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
+ if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+ mPressedStateHelper.buttonPressDelayed(
+ PressedStateHelper.BUTTON_INCREMENT);
+ }
+ }
+ // Make sure we support flinging inside scrollables.
+ getParent().requestDisallowInterceptTouchEvent(true);
+ if (!mFlingScroller.isFinished()) {
+ mFlingScroller.forceFinished(true);
+ mAdjustScroller.forceFinished(true);
+ onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ } else if (!mAdjustScroller.isFinished()) {
+ mFlingScroller.forceFinished(true);
+ mAdjustScroller.forceFinished(true);
+ } else if (mLastDownEventY < mTopSelectionDividerTop) {
+ postChangeCurrentByOneFromLongPress(
+ false, ViewConfiguration.getLongPressTimeout());
+ } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
+ postChangeCurrentByOneFromLongPress(
+ true, ViewConfiguration.getLongPressTimeout());
+ } else {
+ mPerformClickOnTap = true;
+ postBeginSoftInputOnLongPressCommand();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!isEnabled() || !mHasSelectorWheel) {
+ return false;
+ }
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(event);
+ int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_MOVE: {
+ if (mIgnoreMoveEvents) {
+ break;
+ }
+ float currentMoveY = event.getY();
+ if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
+ if (deltaDownY > mTouchSlop) {
+ removeAllCallbacks();
+ onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+ } else {
+ int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
+ scrollBy(0, deltaMoveY);
+ invalidate();
+ }
+ mLastDownOrMoveEventY = currentMoveY;
+ } break;
+ case MotionEvent.ACTION_UP: {
+ removeBeginSoftInputCommand();
+ removeChangeCurrentByOneFromLongPress();
+ mPressedStateHelper.cancel();
+ VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ int initialVelocity = (int) velocityTracker.getYVelocity();
+ if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
+ fling(initialVelocity);
+ onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ } else {
+ int eventY = (int) event.getY();
+ int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
+ long deltaTime = event.getEventTime() - mLastDownEventTime;
+ if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
+ if (mPerformClickOnTap) {
+ mPerformClickOnTap = false;
+ performClick();
+ } else {
+ int selectorIndexOffset = (eventY / mSelectorElementHeight)
+ - SELECTOR_MIDDLE_ITEM_INDEX;
+ if (selectorIndexOffset > 0) {
+ changeValueByOne(true);
+ mPressedStateHelper.buttonTapped(
+ PressedStateHelper.BUTTON_INCREMENT);
+ } else if (selectorIndexOffset < 0) {
+ changeValueByOne(false);
+ mPressedStateHelper.buttonTapped(
+ PressedStateHelper.BUTTON_DECREMENT);
+ }
+ }
+ } else {
+ ensureScrollWheelAdjusted();
+ }
+ onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ } break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ removeAllCallbacks();
+ break;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ final int keyCode = event.getKeyCode();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ removeAllCallbacks();
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!mHasSelectorWheel) {
+ break;
+ }
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
+ ? getValue() < getMaxValue() : getValue() > getMinValue())) {
+ requestFocus();
+ mLastHandledDownDpadKeyCode = keyCode;
+ removeAllCallbacks();
+ if (mFlingScroller.isFinished()) {
+ changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ return true;
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ if (mLastHandledDownDpadKeyCode == keyCode) {
+ mLastHandledDownDpadKeyCode = -1;
+ return true;
+ }
+ break;
+ }
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ removeAllCallbacks();
+ break;
+ }
+ return super.dispatchTrackballEvent(event);
+ }
+
+ @Override
+ protected boolean dispatchHoverEvent(MotionEvent event) {
+ if (!mHasSelectorWheel) {
+ return super.dispatchHoverEvent(event);
+ }
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ final int eventY = (int) event.getY();
+ final int hoveredVirtualViewId;
+ if (eventY < mTopSelectionDividerTop) {
+ hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
+ } else if (eventY > mBottomSelectionDividerBottom) {
+ hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
+ } else {
+ hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
+ }
+ final int action = event.getActionMasked();
+ AccessibilityNodeProviderImpl provider =
+ (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER: {
+ provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+ AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
+ provider.performAction(hoveredVirtualViewId,
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ } break;
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
+ && mLastHoveredChildVirtualViewId != View.NO_ID) {
+ provider.sendAccessibilityEventForVirtualView(
+ mLastHoveredChildVirtualViewId,
+ AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+ AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
+ provider.performAction(hoveredVirtualViewId,
+ AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+ }
+ } break;
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+ AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ mLastHoveredChildVirtualViewId = View.NO_ID;
+ } break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void computeScroll() {
+ Scroller scroller = mFlingScroller;
+ if (scroller.isFinished()) {
+ scroller = mAdjustScroller;
+ if (scroller.isFinished()) {
+ return;
+ }
+ }
+ scroller.computeScrollOffset();
+ int currentScrollerY = scroller.getCurrY();
+ if (mPreviousScrollerY == 0) {
+ mPreviousScrollerY = scroller.getStartY();
+ }
+ scrollBy(0, currentScrollerY - mPreviousScrollerY);
+ mPreviousScrollerY = currentScrollerY;
+ if (scroller.isFinished()) {
+ onScrollerFinished(scroller);
+ } else {
+ invalidate();
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (!mHasSelectorWheel) {
+ mIncrementButton.setEnabled(enabled);
+ }
+ if (!mHasSelectorWheel) {
+ mDecrementButton.setEnabled(enabled);
+ }
+ mInputText.setEnabled(enabled);
+ }
+
+ @Override
+ public void scrollBy(int x, int y) {
+ int[] selectorIndices = mSelectorIndices;
+ int startScrollOffset = mCurrentScrollOffset;
+ if (!mWrapSelectorWheel && y > 0
+ && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
+ mCurrentScrollOffset = mInitialScrollOffset;
+ return;
+ }
+ if (!mWrapSelectorWheel && y < 0
+ && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
+ mCurrentScrollOffset = mInitialScrollOffset;
+ return;
+ }
+ mCurrentScrollOffset += y;
+ while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
+ mCurrentScrollOffset -= mSelectorElementHeight;
+ decrementSelectorIndices(selectorIndices);
+ setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
+ if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
+ mCurrentScrollOffset = mInitialScrollOffset;
+ }
+ }
+ while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
+ mCurrentScrollOffset += mSelectorElementHeight;
+ incrementSelectorIndices(selectorIndices);
+ setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
+ if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
+ mCurrentScrollOffset = mInitialScrollOffset;
+ }
+ }
+ if (startScrollOffset != mCurrentScrollOffset) {
+ onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);
+ }
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return mCurrentScrollOffset;
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return (mMaxValue - mMinValue + 1) * mSelectorElementHeight;
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ return getHeight();
+ }
+
+ @Override
+ public int getSolidColor() {
+ return mSolidColor;
+ }
+
+ /**
+ * Sets the listener to be notified on change of the current value.
+ *
+ * @param onValueChangedListener The listener.
+ */
+ public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
+ mOnValueChangeListener = onValueChangedListener;
+ }
+
+ /**
+ * Set listener to be notified for scroll state changes.
+ *
+ * @param onScrollListener The listener.
+ */
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
+ mOnScrollListener = onScrollListener;
+ }
+
+ /**
+ * Set the formatter to be used for formatting the current value.
+ * <p>
+ * Note: If you have provided alternative values for the values this
+ * formatter is never invoked.
+ * </p>
+ *
+ * @param formatter The formatter object. If formatter is <code>null</code>,
+ * {@link String#valueOf(int)} will be used.
+ *@see #setDisplayedValues(String[])
+ */
+ public void setFormatter(Formatter formatter) {
+ if (formatter == mFormatter) {
+ return;
+ }
+ mFormatter = formatter;
+ initializeSelectorWheelIndices();
+ updateInputTextView();
+ }
+
+ /**
+ * Set the current value for the number picker.
+ * <p>
+ * If the argument is less than the {@link NumberPicker#getMinValue()} and
+ * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
+ * current value is set to the {@link NumberPicker#getMinValue()} value.
+ * </p>
+ * <p>
+ * If the argument is less than the {@link NumberPicker#getMinValue()} and
+ * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
+ * current value is set to the {@link NumberPicker#getMaxValue()} value.
+ * </p>
+ * <p>
+ * If the argument is less than the {@link NumberPicker#getMaxValue()} and
+ * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
+ * current value is set to the {@link NumberPicker#getMaxValue()} value.
+ * </p>
+ * <p>
+ * If the argument is less than the {@link NumberPicker#getMaxValue()} and
+ * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
+ * current value is set to the {@link NumberPicker#getMinValue()} value.
+ * </p>
+ *
+ * @param value The current value.
+ * @see #setWrapSelectorWheel(boolean)
+ * @see #setMinValue(int)
+ * @see #setMaxValue(int)
+ */
+ public void setValue(int value) {
+ setValueInternal(value, false);
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!mHasSelectorWheel) {
+ return super.performClick();
+ } else if (!super.performClick()) {
+ showSoftInput();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean performLongClick() {
+ if (!mHasSelectorWheel) {
+ return super.performLongClick();
+ } else if (!super.performLongClick()) {
+ showSoftInput();
+ mIgnoreMoveEvents = true;
+ }
+ return true;
+ }
+
+ /**
+ * Shows the soft input for its input text.
+ */
+ private void showSoftInput() {
+ InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+ if (inputMethodManager != null) {
+ if (mHasSelectorWheel) {
+ mInputText.setVisibility(View.VISIBLE);
+ }
+ mInputText.requestFocus();
+ inputMethodManager.showSoftInput(mInputText, 0);
+ }
+ }
+
+ /**
+ * Hides the soft input if it is active for the input text.
+ */
+ private void hideSoftInput() {
+ InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+ if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
+ inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ if (mHasSelectorWheel) {
+ mInputText.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Computes the max width if no such specified as an attribute.
+ */
+ private void tryComputeMaxWidth() {
+ if (!mComputeMaxWidth) {
+ return;
+ }
+ int maxTextWidth = 0;
+ if (mDisplayedValues == null) {
+ float maxDigitWidth = 0;
+ for (int i = 0; i <= 9; i++) {
+ final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
+ if (digitWidth > maxDigitWidth) {
+ maxDigitWidth = digitWidth;
+ }
+ }
+ int numberOfDigits = 0;
+ int current = mMaxValue;
+ while (current > 0) {
+ numberOfDigits++;
+ current = current / 10;
+ }
+ maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
+ } else {
+ final int valueCount = mDisplayedValues.length;
+ for (int i = 0; i < valueCount; i++) {
+ final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
+ if (textWidth > maxTextWidth) {
+ maxTextWidth = (int) textWidth;
+ }
+ }
+ }
+ maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
+ if (mMaxWidth != maxTextWidth) {
+ if (maxTextWidth > mMinWidth) {
+ mMaxWidth = maxTextWidth;
+ } else {
+ mMaxWidth = mMinWidth;
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Gets whether the selector wheel wraps when reaching the min/max value.
+ *
+ * @return True if the selector wheel wraps.
+ *
+ * @see #getMinValue()
+ * @see #getMaxValue()
+ */
+ public boolean getWrapSelectorWheel() {
+ return mWrapSelectorWheel;
+ }
+
+ /**
+ * Sets whether the selector wheel shown during flinging/scrolling should
+ * wrap around the {@link NumberPicker#getMinValue()} and
+ * {@link NumberPicker#getMaxValue()} values.
+ * <p>
+ * By default if the range (max - min) is more than the number of items shown
+ * on the selector wheel the selector wheel wrapping is enabled.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> If the number of items, i.e. the range (
+ * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
+ * the number of items shown on the selector wheel, the selector wheel will
+ * not wrap. Hence, in such a case calling this method is a NOP.
+ * </p>
+ *
+ * @param wrapSelectorWheel Whether to wrap.
+ */
+ public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
+ mWrapSelectorWheelPreferred = wrapSelectorWheel;
+ updateWrapSelectorWheel();
+
+ }
+
+ /**
+ * Whether or not the selector wheel should be wrapped is determined by user choice and whether
+ * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the
+ * latter is calculated based on min & max value set vs selector's visual length. Therefore,
+ * this method should be called any time any of the 3 values (i.e. user choice, min and max
+ * value) gets updated.
+ */
+ private void updateWrapSelectorWheel() {
+ final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
+ mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred;
+ }
+
+ /**
+ * Sets the speed at which the numbers be incremented and decremented when
+ * the up and down buttons are long pressed respectively.
+ * <p>
+ * The default value is 300 ms.
+ * </p>
+ *
+ * @param intervalMillis The speed (in milliseconds) at which the numbers
+ * will be incremented and decremented.
+ */
+ public void setOnLongPressUpdateInterval(long intervalMillis) {
+ mLongPressUpdateInterval = intervalMillis;
+ }
+
+ /**
+ * Returns the value of the picker.
+ *
+ * @return The value.
+ */
+ public int getValue() {
+ return mValue;
+ }
+
+ /**
+ * Returns the min value of the picker.
+ *
+ * @return The min value
+ */
+ public int getMinValue() {
+ return mMinValue;
+ }
+
+ /**
+ * Sets the min value of the picker.
+ *
+ * @param minValue The min value inclusive.
+ *
+ * <strong>Note:</strong> The length of the displayed values array
+ * set via {@link #setDisplayedValues(String[])} must be equal to the
+ * range of selectable numbers which is equal to
+ * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+ */
+ public void setMinValue(int minValue) {
+ if (mMinValue == minValue) {
+ return;
+ }
+ if (minValue < 0) {
+ throw new IllegalArgumentException("minValue must be >= 0");
+ }
+ mMinValue = minValue;
+ if (mMinValue > mValue) {
+ mValue = mMinValue;
+ }
+ updateWrapSelectorWheel();
+ initializeSelectorWheelIndices();
+ updateInputTextView();
+ tryComputeMaxWidth();
+ invalidate();
+ }
+
+ /**
+ * Returns the max value of the picker.
+ *
+ * @return The max value.
+ */
+ public int getMaxValue() {
+ return mMaxValue;
+ }
+
+ /**
+ * Sets the max value of the picker.
+ *
+ * @param maxValue The max value inclusive.
+ *
+ * <strong>Note:</strong> The length of the displayed values array
+ * set via {@link #setDisplayedValues(String[])} must be equal to the
+ * range of selectable numbers which is equal to
+ * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+ */
+ public void setMaxValue(int maxValue) {
+ if (mMaxValue == maxValue) {
+ return;
+ }
+ if (maxValue < 0) {
+ throw new IllegalArgumentException("maxValue must be >= 0");
+ }
+ mMaxValue = maxValue;
+ if (mMaxValue < mValue) {
+ mValue = mMaxValue;
+ }
+ updateWrapSelectorWheel();
+ initializeSelectorWheelIndices();
+ updateInputTextView();
+ tryComputeMaxWidth();
+ invalidate();
+ }
+
+ /**
+ * Gets the values to be displayed instead of string values.
+ *
+ * @return The displayed values.
+ */
+ public String[] getDisplayedValues() {
+ return mDisplayedValues;
+ }
+
+ /**
+ * Sets the values to be displayed.
+ *
+ * @param displayedValues The displayed values.
+ *
+ * <strong>Note:</strong> The length of the displayed values array
+ * must be equal to the range of selectable numbers which is equal to
+ * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+ */
+ public void setDisplayedValues(String[] displayedValues) {
+ if (mDisplayedValues == displayedValues) {
+ return;
+ }
+ mDisplayedValues = displayedValues;
+ if (mDisplayedValues != null) {
+ // Allow text entry rather than strictly numeric entry.
+ mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ } else {
+ mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+ }
+ updateInputTextView();
+ initializeSelectorWheelIndices();
+ tryComputeMaxWidth();
+ }
+
+ /**
+ * Retrieves the displayed value for the current selection in this picker.
+ *
+ * @hide
+ */
+ @TestApi
+ public CharSequence getDisplayedValueForCurrentSelection() {
+ // The cache field itself is initialized at declaration time, and since it's final, it
+ // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is
+ // called, directly or indirectly, on every call to setDisplayedValues, setFormatter,
+ // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the
+ // picker. As such, the contents of the cache are always synced to the latest state of
+ // the widget.
+ return mSelectorIndexToStringCache.get(getValue());
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeAllCallbacks();
+ }
+
+ @CallSuper
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final Drawable selectionDivider = mSelectionDivider;
+ if (selectionDivider != null && selectionDivider.isStateful()
+ && selectionDivider.setState(getDrawableState())) {
+ invalidateDrawable(selectionDivider);
+ }
+ }
+
+ @CallSuper
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+
+ if (mSelectionDivider != null) {
+ mSelectionDivider.jumpToCurrentState();
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
+ super.onResolveDrawables(layoutDirection);
+
+ if (mSelectionDivider != null) {
+ mSelectionDivider.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (!mHasSelectorWheel) {
+ super.onDraw(canvas);
+ return;
+ }
+ final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true;
+ float x = (mRight - mLeft) / 2;
+ float y = mCurrentScrollOffset;
+
+ // draw the virtual buttons pressed state if needed
+ if (showSelectorWheel && mVirtualButtonPressedDrawable != null
+ && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+ if (mDecrementVirtualButtonPressed) {
+ mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
+ mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
+ mVirtualButtonPressedDrawable.draw(canvas);
+ }
+ if (mIncrementVirtualButtonPressed) {
+ mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
+ mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
+ mBottom);
+ mVirtualButtonPressedDrawable.draw(canvas);
+ }
+ }
+
+ // draw the selector wheel
+ int[] selectorIndices = mSelectorIndices;
+ for (int i = 0; i < selectorIndices.length; i++) {
+ int selectorIndex = selectorIndices[i];
+ String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
+ // Do not draw the middle item if input is visible since the input
+ // is shown only if the wheel is static and it covers the middle
+ // item. Otherwise, if the user starts editing the text via the
+ // IME he may see a dimmed version of the old value intermixed
+ // with the new one.
+ if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
+ (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
+ canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
+ }
+ y += mSelectorElementHeight;
+ }
+
+ // draw the selection dividers
+ if (showSelectorWheel && mSelectionDivider != null) {
+ // draw the top divider
+ int topOfTopDivider = mTopSelectionDividerTop;
+ int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
+ mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
+ mSelectionDivider.draw(canvas);
+
+ // draw the bottom divider
+ int bottomOfBottomDivider = mBottomSelectionDividerBottom;
+ int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
+ mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
+ mSelectionDivider.draw(canvas);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setClassName(NumberPicker.class.getName());
+ event.setScrollable(true);
+ event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
+ event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
+ }
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ if (!mHasSelectorWheel) {
+ return super.getAccessibilityNodeProvider();
+ }
+ if (mAccessibilityNodeProvider == null) {
+ mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
+ }
+ return mAccessibilityNodeProvider;
+ }
+
+ /**
+ * Makes a measure spec that tries greedily to use the max value.
+ *
+ * @param measureSpec The measure spec.
+ * @param maxSize The max value for the size.
+ * @return A measure spec greedily imposing the max size.
+ */
+ private int makeMeasureSpec(int measureSpec, int maxSize) {
+ if (maxSize == SIZE_UNSPECIFIED) {
+ return measureSpec;
+ }
+ final int size = MeasureSpec.getSize(measureSpec);
+ final int mode = MeasureSpec.getMode(measureSpec);
+ switch (mode) {
+ case MeasureSpec.EXACTLY:
+ return measureSpec;
+ case MeasureSpec.AT_MOST:
+ return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
+ case MeasureSpec.UNSPECIFIED:
+ return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
+ default:
+ throw new IllegalArgumentException("Unknown measure mode: " + mode);
+ }
+ }
+
+ /**
+ * Utility to reconcile a desired size and state, with constraints imposed
+ * by a MeasureSpec. Tries to respect the min size, unless a different size
+ * is imposed by the constraints.
+ *
+ * @param minSize The minimal desired size.
+ * @param measuredSize The currently measured size.
+ * @param measureSpec The current measure spec.
+ * @return The resolved size and state.
+ */
+ private int resolveSizeAndStateRespectingMinSize(
+ int minSize, int measuredSize, int measureSpec) {
+ if (minSize != SIZE_UNSPECIFIED) {
+ final int desiredWidth = Math.max(minSize, measuredSize);
+ return resolveSizeAndState(desiredWidth, measureSpec, 0);
+ } else {
+ return measuredSize;
+ }
+ }
+
+ /**
+ * Resets the selector indices and clear the cached string representation of
+ * these indices.
+ */
+ private void initializeSelectorWheelIndices() {
+ mSelectorIndexToStringCache.clear();
+ int[] selectorIndices = mSelectorIndices;
+ int current = getValue();
+ for (int i = 0; i < mSelectorIndices.length; i++) {
+ int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
+ if (mWrapSelectorWheel) {
+ selectorIndex = getWrappedSelectorIndex(selectorIndex);
+ }
+ selectorIndices[i] = selectorIndex;
+ ensureCachedScrollSelectorValue(selectorIndices[i]);
+ }
+ }
+
+ /**
+ * Sets the current value of this NumberPicker.
+ *
+ * @param current The new value of the NumberPicker.
+ * @param notifyChange Whether to notify if the current value changed.
+ */
+ private void setValueInternal(int current, boolean notifyChange) {
+ if (mValue == current) {
+ return;
+ }
+ // Wrap around the values if we go past the start or end
+ if (mWrapSelectorWheel) {
+ current = getWrappedSelectorIndex(current);
+ } else {
+ current = Math.max(current, mMinValue);
+ current = Math.min(current, mMaxValue);
+ }
+ int previous = mValue;
+ mValue = current;
+ // If we're flinging, we'll update the text view at the end when it becomes visible
+ if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
+ updateInputTextView();
+ }
+ if (notifyChange) {
+ notifyChange(previous, current);
+ }
+ initializeSelectorWheelIndices();
+ invalidate();
+ }
+
+ /**
+ * Changes the current value by one which is increment or
+ * decrement based on the passes argument.
+ * decrement the current value.
+ *
+ * @param increment True to increment, false to decrement.
+ */
+ private void changeValueByOne(boolean increment) {
+ if (mHasSelectorWheel) {
+ hideSoftInput();
+ if (!moveToFinalScrollerPosition(mFlingScroller)) {
+ moveToFinalScrollerPosition(mAdjustScroller);
+ }
+ mPreviousScrollerY = 0;
+ if (increment) {
+ mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
+ } else {
+ mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
+ }
+ invalidate();
+ } else {
+ if (increment) {
+ setValueInternal(mValue + 1, true);
+ } else {
+ setValueInternal(mValue - 1, true);
+ }
+ }
+ }
+
+ private void initializeSelectorWheel() {
+ initializeSelectorWheelIndices();
+ int[] selectorIndices = mSelectorIndices;
+ int totalTextHeight = selectorIndices.length * mTextSize;
+ float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
+ float textGapCount = selectorIndices.length;
+ mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
+ mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
+ // Ensure that the middle item is positioned the same as the text in
+ // mInputText
+ int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
+ mInitialScrollOffset = editTextTextPosition
+ - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
+ mCurrentScrollOffset = mInitialScrollOffset;
+ updateInputTextView();
+ }
+
+ private void initializeFadingEdges() {
+ setVerticalFadingEdgeEnabled(true);
+ setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
+ }
+
+ /**
+ * Callback invoked upon completion of a given <code>scroller</code>.
+ */
+ private void onScrollerFinished(Scroller scroller) {
+ if (scroller == mFlingScroller) {
+ ensureScrollWheelAdjusted();
+ updateInputTextView();
+ onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ } else {
+ if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+ updateInputTextView();
+ }
+ }
+ }
+
+ /**
+ * Handles transition to a given <code>scrollState</code>
+ */
+ private void onScrollStateChange(int scrollState) {
+ if (mScrollState == scrollState) {
+ return;
+ }
+ mScrollState = scrollState;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChange(this, scrollState);
+ }
+ }
+
+ /**
+ * Flings the selector with the given <code>velocityY</code>.
+ */
+ private void fling(int velocityY) {
+ mPreviousScrollerY = 0;
+
+ if (velocityY > 0) {
+ mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
+ } else {
+ mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
+ }
+
+ invalidate();
+ }
+
+ /**
+ * @return The wrapped index <code>selectorIndex</code> value.
+ */
+ private int getWrappedSelectorIndex(int selectorIndex) {
+ if (selectorIndex > mMaxValue) {
+ return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
+ } else if (selectorIndex < mMinValue) {
+ return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
+ }
+ return selectorIndex;
+ }
+
+ /**
+ * Increments the <code>selectorIndices</code> whose string representations
+ * will be displayed in the selector.
+ */
+ private void incrementSelectorIndices(int[] selectorIndices) {
+ for (int i = 0; i < selectorIndices.length - 1; i++) {
+ selectorIndices[i] = selectorIndices[i + 1];
+ }
+ int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
+ if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
+ nextScrollSelectorIndex = mMinValue;
+ }
+ selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
+ ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
+ }
+
+ /**
+ * Decrements the <code>selectorIndices</code> whose string representations
+ * will be displayed in the selector.
+ */
+ private void decrementSelectorIndices(int[] selectorIndices) {
+ for (int i = selectorIndices.length - 1; i > 0; i--) {
+ selectorIndices[i] = selectorIndices[i - 1];
+ }
+ int nextScrollSelectorIndex = selectorIndices[1] - 1;
+ if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
+ nextScrollSelectorIndex = mMaxValue;
+ }
+ selectorIndices[0] = nextScrollSelectorIndex;
+ ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
+ }
+
+ /**
+ * Ensures we have a cached string representation of the given <code>
+ * selectorIndex</code> to avoid multiple instantiations of the same string.
+ */
+ private void ensureCachedScrollSelectorValue(int selectorIndex) {
+ SparseArray<String> cache = mSelectorIndexToStringCache;
+ String scrollSelectorValue = cache.get(selectorIndex);
+ if (scrollSelectorValue != null) {
+ return;
+ }
+ if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
+ scrollSelectorValue = "";
+ } else {
+ if (mDisplayedValues != null) {
+ int displayedValueIndex = selectorIndex - mMinValue;
+ scrollSelectorValue = mDisplayedValues[displayedValueIndex];
+ } else {
+ scrollSelectorValue = formatNumber(selectorIndex);
+ }
+ }
+ cache.put(selectorIndex, scrollSelectorValue);
+ }
+
+ private String formatNumber(int value) {
+ return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
+ }
+
+ private void validateInputTextView(View v) {
+ String str = String.valueOf(((TextView) v).getText());
+ if (TextUtils.isEmpty(str)) {
+ // Restore to the old value as we don't allow empty values
+ updateInputTextView();
+ } else {
+ // Check the new value and ensure it's in range
+ int current = getSelectedPos(str.toString());
+ setValueInternal(current, true);
+ }
+ }
+
+ /**
+ * Updates the view of this NumberPicker. If displayValues were specified in
+ * the string corresponding to the index specified by the current value will
+ * be returned. Otherwise, the formatter specified in {@link #setFormatter}
+ * will be used to format the number.
+ *
+ * @return Whether the text was updated.
+ */
+ private boolean updateInputTextView() {
+ /*
+ * If we don't have displayed values then use the current number else
+ * find the correct value in the displayed values for the current
+ * number.
+ */
+ String text = (mDisplayedValues == null) ? formatNumber(mValue)
+ : mDisplayedValues[mValue - mMinValue];
+ if (!TextUtils.isEmpty(text)) {
+ CharSequence beforeText = mInputText.getText();
+ if (!text.equals(beforeText.toString())) {
+ mInputText.setText(text);
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+ mInputText.onInitializeAccessibilityEvent(event);
+ mInputText.onPopulateAccessibilityEvent(event);
+ event.setFromIndex(0);
+ event.setRemovedCount(beforeText.length());
+ event.setAddedCount(text.length());
+ event.setBeforeText(beforeText);
+ event.setSource(NumberPicker.this,
+ AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT);
+ requestSendAccessibilityEvent(NumberPicker.this, event);
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Notifies the listener, if registered, of a change of the value of this
+ * NumberPicker.
+ */
+ private void notifyChange(int previous, int current) {
+ if (mOnValueChangeListener != null) {
+ mOnValueChangeListener.onValueChange(this, previous, mValue);
+ }
+ }
+
+ /**
+ * Posts a command for changing the current value by one.
+ *
+ * @param increment Whether to increment or decrement the value.
+ */
+ private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
+ if (mChangeCurrentByOneFromLongPressCommand == null) {
+ mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
+ } else {
+ removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+ }
+ mChangeCurrentByOneFromLongPressCommand.setStep(increment);
+ postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
+ }
+
+ /**
+ * Removes the command for changing the current value by one.
+ */
+ private void removeChangeCurrentByOneFromLongPress() {
+ if (mChangeCurrentByOneFromLongPressCommand != null) {
+ removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+ }
+ }
+
+ /**
+ * Posts a command for beginning an edit of the current value via IME on
+ * long press.
+ */
+ private void postBeginSoftInputOnLongPressCommand() {
+ if (mBeginSoftInputOnLongPressCommand == null) {
+ mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
+ } else {
+ removeCallbacks(mBeginSoftInputOnLongPressCommand);
+ }
+ postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
+ }
+
+ /**
+ * Removes the command for beginning an edit of the current value via IME.
+ */
+ private void removeBeginSoftInputCommand() {
+ if (mBeginSoftInputOnLongPressCommand != null) {
+ removeCallbacks(mBeginSoftInputOnLongPressCommand);
+ }
+ }
+
+ /**
+ * Removes all pending callback from the message queue.
+ */
+ private void removeAllCallbacks() {
+ if (mChangeCurrentByOneFromLongPressCommand != null) {
+ removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+ }
+ if (mSetSelectionCommand != null) {
+ mSetSelectionCommand.cancel();
+ }
+ if (mBeginSoftInputOnLongPressCommand != null) {
+ removeCallbacks(mBeginSoftInputOnLongPressCommand);
+ }
+ mPressedStateHelper.cancel();
+ }
+
+ /**
+ * @return The selected index given its displayed <code>value</code>.
+ */
+ private int getSelectedPos(String value) {
+ if (mDisplayedValues == null) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ // Ignore as if it's not a number we don't care
+ }
+ } else {
+ for (int i = 0; i < mDisplayedValues.length; i++) {
+ // Don't force the user to type in jan when ja will do
+ value = value.toLowerCase();
+ if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
+ return mMinValue + i;
+ }
+ }
+
+ /*
+ * The user might have typed in a number into the month field i.e.
+ * 10 instead of OCT so support that too.
+ */
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+
+ // Ignore as if it's not a number we don't care
+ }
+ }
+ return mMinValue;
+ }
+
+ /**
+ * Posts a {@link SetSelectionCommand} from the given
+ * {@code selectionStart} to {@code selectionEnd}.
+ */
+ private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
+ if (mSetSelectionCommand == null) {
+ mSetSelectionCommand = new SetSelectionCommand(mInputText);
+ }
+ mSetSelectionCommand.post(selectionStart, selectionEnd);
+ }
+
+ /**
+ * The numbers accepted by the input text's {@link Filter}
+ */
+ private static final char[] DIGIT_CHARACTERS = new char[] {
+ // Latin digits are the common case
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ // Arabic-Indic
+ '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
+ , '\u0669',
+ // Extended Arabic-Indic
+ '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
+ , '\u06f9',
+ // Hindi and Marathi (Devanagari script)
+ '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
+ , '\u096f',
+ // Bengali
+ '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
+ , '\u09ef',
+ // Kannada
+ '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
+ , '\u0cef'
+ };
+
+ /**
+ * Filter for accepting only valid indices or prefixes of the string
+ * representation of valid indices.
+ */
+ class InputTextFilter extends NumberKeyListener {
+
+ // XXX This doesn't allow for range limits when controlled by a
+ // soft input method!
+ public int getInputType() {
+ return InputType.TYPE_CLASS_TEXT;
+ }
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return DIGIT_CHARACTERS;
+ }
+
+ @Override
+ public CharSequence filter(
+ CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
+ // We don't know what the output will be, so always cancel any
+ // pending set selection command.
+ if (mSetSelectionCommand != null) {
+ mSetSelectionCommand.cancel();
+ }
+
+ if (mDisplayedValues == null) {
+ CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
+ if (filtered == null) {
+ filtered = source.subSequence(start, end);
+ }
+
+ String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+ + dest.subSequence(dend, dest.length());
+
+ if ("".equals(result)) {
+ return result;
+ }
+ int val = getSelectedPos(result);
+
+ /*
+ * Ensure the user can't type in a value greater than the max
+ * allowed. We have to allow less than min as the user might
+ * want to delete some numbers and then type a new number.
+ * And prevent multiple-"0" that exceeds the length of upper
+ * bound number.
+ */
+ if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
+ return "";
+ } else {
+ return filtered;
+ }
+ } else {
+ CharSequence filtered = String.valueOf(source.subSequence(start, end));
+ if (TextUtils.isEmpty(filtered)) {
+ return "";
+ }
+ String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+ + dest.subSequence(dend, dest.length());
+ String str = String.valueOf(result).toLowerCase();
+ for (String val : mDisplayedValues) {
+ String valLowerCase = val.toLowerCase();
+ if (valLowerCase.startsWith(str)) {
+ postSetSelectionCommand(result.length(), val.length());
+ return val.subSequence(dstart, val.length());
+ }
+ }
+ return "";
+ }
+ }
+ }
+
+ /**
+ * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
+ * middle element is in the middle of the widget.
+ *
+ * @return Whether an adjustment has been made.
+ */
+ private boolean ensureScrollWheelAdjusted() {
+ // adjust to the closest value
+ int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
+ if (deltaY != 0) {
+ mPreviousScrollerY = 0;
+ if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
+ deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
+ }
+ mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
+ invalidate();
+ return true;
+ }
+ return false;
+ }
+
+ class PressedStateHelper implements Runnable {
+ public static final int BUTTON_INCREMENT = 1;
+ public static final int BUTTON_DECREMENT = 2;
+
+ private final int MODE_PRESS = 1;
+ private final int MODE_TAPPED = 2;
+
+ private int mManagedButton;
+ private int mMode;
+
+ public void cancel() {
+ mMode = 0;
+ mManagedButton = 0;
+ NumberPicker.this.removeCallbacks(this);
+ if (mIncrementVirtualButtonPressed) {
+ mIncrementVirtualButtonPressed = false;
+ invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+ }
+ mDecrementVirtualButtonPressed = false;
+ if (mDecrementVirtualButtonPressed) {
+ invalidate(0, 0, mRight, mTopSelectionDividerTop);
+ }
+ }
+
+ public void buttonPressDelayed(int button) {
+ cancel();
+ mMode = MODE_PRESS;
+ mManagedButton = button;
+ NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
+ }
+
+ public void buttonTapped(int button) {
+ cancel();
+ mMode = MODE_TAPPED;
+ mManagedButton = button;
+ NumberPicker.this.post(this);
+ }
+
+ @Override
+ public void run() {
+ switch (mMode) {
+ case MODE_PRESS: {
+ switch (mManagedButton) {
+ case BUTTON_INCREMENT: {
+ mIncrementVirtualButtonPressed = true;
+ invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+ } break;
+ case BUTTON_DECREMENT: {
+ mDecrementVirtualButtonPressed = true;
+ invalidate(0, 0, mRight, mTopSelectionDividerTop);
+ }
+ }
+ } break;
+ case MODE_TAPPED: {
+ switch (mManagedButton) {
+ case BUTTON_INCREMENT: {
+ if (!mIncrementVirtualButtonPressed) {
+ NumberPicker.this.postDelayed(this,
+ ViewConfiguration.getPressedStateDuration());
+ }
+ mIncrementVirtualButtonPressed ^= true;
+ invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+ } break;
+ case BUTTON_DECREMENT: {
+ if (!mDecrementVirtualButtonPressed) {
+ NumberPicker.this.postDelayed(this,
+ ViewConfiguration.getPressedStateDuration());
+ }
+ mDecrementVirtualButtonPressed ^= true;
+ invalidate(0, 0, mRight, mTopSelectionDividerTop);
+ }
+ }
+ } break;
+ }
+ }
+ }
+
+ /**
+ * Command for setting the input text selection.
+ */
+ private static class SetSelectionCommand implements Runnable {
+ private final EditText mInputText;
+
+ private int mSelectionStart;
+ private int mSelectionEnd;
+
+ /** Whether this runnable is currently posted. */
+ private boolean mPosted;
+
+ public SetSelectionCommand(EditText inputText) {
+ mInputText = inputText;
+ }
+
+ public void post(int selectionStart, int selectionEnd) {
+ mSelectionStart = selectionStart;
+ mSelectionEnd = selectionEnd;
+
+ if (!mPosted) {
+ mInputText.post(this);
+ mPosted = true;
+ }
+ }
+
+ public void cancel() {
+ if (mPosted) {
+ mInputText.removeCallbacks(this);
+ mPosted = false;
+ }
+ }
+
+ @Override
+ public void run() {
+ mPosted = false;
+ mInputText.setSelection(mSelectionStart, mSelectionEnd);
+ }
+ }
+
+ /**
+ * Command for changing the current value from a long press by one.
+ */
+ class ChangeCurrentByOneFromLongPressCommand implements Runnable {
+ private boolean mIncrement;
+
+ private void setStep(boolean increment) {
+ mIncrement = increment;
+ }
+
+ @Override
+ public void run() {
+ changeValueByOne(mIncrement);
+ postDelayed(this, mLongPressUpdateInterval);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class CustomEditText extends EditText {
+
+ public CustomEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void onEditorAction(int actionCode) {
+ super.onEditorAction(actionCode);
+ if (actionCode == EditorInfo.IME_ACTION_DONE) {
+ clearFocus();
+ }
+ }
+ }
+
+ /**
+ * Command for beginning soft input on long press.
+ */
+ class BeginSoftInputOnLongPressCommand implements Runnable {
+
+ @Override
+ public void run() {
+ performLongClick();
+ }
+ }
+
+ /**
+ * Class for managing virtual view tree rooted at this picker.
+ */
+ class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
+ private static final int UNDEFINED = Integer.MIN_VALUE;
+
+ private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
+
+ private static final int VIRTUAL_VIEW_ID_INPUT = 2;
+
+ private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
+
+ private final Rect mTempRect = new Rect();
+
+ private final int[] mTempArray = new int[2];
+
+ private int mAccessibilityFocusedView = UNDEFINED;
+
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ switch (virtualViewId) {
+ case View.NO_ID:
+ return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
+ mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
+ case VIRTUAL_VIEW_ID_DECREMENT:
+ return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
+ getVirtualDecrementButtonText(), mScrollX, mScrollY,
+ mScrollX + (mRight - mLeft),
+ mTopSelectionDividerTop + mSelectionDividerHeight);
+ case VIRTUAL_VIEW_ID_INPUT:
+ return createAccessibiltyNodeInfoForInputText(mScrollX,
+ mTopSelectionDividerTop + mSelectionDividerHeight,
+ mScrollX + (mRight - mLeft),
+ mBottomSelectionDividerBottom - mSelectionDividerHeight);
+ case VIRTUAL_VIEW_ID_INCREMENT:
+ return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
+ getVirtualIncrementButtonText(), mScrollX,
+ mBottomSelectionDividerBottom - mSelectionDividerHeight,
+ mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
+ }
+ return super.createAccessibilityNodeInfo(virtualViewId);
+ }
+
+ @Override
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
+ int virtualViewId) {
+ if (TextUtils.isEmpty(searched)) {
+ return Collections.emptyList();
+ }
+ String searchedLowerCase = searched.toLowerCase();
+ List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
+ switch (virtualViewId) {
+ case View.NO_ID: {
+ findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+ VIRTUAL_VIEW_ID_DECREMENT, result);
+ findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+ VIRTUAL_VIEW_ID_INPUT, result);
+ findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+ VIRTUAL_VIEW_ID_INCREMENT, result);
+ return result;
+ }
+ case VIRTUAL_VIEW_ID_DECREMENT:
+ case VIRTUAL_VIEW_ID_INCREMENT:
+ case VIRTUAL_VIEW_ID_INPUT: {
+ findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
+ result);
+ return result;
+ }
+ }
+ return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ switch (virtualViewId) {
+ case View.NO_ID: {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView != virtualViewId) {
+ mAccessibilityFocusedView = virtualViewId;
+ requestAccessibilityFocus();
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView == virtualViewId) {
+ mAccessibilityFocusedView = UNDEFINED;
+ clearAccessibilityFocus();
+ return true;
+ }
+ return false;
+ }
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ if (NumberPicker.this.isEnabled()
+ && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
+ changeValueByOne(true);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ if (NumberPicker.this.isEnabled()
+ && (getWrapSelectorWheel() || getValue() > getMinValue())) {
+ changeValueByOne(false);
+ return true;
+ }
+ } return false;
+ }
+ } break;
+ case VIRTUAL_VIEW_ID_INPUT: {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_FOCUS: {
+ if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) {
+ return mInputText.requestFocus();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
+ if (NumberPicker.this.isEnabled() && mInputText.isFocused()) {
+ mInputText.clearFocus();
+ return true;
+ }
+ return false;
+ }
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ if (NumberPicker.this.isEnabled()) {
+ performClick();
+ return true;
+ }
+ return false;
+ }
+ case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
+ if (NumberPicker.this.isEnabled()) {
+ performLongClick();
+ return true;
+ }
+ return false;
+ }
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView != virtualViewId) {
+ mAccessibilityFocusedView = virtualViewId;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ mInputText.invalidate();
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView == virtualViewId) {
+ mAccessibilityFocusedView = UNDEFINED;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ mInputText.invalidate();
+ return true;
+ }
+ } return false;
+ default: {
+ return mInputText.performAccessibilityAction(action, arguments);
+ }
+ }
+ } return false;
+ case VIRTUAL_VIEW_ID_INCREMENT: {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ if (NumberPicker.this.isEnabled()) {
+ NumberPicker.this.changeValueByOne(true);
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_CLICKED);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView != virtualViewId) {
+ mAccessibilityFocusedView = virtualViewId;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView == virtualViewId) {
+ mAccessibilityFocusedView = UNDEFINED;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+ return true;
+ }
+ } return false;
+ }
+ } return false;
+ case VIRTUAL_VIEW_ID_DECREMENT: {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ if (NumberPicker.this.isEnabled()) {
+ final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
+ NumberPicker.this.changeValueByOne(increment);
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_CLICKED);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView != virtualViewId) {
+ mAccessibilityFocusedView = virtualViewId;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ invalidate(0, 0, mRight, mTopSelectionDividerTop);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+ if (mAccessibilityFocusedView == virtualViewId) {
+ mAccessibilityFocusedView = UNDEFINED;
+ sendAccessibilityEventForVirtualView(virtualViewId,
+ AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ invalidate(0, 0, mRight, mTopSelectionDividerTop);
+ return true;
+ }
+ } return false;
+ }
+ } return false;
+ }
+ return super.performAction(virtualViewId, action, arguments);
+ }
+
+ public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
+ switch (virtualViewId) {
+ case VIRTUAL_VIEW_ID_DECREMENT: {
+ if (hasVirtualDecrementButton()) {
+ sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
+ getVirtualDecrementButtonText());
+ }
+ } break;
+ case VIRTUAL_VIEW_ID_INPUT: {
+ sendAccessibilityEventForVirtualText(eventType);
+ } break;
+ case VIRTUAL_VIEW_ID_INCREMENT: {
+ if (hasVirtualIncrementButton()) {
+ sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
+ getVirtualIncrementButtonText());
+ }
+ } break;
+ }
+ }
+
+ private void sendAccessibilityEventForVirtualText(int eventType) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ mInputText.onInitializeAccessibilityEvent(event);
+ mInputText.onPopulateAccessibilityEvent(event);
+ event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+ requestSendAccessibilityEvent(NumberPicker.this, event);
+ }
+ }
+
+ private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
+ String text) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setClassName(Button.class.getName());
+ event.setPackageName(mContext.getPackageName());
+ event.getText().add(text);
+ event.setEnabled(NumberPicker.this.isEnabled());
+ event.setSource(NumberPicker.this, virtualViewId);
+ requestSendAccessibilityEvent(NumberPicker.this, event);
+ }
+ }
+
+ private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
+ int virtualViewId, List<AccessibilityNodeInfo> outResult) {
+ switch (virtualViewId) {
+ case VIRTUAL_VIEW_ID_DECREMENT: {
+ String text = getVirtualDecrementButtonText();
+ if (!TextUtils.isEmpty(text)
+ && text.toString().toLowerCase().contains(searchedLowerCase)) {
+ outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
+ }
+ } return;
+ case VIRTUAL_VIEW_ID_INPUT: {
+ CharSequence text = mInputText.getText();
+ if (!TextUtils.isEmpty(text) &&
+ text.toString().toLowerCase().contains(searchedLowerCase)) {
+ outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
+ return;
+ }
+ CharSequence contentDesc = mInputText.getText();
+ if (!TextUtils.isEmpty(contentDesc) &&
+ contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
+ outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
+ return;
+ }
+ } break;
+ case VIRTUAL_VIEW_ID_INCREMENT: {
+ String text = getVirtualIncrementButtonText();
+ if (!TextUtils.isEmpty(text)
+ && text.toString().toLowerCase().contains(searchedLowerCase)) {
+ outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
+ }
+ } return;
+ }
+ }
+
+ private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
+ int left, int top, int right, int bottom) {
+ AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
+ info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+ if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+ if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+ Rect boundsInParent = mTempRect;
+ boundsInParent.set(left, top, right, bottom);
+ info.setVisibleToUser(isVisibleToUser(boundsInParent));
+ info.setBoundsInParent(boundsInParent);
+ Rect boundsInScreen = boundsInParent;
+ int[] locationOnScreen = mTempArray;
+ getLocationOnScreen(locationOnScreen);
+ boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+ info.setBoundsInScreen(boundsInScreen);
+ return info;
+ }
+
+ private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
+ String text, int left, int top, int right, int bottom) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setClassName(Button.class.getName());
+ info.setPackageName(mContext.getPackageName());
+ info.setSource(NumberPicker.this, virtualViewId);
+ info.setParent(NumberPicker.this);
+ info.setText(text);
+ info.setClickable(true);
+ info.setLongClickable(true);
+ info.setEnabled(NumberPicker.this.isEnabled());
+ Rect boundsInParent = mTempRect;
+ boundsInParent.set(left, top, right, bottom);
+ info.setVisibleToUser(isVisibleToUser(boundsInParent));
+ info.setBoundsInParent(boundsInParent);
+ Rect boundsInScreen = boundsInParent;
+ int[] locationOnScreen = mTempArray;
+ getLocationOnScreen(locationOnScreen);
+ boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+ info.setBoundsInScreen(boundsInScreen);
+
+ if (mAccessibilityFocusedView != virtualViewId) {
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+ if (mAccessibilityFocusedView == virtualViewId) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+ if (NumberPicker.this.isEnabled()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ }
+
+ return info;
+ }
+
+ private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
+ int right, int bottom) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setClassName(NumberPicker.class.getName());
+ info.setPackageName(mContext.getPackageName());
+ info.setSource(NumberPicker.this);
+
+ if (hasVirtualDecrementButton()) {
+ info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
+ }
+ info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+ if (hasVirtualIncrementButton()) {
+ info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
+ }
+
+ info.setParent((View) getParentForAccessibility());
+ info.setEnabled(NumberPicker.this.isEnabled());
+ info.setScrollable(true);
+
+ final float applicationScale =
+ getContext().getResources().getCompatibilityInfo().applicationScale;
+
+ Rect boundsInParent = mTempRect;
+ boundsInParent.set(left, top, right, bottom);
+ boundsInParent.scale(applicationScale);
+ info.setBoundsInParent(boundsInParent);
+
+ info.setVisibleToUser(isVisibleToUser());
+
+ Rect boundsInScreen = boundsInParent;
+ int[] locationOnScreen = mTempArray;
+ getLocationOnScreen(locationOnScreen);
+ boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+ boundsInScreen.scale(applicationScale);
+ info.setBoundsInScreen(boundsInScreen);
+
+ if (mAccessibilityFocusedView != View.NO_ID) {
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ }
+ if (mAccessibilityFocusedView == View.NO_ID) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ }
+ if (NumberPicker.this.isEnabled()) {
+ if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ if (getWrapSelectorWheel() || getValue() > getMinValue()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ }
+
+ return info;
+ }
+
+ private boolean hasVirtualDecrementButton() {
+ return getWrapSelectorWheel() || getValue() > getMinValue();
+ }
+
+ private boolean hasVirtualIncrementButton() {
+ return getWrapSelectorWheel() || getValue() < getMaxValue();
+ }
+
+ private String getVirtualDecrementButtonText() {
+ int value = mValue - 1;
+ if (mWrapSelectorWheel) {
+ value = getWrappedSelectorIndex(value);
+ }
+ if (value >= mMinValue) {
+ return (mDisplayedValues == null) ? formatNumber(value)
+ : mDisplayedValues[value - mMinValue];
+ }
+ return null;
+ }
+
+ private String getVirtualIncrementButtonText() {
+ int value = mValue + 1;
+ if (mWrapSelectorWheel) {
+ value = getWrappedSelectorIndex(value);
+ }
+ if (value <= mMaxValue) {
+ return (mDisplayedValues == null) ? formatNumber(value)
+ : mDisplayedValues[value - mMinValue];
+ }
+ return null;
+ }
+ }
+
+ static private String formatNumberWithLocale(int value) {
+ return String.format(Locale.getDefault(), "%d", value);
+ }
+}
diff --git a/android/widget/OnDateChangedListener.java b/android/widget/OnDateChangedListener.java
new file mode 100644
index 0000000..29be888
--- /dev/null
+++ b/android/widget/OnDateChangedListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * The callback used to notify other date picker components of a change in the selected date.
+ *
+ */
+interface OnDateChangedListener {
+
+ public void onDateChanged();
+}
+
diff --git a/android/widget/OverScroller.java b/android/widget/OverScroller.java
new file mode 100644
index 0000000..9938789
--- /dev/null
+++ b/android/widget/OverScroller.java
@@ -0,0 +1,961 @@
+/*
+ * Copyright (C) 2010 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.hardware.SensorManager;
+import android.util.Log;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds
+ * of a scrolling operation. This class is a drop-in replacement for
+ * {@link android.widget.Scroller} in most cases.
+ */
+public class OverScroller {
+ private int mMode;
+
+ private final SplineOverScroller mScrollerX;
+ private final SplineOverScroller mScrollerY;
+
+ private Interpolator mInterpolator;
+
+ private final boolean mFlywheel;
+
+ private static final int DEFAULT_DURATION = 250;
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ /**
+ * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
+ * @param context
+ */
+ public OverScroller(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Creates an OverScroller with flywheel enabled.
+ * @param context The context of this application.
+ * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+ * be used.
+ */
+ public OverScroller(Context context, Interpolator interpolator) {
+ this(context, interpolator, true);
+ }
+
+ /**
+ * Creates an OverScroller.
+ * @param context The context of this application.
+ * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+ * be used.
+ * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+ * @hide
+ */
+ public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
+ if (interpolator == null) {
+ mInterpolator = new Scroller.ViscousFluidInterpolator();
+ } else {
+ mInterpolator = interpolator;
+ }
+ mFlywheel = flywheel;
+ mScrollerX = new SplineOverScroller(context);
+ mScrollerY = new SplineOverScroller(context);
+ }
+
+ /**
+ * Creates an OverScroller with flywheel enabled.
+ * @param context The context of this application.
+ * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+ * be used.
+ * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+ * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+ * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+ * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+ * behavior is no longer supported and this coefficient has no effect.
+ * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
+ */
+ @Deprecated
+ public OverScroller(Context context, Interpolator interpolator,
+ float bounceCoefficientX, float bounceCoefficientY) {
+ this(context, interpolator, true);
+ }
+
+ /**
+ * Creates an OverScroller.
+ * @param context The context of this application.
+ * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+ * be used.
+ * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+ * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+ * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+ * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+ * behavior is no longer supported and this coefficient has no effect.
+ * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+ * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
+ */
+ @Deprecated
+ public OverScroller(Context context, Interpolator interpolator,
+ float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
+ this(context, interpolator, flywheel);
+ }
+
+ void setInterpolator(Interpolator interpolator) {
+ if (interpolator == null) {
+ mInterpolator = new Scroller.ViscousFluidInterpolator();
+ } else {
+ mInterpolator = interpolator;
+ }
+ }
+
+ /**
+ * The amount of friction applied to flings. The default value
+ * is {@link ViewConfiguration#getScrollFriction}.
+ *
+ * @param friction A scalar dimension-less value representing the coefficient of
+ * friction.
+ */
+ public final void setFriction(float friction) {
+ mScrollerX.setFriction(friction);
+ mScrollerY.setFriction(friction);
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mScrollerX.mFinished && mScrollerY.mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value. Contrary to
+ * {@link #abortAnimation()}, forcing the animation to finished
+ * does NOT cause the scroller to move to the final x and y
+ * position.
+ *
+ * @param finished The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mScrollerX.mFinished = mScrollerY.mFinished = finished;
+ }
+
+ /**
+ * 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 mScrollerX.mCurrentPosition;
+ }
+
+ /**
+ * 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 mScrollerY.mCurrentPosition;
+ }
+
+ /**
+ * Returns the absolute value of the current velocity.
+ *
+ * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
+ */
+ public float getCurrVelocity() {
+ return (float) Math.hypot(mScrollerX.mCurrVelocity, mScrollerY.mCurrVelocity);
+ }
+
+ /**
+ * Returns the start X offset in the scroll.
+ *
+ * @return The start X offset as an absolute distance from the origin.
+ */
+ public final int getStartX() {
+ return mScrollerX.mStart;
+ }
+
+ /**
+ * Returns the start Y offset in the scroll.
+ *
+ * @return The start Y offset as an absolute distance from the origin.
+ */
+ public final int getStartY() {
+ return mScrollerY.mStart;
+ }
+
+ /**
+ * 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 mScrollerX.mFinal;
+ }
+
+ /**
+ * 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 mScrollerY.mFinal;
+ }
+
+ /**
+ * Returns how long the scroll event will take, in milliseconds.
+ *
+ * @return The duration of the scroll in milliseconds.
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScrollers don't necessarily have a fixed duration.
+ * This function will lie to the best of its ability.
+ */
+ @Deprecated
+ public final int getDuration() {
+ return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
+ }
+
+ /**
+ * Extend the scroll animation. This allows a running animation to scroll
+ * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+ *
+ * @param extend Additional time to scroll in milliseconds.
+ * @see #setFinalX(int)
+ * @see #setFinalY(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScrollers don't necessarily have a fixed duration.
+ * Instead of setting a new final position and extending
+ * the duration of an existing scroll, use startScroll
+ * to begin a new animation.
+ */
+ @Deprecated
+ public void extendDuration(int extend) {
+ mScrollerX.extendDuration(extend);
+ mScrollerY.extendDuration(extend);
+ }
+
+ /**
+ * Sets the final position (X) for this scroller.
+ *
+ * @param newX The new X offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalY(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScroller's final position may change during an animation.
+ * Instead of setting a new final position and extending
+ * the duration of an existing scroll, use startScroll
+ * to begin a new animation.
+ */
+ @Deprecated
+ public void setFinalX(int newX) {
+ mScrollerX.setFinalPosition(newX);
+ }
+
+ /**
+ * Sets the final position (Y) for this scroller.
+ *
+ * @param newY The new Y offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalX(int)
+ *
+ * @hide Pending removal once nothing depends on it
+ * @deprecated OverScroller's final position may change during an animation.
+ * Instead of setting a new final position and extending
+ * the duration of an existing scroll, use startScroll
+ * to begin a new animation.
+ */
+ @Deprecated
+ public void setFinalY(int newY) {
+ mScrollerY.setFinalPosition(newY);
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true, the
+ * animation is not yet finished.
+ */
+ public boolean computeScrollOffset() {
+ if (isFinished()) {
+ return false;
+ }
+
+ switch (mMode) {
+ case SCROLL_MODE:
+ long time = AnimationUtils.currentAnimationTimeMillis();
+ // Any scroller can be used for time, since they were started
+ // together in scroll mode. We use X here.
+ final long elapsedTime = time - mScrollerX.mStartTime;
+
+ final int duration = mScrollerX.mDuration;
+ if (elapsedTime < duration) {
+ final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
+ mScrollerX.updateScroll(q);
+ mScrollerY.updateScroll(q);
+ } else {
+ abortAnimation();
+ }
+ break;
+
+ case FLING_MODE:
+ if (!mScrollerX.mFinished) {
+ if (!mScrollerX.update()) {
+ if (!mScrollerX.continueWhenFinished()) {
+ mScrollerX.finish();
+ }
+ }
+ }
+
+ if (!mScrollerY.mFinished) {
+ if (!mScrollerY.update()) {
+ if (!mScrollerY.continueWhenFinished()) {
+ mScrollerY.finish();
+ }
+ }
+ }
+
+ break;
+ }
+
+ 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;
+ mScrollerX.startScroll(startX, dx, duration);
+ mScrollerY.startScroll(startY, dy, duration);
+ }
+
+ /**
+ * Call this when you want to 'spring back' into a valid coordinate range.
+ *
+ * @param startX Starting X coordinate
+ * @param startY Starting Y coordinate
+ * @param minX Minimum valid X value
+ * @param maxX Maximum valid X value
+ * @param minY Minimum valid Y value
+ * @param maxY Minimum valid Y value
+ * @return true if a springback was initiated, false if startX and startY were
+ * already within the valid range.
+ */
+ public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
+ mMode = FLING_MODE;
+
+ // Make sure both methods are called.
+ final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
+ final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
+ return spingbackX || spingbackY;
+ }
+
+ public void fling(int startX, int startY, int velocityX, int velocityY,
+ int minX, int maxX, int minY, int maxY) {
+ fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance traveled 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
+ * unless overX > 0. If overfling is allowed, it will use minX as
+ * a springback boundary.
+ * @param maxX Maximum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use maxX as
+ * a springback boundary.
+ * @param minY Minimum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use minY as
+ * a springback boundary.
+ * @param maxY Maximum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use maxY as
+ * a springback boundary.
+ * @param overX Overfling range. If > 0, horizontal overfling in either
+ * direction will be possible.
+ * @param overY Overfling range. If > 0, vertical overfling in either
+ * direction will be possible.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY,
+ int minX, int maxX, int minY, int maxY, int overX, int overY) {
+ // Continue a scroll or fling in progress
+ if (mFlywheel && !isFinished()) {
+ float oldVelocityX = mScrollerX.mCurrVelocity;
+ float oldVelocityY = mScrollerY.mCurrVelocity;
+ if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+ Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+ velocityX += oldVelocityX;
+ velocityY += oldVelocityY;
+ }
+ }
+
+ mMode = FLING_MODE;
+ mScrollerX.fling(startX, velocityX, minX, maxX, overX);
+ mScrollerY.fling(startY, velocityY, minY, maxY, overY);
+ }
+
+ /**
+ * Notify the scroller that we've reached a horizontal boundary.
+ * Normally the information to handle this will already be known
+ * when the animation is started, such as in a call to one of the
+ * fling functions. However there are cases where this cannot be known
+ * in advance. This function will transition the current motion and
+ * animate from startX to finalX as appropriate.
+ *
+ * @param startX Starting/current X position
+ * @param finalX Desired final X position
+ * @param overX Magnitude of overscroll allowed. This should be the maximum
+ * desired distance from finalX. Absolute value - must be positive.
+ */
+ public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
+ mScrollerX.notifyEdgeReached(startX, finalX, overX);
+ }
+
+ /**
+ * Notify the scroller that we've reached a vertical boundary.
+ * Normally the information to handle this will already be known
+ * when the animation is started, such as in a call to one of the
+ * fling functions. However there are cases where this cannot be known
+ * in advance. This function will animate a parabolic motion from
+ * startY to finalY.
+ *
+ * @param startY Starting/current Y position
+ * @param finalY Desired final Y position
+ * @param overY Magnitude of overscroll allowed. This should be the maximum
+ * desired distance from finalY. Absolute value - must be positive.
+ */
+ public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
+ mScrollerY.notifyEdgeReached(startY, finalY, overY);
+ }
+
+ /**
+ * Returns whether the current Scroller is currently returning to a valid position.
+ * Valid bounds were provided by the
+ * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
+ *
+ * One should check this value before calling
+ * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
+ * to restore a valid position will then be stopped. The caller has to take into account
+ * the fact that the started scroll will start from an overscrolled position.
+ *
+ * @return true when the current position is overscrolled and in the process of
+ * interpolating back to a valid value.
+ */
+ public boolean isOverScrolled() {
+ return ((!mScrollerX.mFinished &&
+ mScrollerX.mState != SplineOverScroller.SPLINE) ||
+ (!mScrollerY.mFinished &&
+ mScrollerY.mState != SplineOverScroller.SPLINE));
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating causes the scroller to move to the final x and y
+ * positions.
+ *
+ * @see #forceFinished(boolean)
+ */
+ public void abortAnimation() {
+ mScrollerX.finish();
+ mScrollerY.finish();
+ }
+
+ /**
+ * Returns the time elapsed since the beginning of the scrolling.
+ *
+ * @return The elapsed time in milliseconds.
+ *
+ * @hide
+ */
+ public int timePassed() {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
+ return (int) (time - startTime);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isScrollingInDirection(float xvel, float yvel) {
+ final int dx = mScrollerX.mFinal - mScrollerX.mStart;
+ final int dy = mScrollerY.mFinal - mScrollerY.mStart;
+ return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
+ Math.signum(yvel) == Math.signum(dy);
+ }
+
+ static class SplineOverScroller {
+ // Initial position
+ private int mStart;
+
+ // Current position
+ private int mCurrentPosition;
+
+ // Final position
+ private int mFinal;
+
+ // Initial velocity
+ private int mVelocity;
+
+ // Current velocity
+ private float mCurrVelocity;
+
+ // Constant current deceleration
+ private float mDeceleration;
+
+ // Animation starting time, in system milliseconds
+ private long mStartTime;
+
+ // Animation duration, in milliseconds
+ private int mDuration;
+
+ // Duration to complete spline component of animation
+ private int mSplineDuration;
+
+ // Distance to travel along spline animation
+ private int mSplineDistance;
+
+ // Whether the animation is currently in progress
+ private boolean mFinished;
+
+ // The allowed overshot distance before boundary is reached.
+ private int mOver;
+
+ // Fling friction
+ private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+ // Current state of the animation.
+ private int mState = SPLINE;
+
+ // Constant gravity value, used in the deceleration phase.
+ private static final float GRAVITY = 2000.0f;
+
+ // A context-specific coefficient adjusted to physical values.
+ private float mPhysicalCoeff;
+
+ private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+ private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+ private static final float START_TENSION = 0.5f;
+ private static final float END_TENSION = 1.0f;
+ private static final float P1 = START_TENSION * INFLEXION;
+ private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+ private static final int NB_SAMPLES = 100;
+ private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+ private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+ private static final int SPLINE = 0;
+ private static final int CUBIC = 1;
+ private static final int BALLISTIC = 2;
+
+ static {
+ float x_min = 0.0f;
+ float y_min = 0.0f;
+ for (int i = 0; i < NB_SAMPLES; i++) {
+ final float alpha = (float) i / NB_SAMPLES;
+
+ float x_max = 1.0f;
+ float x, tx, coef;
+ while (true) {
+ x = x_min + (x_max - x_min) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+ if (Math.abs(tx - alpha) < 1E-5) break;
+ if (tx > alpha) x_max = x;
+ else x_min = x;
+ }
+ SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+ float y_max = 1.0f;
+ float y, dy;
+ while (true) {
+ y = y_min + (y_max - y_min) / 2.0f;
+ coef = 3.0f * y * (1.0f - y);
+ dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+ if (Math.abs(dy - alpha) < 1E-5) break;
+ if (dy > alpha) y_max = y;
+ else y_min = y;
+ }
+ SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+ }
+ SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+ }
+
+ void setFriction(float friction) {
+ mFlingFriction = friction;
+ }
+
+ SplineOverScroller(Context context) {
+ mFinished = true;
+ final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi
+ * 0.84f; // look and feel tuning
+ }
+
+ void updateScroll(float q) {
+ mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+ }
+
+ /*
+ * Get a signed deceleration that will reduce the velocity.
+ */
+ static private float getDeceleration(int velocity) {
+ return velocity > 0 ? -GRAVITY : GRAVITY;
+ }
+
+ /*
+ * Modifies mDuration to the duration it takes to get from start to newFinal using the
+ * spline interpolation. The previous duration was needed to get to oldFinal.
+ */
+ private void adjustDuration(int start, int oldFinal, int newFinal) {
+ final int oldDistance = oldFinal - start;
+ final int newDistance = newFinal - start;
+ final float x = Math.abs((float) newDistance / oldDistance);
+ final int index = (int) (NB_SAMPLES * x);
+ if (index < NB_SAMPLES) {
+ final float x_inf = (float) index / NB_SAMPLES;
+ final float x_sup = (float) (index + 1) / NB_SAMPLES;
+ final float t_inf = SPLINE_TIME[index];
+ final float t_sup = SPLINE_TIME[index + 1];
+ final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
+ mDuration *= timeCoef;
+ }
+ }
+
+ void startScroll(int start, int distance, int duration) {
+ mFinished = false;
+
+ mCurrentPosition = mStart = start;
+ mFinal = start + distance;
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = duration;
+
+ // Unused
+ mDeceleration = 0.0f;
+ mVelocity = 0;
+ }
+
+ void finish() {
+ mCurrentPosition = mFinal;
+ // Not reset since WebView relies on this value for fast fling.
+ // TODO: restore when WebView uses the fast fling implemented in this class.
+ // mCurrVelocity = 0.0f;
+ mFinished = true;
+ }
+
+ void setFinalPosition(int position) {
+ mFinal = position;
+ mFinished = false;
+ }
+
+ void extendDuration(int extend) {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final int elapsedTime = (int) (time - mStartTime);
+ mDuration = elapsedTime + extend;
+ mFinished = false;
+ }
+
+ boolean springback(int start, int min, int max) {
+ mFinished = true;
+
+ mCurrentPosition = mStart = mFinal = start;
+ mVelocity = 0;
+
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mDuration = 0;
+
+ if (start < min) {
+ startSpringback(start, min, 0);
+ } else if (start > max) {
+ startSpringback(start, max, 0);
+ }
+
+ return !mFinished;
+ }
+
+ private void startSpringback(int start, int end, int velocity) {
+ // mStartTime has been set
+ mFinished = false;
+ mState = CUBIC;
+ mCurrentPosition = mStart = start;
+ mFinal = end;
+ final int delta = start - end;
+ mDeceleration = getDeceleration(delta);
+ // TODO take velocity into account
+ mVelocity = -delta; // only sign is used
+ mOver = Math.abs(delta);
+ mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+ }
+
+ void fling(int start, int velocity, int min, int max, int over) {
+ mOver = over;
+ mFinished = false;
+ mCurrVelocity = mVelocity = velocity;
+ mDuration = mSplineDuration = 0;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mCurrentPosition = mStart = start;
+
+ if (start > max || start < min) {
+ startAfterEdge(start, min, max, velocity);
+ return;
+ }
+
+ mState = SPLINE;
+ double totalDistance = 0.0;
+
+ if (velocity != 0) {
+ mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+ totalDistance = getSplineFlingDistance(velocity);
+ }
+
+ mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+ mFinal = start + mSplineDistance;
+
+ // Clamp to a valid final position
+ if (mFinal < min) {
+ adjustDuration(mStart, mFinal, min);
+ mFinal = min;
+ }
+
+ if (mFinal > max) {
+ adjustDuration(mStart, mFinal, max);
+ mFinal = max;
+ }
+ }
+
+ private double getSplineDeceleration(int velocity) {
+ return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+ }
+
+ private double getSplineFlingDistance(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+ }
+
+ /* Returns the duration, expressed in milliseconds */
+ private int getSplineFlingDuration(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return (int) (1000.0 * Math.exp(l / decelMinusOne));
+ }
+
+ private void fitOnBounceCurve(int start, int end, int velocity) {
+ // Simulate a bounce that started from edge
+ final float durationToApex = - velocity / mDeceleration;
+ // The float cast below is necessary to avoid integer overflow.
+ final float velocitySquared = (float) velocity * velocity;
+ final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
+ final float distanceToEdge = Math.abs(end - start);
+ final float totalDuration = (float) Math.sqrt(
+ 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+ mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+ mCurrentPosition = mStart = end;
+ mVelocity = (int) (- mDeceleration * totalDuration);
+ }
+
+ private void startBounceAfterEdge(int start, int end, int velocity) {
+ mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+ fitOnBounceCurve(start, end, velocity);
+ onEdgeReached();
+ }
+
+ private void startAfterEdge(int start, int min, int max, int velocity) {
+ if (start > min && start < max) {
+ Log.e("OverScroller", "startAfterEdge called from a valid position");
+ mFinished = true;
+ return;
+ }
+ final boolean positive = start > max;
+ final int edge = positive ? max : min;
+ final int overDistance = start - edge;
+ boolean keepIncreasing = overDistance * velocity >= 0;
+ if (keepIncreasing) {
+ // Will result in a bounce or a to_boundary depending on velocity.
+ startBounceAfterEdge(start, edge, velocity);
+ } else {
+ final double totalDistance = getSplineFlingDistance(velocity);
+ if (totalDistance > Math.abs(overDistance)) {
+ fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
+ } else {
+ startSpringback(start, edge, velocity);
+ }
+ }
+ }
+
+ void notifyEdgeReached(int start, int end, int over) {
+ // mState is used to detect successive notifications
+ if (mState == SPLINE) {
+ mOver = over;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ // We were in fling/scroll mode before: current velocity is such that distance to
+ // edge is increasing. This ensures that startAfterEdge will not start a new fling.
+ startAfterEdge(start, end, end, (int) mCurrVelocity);
+ }
+ }
+
+ private void onEdgeReached() {
+ // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+ // The float cast below is necessary to avoid integer overflow.
+ final float velocitySquared = (float) mVelocity * mVelocity;
+ float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
+ final float sign = Math.signum(mVelocity);
+
+ if (distance > mOver) {
+ // Default deceleration is not sufficient to slow us down before boundary
+ mDeceleration = - sign * velocitySquared / (2.0f * mOver);
+ distance = mOver;
+ }
+
+ mOver = (int) distance;
+ mState = BALLISTIC;
+ mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+ mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
+ }
+
+ boolean continueWhenFinished() {
+ switch (mState) {
+ case SPLINE:
+ // Duration from start to null velocity
+ if (mDuration < mSplineDuration) {
+ // If the animation was clamped, we reached the edge
+ mCurrentPosition = mStart = mFinal;
+ // TODO Better compute speed when edge was reached
+ mVelocity = (int) mCurrVelocity;
+ mDeceleration = getDeceleration(mVelocity);
+ mStartTime += mDuration;
+ onEdgeReached();
+ } else {
+ // Normal stop, no need to continue
+ return false;
+ }
+ break;
+ case BALLISTIC:
+ mStartTime += mDuration;
+ startSpringback(mFinal, mStart, 0);
+ break;
+ case CUBIC:
+ return false;
+ }
+
+ update();
+ return true;
+ }
+
+ /*
+ * Update the current position and velocity for current time. Returns
+ * true if update has been done and false if animation duration has been
+ * reached.
+ */
+ boolean update() {
+ final long time = AnimationUtils.currentAnimationTimeMillis();
+ final long currentTime = time - mStartTime;
+
+ if (currentTime == 0) {
+ // Skip work but report that we're still going if we have a nonzero duration.
+ return mDuration > 0;
+ }
+ if (currentTime > mDuration) {
+ return false;
+ }
+
+ double distance = 0.0;
+ switch (mState) {
+ case SPLINE: {
+ final float t = (float) currentTime / mSplineDuration;
+ final int index = (int) (NB_SAMPLES * t);
+ float distanceCoef = 1.f;
+ float velocityCoef = 0.f;
+ if (index < NB_SAMPLES) {
+ final float t_inf = (float) index / NB_SAMPLES;
+ final float t_sup = (float) (index + 1) / NB_SAMPLES;
+ final float d_inf = SPLINE_POSITION[index];
+ final float d_sup = SPLINE_POSITION[index + 1];
+ velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+ distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+ }
+
+ distance = distanceCoef * mSplineDistance;
+ mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+ break;
+ }
+
+ case BALLISTIC: {
+ final float t = currentTime / 1000.0f;
+ mCurrVelocity = mVelocity + mDeceleration * t;
+ distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+ break;
+ }
+
+ case CUBIC: {
+ final float t = (float) (currentTime) / mDuration;
+ final float t2 = t * t;
+ final float sign = Math.signum(mVelocity);
+ distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
+ mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
+ break;
+ }
+ }
+
+ mCurrentPosition = mStart + (int) Math.round(distance);
+
+ return true;
+ }
+ }
+}
diff --git a/android/widget/PopupMenu.java b/android/widget/PopupMenu.java
new file mode 100644
index 0000000..59bbc3b
--- /dev/null
+++ b/android/widget/PopupMenu.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2010 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.MenuRes;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * A PopupMenu displays a {@link Menu} in a modal popup window anchored to a
+ * {@link View}. The popup will appear below the anchor view if there is room,
+ * or above it if there is not. If the IME is visible the popup will not
+ * overlap it until it is touched. Touching outside of the popup will dismiss
+ * it.
+ */
+public class PopupMenu {
+ private final Context mContext;
+ private final MenuBuilder mMenu;
+ private final View mAnchor;
+ private final MenuPopupHelper mPopup;
+
+ private OnMenuItemClickListener mMenuItemClickListener;
+ private OnDismissListener mOnDismissListener;
+ private OnTouchListener mDragListener;
+
+ /**
+ * Constructor to create a new popup menu with an anchor view.
+ *
+ * @param context Context the popup menu is running in, through which it
+ * can access the current theme, resources, etc.
+ * @param anchor Anchor view for this popup. The popup will appear below
+ * the anchor if there is room, or above it if there is not.
+ */
+ public PopupMenu(Context context, View anchor) {
+ this(context, anchor, Gravity.NO_GRAVITY);
+ }
+
+ /**
+ * Constructor to create a new popup menu with an anchor view and alignment
+ * gravity.
+ *
+ * @param context Context the popup menu is running in, through which it
+ * can access the current theme, resources, etc.
+ * @param anchor Anchor view for this popup. The popup will appear below
+ * the anchor if there is room, or above it if there is not.
+ * @param gravity The {@link Gravity} value for aligning the popup with its
+ * anchor.
+ */
+ public PopupMenu(Context context, View anchor, int gravity) {
+ this(context, anchor, gravity, R.attr.popupMenuStyle, 0);
+ }
+
+ /**
+ * Constructor a create a new popup menu with a specific style.
+ *
+ * @param context Context the popup menu is running in, through which it
+ * can access the current theme, resources, etc.
+ * @param anchor Anchor view for this popup. The popup will appear below
+ * the anchor if there is room, or above it if there is not.
+ * @param gravity The {@link Gravity} value for aligning the popup with its
+ * anchor.
+ * @param popupStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the popup window. Can be 0 to not look for defaults.
+ * @param popupStyleRes A resource identifier of a style resource that
+ * supplies default values for the popup window, used only if
+ * popupStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
+ */
+ public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr,
+ int popupStyleRes) {
+ mContext = context;
+ mAnchor = anchor;
+
+ mMenu = new MenuBuilder(context);
+ mMenu.setCallback(new MenuBuilder.Callback() {
+ @Override
+ public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+ if (mMenuItemClickListener != null) {
+ return mMenuItemClickListener.onMenuItemClick(item);
+ }
+ return false;
+ }
+
+ @Override
+ public void onMenuModeChange(MenuBuilder menu) {
+ }
+ });
+
+ mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes);
+ mPopup.setGravity(gravity);
+ mPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ if (mOnDismissListener != null) {
+ mOnDismissListener.onDismiss(PopupMenu.this);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets the gravity used to align the popup window to its anchor view.
+ * <p>
+ * If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.
+ *
+ * @param gravity the gravity used to align the popup window
+ * @see #getGravity()
+ */
+ public void setGravity(int gravity) {
+ mPopup.setGravity(gravity);
+ }
+
+ /**
+ * @return the gravity used to align the popup window to its anchor view
+ * @see #setGravity(int)
+ */
+ public int getGravity() {
+ return mPopup.getGravity();
+ }
+
+ /**
+ * Returns an {@link OnTouchListener} that can be added to the anchor view
+ * to implement drag-to-open behavior.
+ * <p>
+ * When the listener is set on a view, touching that view and dragging
+ * outside of its bounds will open the popup window. Lifting will select
+ * the currently touched list item.
+ * <p>
+ * Example usage:
+ * <pre>
+ * PopupMenu myPopup = new PopupMenu(context, myAnchor);
+ * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
+ * </pre>
+ *
+ * @return a touch listener that controls drag-to-open behavior
+ */
+ public OnTouchListener getDragToOpenListener() {
+ if (mDragListener == null) {
+ mDragListener = new ForwardingListener(mAnchor) {
+ @Override
+ protected boolean onForwardingStarted() {
+ show();
+ return true;
+ }
+
+ @Override
+ protected boolean onForwardingStopped() {
+ dismiss();
+ return true;
+ }
+
+ @Override
+ public ShowableListMenu getPopup() {
+ // This will be null until show() is called.
+ return mPopup.getPopup();
+ }
+ };
+ }
+
+ return mDragListener;
+ }
+
+ /**
+ * Returns the {@link Menu} associated with this popup. Populate the
+ * returned Menu with items before calling {@link #show()}.
+ *
+ * @return the {@link Menu} associated with this popup
+ * @see #show()
+ * @see #getMenuInflater()
+ */
+ public Menu getMenu() {
+ return mMenu;
+ }
+
+ /**
+ * @return a {@link MenuInflater} that can be used to inflate menu items
+ * from XML into the menu returned by {@link #getMenu()}
+ * @see #getMenu()
+ */
+ public MenuInflater getMenuInflater() {
+ return new MenuInflater(mContext);
+ }
+
+ /**
+ * Inflate a menu resource into this PopupMenu. This is equivalent to
+ * calling {@code popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu())}.
+ *
+ * @param menuRes Menu resource to inflate
+ */
+ public void inflate(@MenuRes int menuRes) {
+ getMenuInflater().inflate(menuRes, mMenu);
+ }
+
+ /**
+ * Show the menu popup anchored to the view specified during construction.
+ *
+ * @see #dismiss()
+ */
+ public void show() {
+ mPopup.show();
+ }
+
+ /**
+ * Dismiss the menu popup.
+ *
+ * @see #show()
+ */
+ public void dismiss() {
+ mPopup.dismiss();
+ }
+
+ /**
+ * Sets a listener that will be notified when the user selects an item from
+ * the menu.
+ *
+ * @param listener the listener to notify
+ */
+ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+ mMenuItemClickListener = listener;
+ }
+
+ /**
+ * Sets a listener that will be notified when this menu is dismissed.
+ *
+ * @param listener the listener to notify
+ */
+ public void setOnDismissListener(OnDismissListener listener) {
+ mOnDismissListener = listener;
+ }
+
+ /**
+ * Interface responsible for receiving menu item click events if the items
+ * themselves do not have individual item click listeners.
+ */
+ public interface OnMenuItemClickListener {
+ /**
+ * This method will be invoked when a menu item is clicked if the item
+ * itself did not already handle the event.
+ *
+ * @param item the menu item that was clicked
+ * @return {@code true} if the event was handled, {@code false}
+ * otherwise
+ */
+ boolean onMenuItemClick(MenuItem item);
+ }
+
+ /**
+ * Callback interface used to notify the application that the menu has closed.
+ */
+ public interface OnDismissListener {
+ /**
+ * Called when the associated menu has been dismissed.
+ *
+ * @param menu the popup menu that was dismissed
+ */
+ void onDismiss(PopupMenu menu);
+ }
+
+ /**
+ * Returns the {@link ListView} representing the list of menu items in the currently showing
+ * menu.
+ *
+ * @return The view representing the list of menu items.
+ * @hide
+ */
+ @TestApi
+ public ListView getMenuListView() {
+ if (!mPopup.isShowing()) {
+ return null;
+ }
+ return mPopup.getPopup().getListView();
+ }
+}
diff --git a/android/widget/PopupWindow.java b/android/widget/PopupWindow.java
new file mode 100644
index 0000000..bf25915
--- /dev/null
+++ b/android/widget/PopupWindow.java
@@ -0,0 +1,2591 @@
+/*
+ * 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 static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams
+ .PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
+import android.os.IBinder;
+import android.transition.Transition;
+import android.transition.Transition.EpicenterCallback;
+import android.transition.Transition.TransitionListener;
+import android.transition.TransitionInflater;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.R;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * <p>
+ * This class represents a popup window that can be used to display an
+ * arbitrary view. The popup window is a floating container that appears on top
+ * of the current activity.
+ * </p>
+ * <a name="Animation"></a>
+ * <h3>Animation</h3>
+ * <p>
+ * On all versions of Android, popup window enter and exit animations may be
+ * specified by calling {@link #setAnimationStyle(int)} and passing the
+ * resource ID for an animation style that defines {@code windowEnterAnimation}
+ * and {@code windowExitAnimation}. For example, passing
+ * {@link android.R.style#Animation_Dialog} will give a scale and alpha
+ * animation.
+ * </br>
+ * A window animation style may also be specified in the popup window's style
+ * XML via the {@link android.R.styleable#PopupWindow_popupAnimationStyle popupAnimationStyle}
+ * attribute.
+ * </p>
+ * <p>
+ * Starting with API 23, more complex popup window enter and exit transitions
+ * may be specified by calling either {@link #setEnterTransition(Transition)}
+ * or {@link #setExitTransition(Transition)} and passing a {@link Transition}.
+ * </br>
+ * Popup enter and exit transitions may also be specified in the popup window's
+ * style XML via the {@link android.R.styleable#PopupWindow_popupEnterTransition popupEnterTransition}
+ * and {@link android.R.styleable#PopupWindow_popupExitTransition popupExitTransition}
+ * attributes, respectively.
+ * </p>
+ *
+ * @attr ref android.R.styleable#PopupWindow_overlapAnchor
+ * @attr ref android.R.styleable#PopupWindow_popupAnimationStyle
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ * @attr ref android.R.styleable#PopupWindow_popupElevation
+ * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+ * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class PopupWindow {
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: the requirements for the
+ * input method should be based on the focusability of the popup. That is
+ * if it is focusable than it needs to work with the input method, else
+ * it doesn't.
+ */
+ public static final int INPUT_METHOD_FROM_FOCUSABLE = 0;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed so that the user can also operate
+ * the input method while it is shown.
+ */
+ public static final int INPUT_METHOD_NEEDED = 1;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed to use as much space on the
+ * screen as needed, regardless of whether this covers the input method.
+ */
+ public static final int INPUT_METHOD_NOT_NEEDED = 2;
+
+ private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START;
+
+ /**
+ * Default animation style indicating that separate animations should be
+ * used for top/bottom anchoring states.
+ */
+ private static final int ANIMATION_STYLE_DEFAULT = -1;
+
+ private final int[] mTmpDrawingLocation = new int[2];
+ private final int[] mTmpScreenLocation = new int[2];
+ private final int[] mTmpAppLocation = new int[2];
+ private final Rect mTempRect = new Rect();
+
+ private Context mContext;
+ private WindowManager mWindowManager;
+
+ /**
+ * Keeps track of popup's parent's decor view. This is needed to dispatch
+ * requestKeyboardShortcuts to the owning Activity.
+ */
+ private WeakReference<View> mParentRootView;
+
+ private boolean mIsShowing;
+ private boolean mIsTransitioningToDismiss;
+ private boolean mIsDropdown;
+
+ /** View that handles event dispatch and content transitions. */
+ private PopupDecorView mDecorView;
+
+ /** View that holds the background and may animate during a transition. */
+ private View mBackgroundView;
+
+ /** The contents of the popup. May be identical to the background view. */
+ private View mContentView;
+
+ private boolean mFocusable;
+ private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE;
+ @SoftInputModeFlags
+ private int mSoftInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
+ private boolean mTouchable = true;
+ private boolean mOutsideTouchable = false;
+ private boolean mClippingEnabled = true;
+ private int mSplitTouchEnabled = -1;
+ private boolean mLayoutInScreen;
+ private boolean mClipToScreen;
+ private boolean mAllowScrollingAnchorParent = true;
+ private boolean mLayoutInsetDecor = false;
+ private boolean mNotTouchModal;
+ private boolean mAttachedInDecor = true;
+ private boolean mAttachedInDecorSet = false;
+
+ private OnTouchListener mTouchInterceptor;
+
+ private int mWidthMode;
+ private int mWidth = LayoutParams.WRAP_CONTENT;
+ private int mLastWidth;
+ private int mHeightMode;
+ private int mHeight = LayoutParams.WRAP_CONTENT;
+ private int mLastHeight;
+
+ private float mElevation;
+
+ private Drawable mBackground;
+ private Drawable mAboveAnchorBackgroundDrawable;
+ private Drawable mBelowAnchorBackgroundDrawable;
+
+ private Transition mEnterTransition;
+ private Transition mExitTransition;
+ private Rect mEpicenterBounds;
+
+ private boolean mAboveAnchor;
+ private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+
+ private OnDismissListener mOnDismissListener;
+ private boolean mIgnoreCheekPress = false;
+
+ private int mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+
+ private int mGravity = Gravity.NO_GRAVITY;
+
+ private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
+ com.android.internal.R.attr.state_above_anchor
+ };
+
+ private final OnAttachStateChangeListener mOnAnchorDetachedListener =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // Anchor might have been reattached in a different position.
+ alignToAnchor();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ // Leave the popup in its current position.
+ // The anchor might become attached again.
+ }
+ };
+
+ private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {}
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mIsAnchorRootAttached = false;
+ }
+ };
+
+ private WeakReference<View> mAnchor;
+ private WeakReference<View> mAnchorRoot;
+ private boolean mIsAnchorRootAttached;
+
+ private final OnScrollChangedListener mOnScrollChangedListener = this::alignToAnchor;
+
+ private final View.OnLayoutChangeListener mOnLayoutChangeListener =
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> alignToAnchor();
+
+ private int mAnchorXoff;
+ private int mAnchorYoff;
+ private int mAnchoredGravity;
+ private boolean mOverlapAnchor;
+
+ private boolean mPopupViewInitialLayoutDirectionInherited;
+
+ /**
+ * <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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does not provide a background.</p>
+ */
+ public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
+ final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
+ mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
+ mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
+
+ // Preserve default behavior from Gingerbread. If the animation is
+ // undefined or explicitly specifies the Gingerbread animation style,
+ // use a sentinel value.
+ if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) {
+ final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0);
+ if (animStyle == R.style.Animation_PopupWindow) {
+ mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+ } else {
+ mAnimationStyle = animStyle;
+ }
+ } else {
+ mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+ }
+
+ final Transition enterTransition = getTransition(a.getResourceId(
+ R.styleable.PopupWindow_popupEnterTransition, 0));
+ final Transition exitTransition;
+ if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition)) {
+ exitTransition = getTransition(a.getResourceId(
+ R.styleable.PopupWindow_popupExitTransition, 0));
+ } else {
+ exitTransition = enterTransition == null ? null : enterTransition.clone();
+ }
+
+ a.recycle();
+
+ setEnterTransition(enterTransition);
+ setExitTransition(exitTransition);
+ setBackgroundDrawable(bg);
+ }
+
+ /**
+ * <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) {
+ if (contentView != null) {
+ mContext = contentView.getContext();
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ }
+
+ setContentView(contentView);
+ setWidth(width);
+ setHeight(height);
+ setFocusable(focusable);
+ }
+
+ /**
+ * Sets the enter transition to be used when the popup window is shown.
+ *
+ * @param enterTransition the enter transition, or {@code null} to clear
+ * @see #getEnterTransition()
+ * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+ */
+ public void setEnterTransition(@Nullable Transition enterTransition) {
+ mEnterTransition = enterTransition;
+ }
+
+ /**
+ * Returns the enter transition to be used when the popup window is shown.
+ *
+ * @return the enter transition, or {@code null} if not set
+ * @see #setEnterTransition(Transition)
+ * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+ */
+ @Nullable
+ public Transition getEnterTransition() {
+ return mEnterTransition;
+ }
+
+ /**
+ * Sets the exit transition to be used when the popup window is dismissed.
+ *
+ * @param exitTransition the exit transition, or {@code null} to clear
+ * @see #getExitTransition()
+ * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+ */
+ public void setExitTransition(@Nullable Transition exitTransition) {
+ mExitTransition = exitTransition;
+ }
+
+ /**
+ * Returns the exit transition to be used when the popup window is
+ * dismissed.
+ *
+ * @return the exit transition, or {@code null} if not set
+ * @see #setExitTransition(Transition)
+ * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+ */
+ @Nullable
+ public Transition getExitTransition() {
+ return mExitTransition;
+ }
+
+ /**
+ * Sets the bounds used as the epicenter of the enter and exit transitions.
+ * <p>
+ * Transitions use a point or Rect, referred to as the epicenter, to orient
+ * the direction of travel. For popup windows, the anchor view bounds are
+ * used as the default epicenter.
+ * <p>
+ * See {@link Transition#setEpicenterCallback(EpicenterCallback)} for more
+ * information about how transition epicenters.
+ *
+ * @param bounds the epicenter bounds relative to the anchor view, or
+ * {@code null} to use the default epicenter
+ * @see #getTransitionEpicenter()
+ * @hide
+ */
+ public void setEpicenterBounds(Rect bounds) {
+ mEpicenterBounds = bounds;
+ }
+
+ private Transition getTransition(int resId) {
+ if (resId != 0 && resId != R.transition.no_transition) {
+ final TransitionInflater inflater = TransitionInflater.from(mContext);
+ final Transition transition = inflater.inflateTransition(resId);
+ if (transition != null) {
+ final boolean isEmpty = transition instanceof TransitionSet
+ && ((TransitionSet) transition).getTransitionCount() == 0;
+ if (!isEmpty) {
+ return transition;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the drawable used as the popup window's background.
+ *
+ * @return the background drawable or {@code null} if not set
+ * @see #setBackgroundDrawable(Drawable)
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public Drawable getBackground() {
+ return mBackground;
+ }
+
+ /**
+ * Specifies the background drawable for this popup window. The background
+ * can be set to {@code null}.
+ *
+ * @param background the popup's background
+ * @see #getBackground()
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public void setBackgroundDrawable(Drawable background) {
+ mBackground = background;
+
+ // If this is a StateListDrawable, try to find and store the drawable to be
+ // used when the drop-down is placed above its anchor view, and the one to be
+ // used when the drop-down is placed below its anchor view. We extract
+ // the drawables ourselves to work around a problem with using refreshDrawableState
+ // that it will take into account the padding of all drawables specified in a
+ // StateListDrawable, thus adding superfluous padding to drop-down views.
+ //
+ // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and
+ // at least one other drawable, intended for the 'below-anchor state'.
+ if (mBackground instanceof StateListDrawable) {
+ StateListDrawable stateList = (StateListDrawable) mBackground;
+
+ // Find the above-anchor view - this one's easy, it should be labeled as such.
+ int aboveAnchorStateIndex = stateList.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET);
+
+ // Now, for the below-anchor view, look for any other drawable specified in the
+ // StateListDrawable which is not for the above-anchor state and use that.
+ int count = stateList.getStateCount();
+ int belowAnchorStateIndex = -1;
+ for (int i = 0; i < count; i++) {
+ if (i != aboveAnchorStateIndex) {
+ belowAnchorStateIndex = i;
+ break;
+ }
+ }
+
+ // Store the drawables we found, if we found them. Otherwise, set them both
+ // to null so that we'll just use refreshDrawableState.
+ if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) {
+ mAboveAnchorBackgroundDrawable = stateList.getStateDrawable(aboveAnchorStateIndex);
+ mBelowAnchorBackgroundDrawable = stateList.getStateDrawable(belowAnchorStateIndex);
+ } else {
+ mBelowAnchorBackgroundDrawable = null;
+ mAboveAnchorBackgroundDrawable = null;
+ }
+ }
+ }
+
+ /**
+ * @return the elevation for this popup window in pixels
+ * @see #setElevation(float)
+ * @attr ref android.R.styleable#PopupWindow_popupElevation
+ */
+ public float getElevation() {
+ return mElevation;
+ }
+
+ /**
+ * Specifies the elevation for this popup window.
+ *
+ * @param elevation the popup's elevation in pixels
+ * @see #getElevation()
+ * @attr ref android.R.styleable#PopupWindow_popupElevation
+ */
+ public void setElevation(float elevation) {
+ mElevation = elevation;
+ }
+
+ /**
+ * <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; by default this flag
+ * is set to false
+ * which means the popup will not ignore cheek press dispatch events.
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @see #update()
+ */
+ public void setIgnoreCheekPress() {
+ mIgnoreCheekPress = true;
+ }
+
+ /**
+ * <p>Change the animation style resource for this popup.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @param animationStyle animation style to use when the popup appears
+ * and disappears. Set to -1 for the default animation, 0 for no
+ * animation, or a resource identifier for an explicit animation.
+ *
+ * @see #update()
+ */
+ 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;
+
+ if (mContext == null && mContentView != null) {
+ mContext = mContentView.getContext();
+ }
+
+ if (mWindowManager == null && mContentView != null) {
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ }
+
+ // Setting the default for attachedInDecor based on SDK version here
+ // instead of in the constructor since we might not have the context
+ // object in the constructor. We only want to set default here if the
+ // app hasn't already set the attachedInDecor.
+ if (mContext != null && !mAttachedInDecorSet) {
+ // Attach popup window in decor frame of parent window by default for
+ // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current
+ // behavior of not attaching to decor frame for older SDKs.
+ setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
+ >= Build.VERSION_CODES.LOLLIPOP_MR1);
+ }
+
+ }
+
+ /**
+ * Set a callback for all touch events being dispatched to the popup
+ * window.
+ */
+ public void setTouchInterceptor(OnTouchListener l) {
+ mTouchInterceptor = l;
+ }
+
+ /**
+ * <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}. By default a popup
+ * window is not focusable.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @param focusable true if the popup should grab focus, false otherwise.
+ *
+ * @see #isFocusable()
+ * @see #isShowing()
+ * @see #update()
+ */
+ public void setFocusable(boolean focusable) {
+ mFocusable = focusable;
+ }
+
+ /**
+ * Return the current value in {@link #setInputMethodMode(int)}.
+ *
+ * @see #setInputMethodMode(int)
+ */
+ public int getInputMethodMode() {
+ return mInputMethodMode;
+
+ }
+
+ /**
+ * Control how the popup operates with an input method: one of
+ * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+ * or {@link #INPUT_METHOD_NOT_NEEDED}.
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @see #getInputMethodMode()
+ * @see #update()
+ */
+ public void setInputMethodMode(int mode) {
+ mInputMethodMode = mode;
+ }
+
+ /**
+ * Sets the operating mode for the soft input area.
+ *
+ * @param mode The desired mode, see
+ * {@link android.view.WindowManager.LayoutParams#softInputMode}
+ * for the full list
+ *
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ * @see #getSoftInputMode()
+ */
+ public void setSoftInputMode(@SoftInputModeFlags int mode) {
+ mSoftInputMode = mode;
+ }
+
+ /**
+ * Returns the current value in {@link #setSoftInputMode(int)}.
+ *
+ * @see #setSoftInputMode(int)
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ */
+ @SoftInputModeFlags
+ public int getSoftInputMode() {
+ return mSoftInputMode;
+ }
+
+ /**
+ * <p>Indicates whether the popup window receives touch events.</p>
+ *
+ * @return true if the popup is touchable, false otherwise
+ *
+ * @see #setTouchable(boolean)
+ */
+ public boolean isTouchable() {
+ return mTouchable;
+ }
+
+ /**
+ * <p>Changes the touchability of the popup window. When touchable, the
+ * window will receive touch events, otherwise touch events will go to the
+ * window below it. By default the window is touchable.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @param touchable true if the popup should receive touch events, false otherwise
+ *
+ * @see #isTouchable()
+ * @see #isShowing()
+ * @see #update()
+ */
+ public void setTouchable(boolean touchable) {
+ mTouchable = touchable;
+ }
+
+ /**
+ * <p>Indicates whether the popup window will be informed of touch events
+ * outside of its window.</p>
+ *
+ * @return true if the popup is outside touchable, false otherwise
+ *
+ * @see #setOutsideTouchable(boolean)
+ */
+ public boolean isOutsideTouchable() {
+ return mOutsideTouchable;
+ }
+
+ /**
+ * <p>Controls whether the pop-up will be informed of touch events outside
+ * of its window. This only makes sense for pop-ups that are touchable
+ * but not focusable, which means touches outside of the window will
+ * be delivered to the window behind. The default is false.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @param touchable true if the popup should receive outside
+ * touch events, false otherwise
+ *
+ * @see #isOutsideTouchable()
+ * @see #isShowing()
+ * @see #update()
+ */
+ public void setOutsideTouchable(boolean touchable) {
+ mOutsideTouchable = touchable;
+ }
+
+ /**
+ * <p>Indicates whether clipping of the popup window is enabled.</p>
+ *
+ * @return true if the clipping is enabled, false otherwise
+ *
+ * @see #setClippingEnabled(boolean)
+ */
+ public boolean isClippingEnabled() {
+ return mClippingEnabled;
+ }
+
+ /**
+ * <p>Allows the popup window to extend beyond the bounds of the screen. By default the
+ * window is clipped to the screen boundaries. Setting this to false will allow windows to be
+ * accurately positioned.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to one of
+ * the {@link #update()} methods.</p>
+ *
+ * @param enabled false if the window should be allowed to extend outside of the screen
+ * @see #isShowing()
+ * @see #isClippingEnabled()
+ * @see #update()
+ */
+ public void setClippingEnabled(boolean enabled) {
+ mClippingEnabled = enabled;
+ }
+
+ /**
+ * Clip this popup window to the screen, but not to the containing window.
+ *
+ * @param enabled True to clip to the screen.
+ * @hide
+ */
+ public void setClipToScreenEnabled(boolean enabled) {
+ mClipToScreen = enabled;
+ }
+
+ /**
+ * Allow PopupWindow to scroll the anchor's parent to provide more room
+ * for the popup. Enabled by default.
+ *
+ * @param enabled True to scroll the anchor's parent when more room is desired by the popup.
+ */
+ void setAllowScrollingAnchorParent(boolean enabled) {
+ mAllowScrollingAnchorParent = enabled;
+ }
+
+ /** @hide */
+ protected final boolean getAllowScrollingAnchorParent() {
+ return mAllowScrollingAnchorParent;
+ }
+
+ /**
+ * <p>Indicates whether the popup window supports splitting touches.</p>
+ *
+ * @return true if the touch splitting is enabled, false otherwise
+ *
+ * @see #setSplitTouchEnabled(boolean)
+ */
+ public boolean isSplitTouchEnabled() {
+ if (mSplitTouchEnabled < 0 && mContext != null) {
+ return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
+ }
+ return mSplitTouchEnabled == 1;
+ }
+
+ /**
+ * <p>Allows the popup window to split touches across other windows that also
+ * support split touch. When this flag is false, the first pointer
+ * that goes down determines the window to which all subsequent touches
+ * go until all pointers go up. When this flag is true, each pointer
+ * (not necessarily the first) that goes down determines the window
+ * to which all subsequent touches of that pointer will go until that
+ * pointer goes up thereby enabling touches with multiple pointers
+ * to be split across multiple windows.</p>
+ *
+ * @param enabled true if the split touches should be enabled, false otherwise
+ * @see #isSplitTouchEnabled()
+ */
+ public void setSplitTouchEnabled(boolean enabled) {
+ mSplitTouchEnabled = enabled ? 1 : 0;
+ }
+
+ /**
+ * <p>Indicates whether the popup window will be forced into using absolute screen coordinates
+ * for positioning.</p>
+ *
+ * @return true if the window will always be positioned in screen coordinates.
+ * @hide
+ */
+ public boolean isLayoutInScreenEnabled() {
+ return mLayoutInScreen;
+ }
+
+ /**
+ * <p>Allows the popup window to force the flag
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN}, overriding default behavior.
+ * This will cause the popup to be positioned in absolute screen coordinates.</p>
+ *
+ * @param enabled true if the popup should always be positioned in screen coordinates
+ * @hide
+ */
+ public void setLayoutInScreenEnabled(boolean enabled) {
+ mLayoutInScreen = enabled;
+ }
+
+ /**
+ * <p>Indicates whether the popup window will be attached in the decor frame of its parent
+ * window.
+ *
+ * @return true if the window will be attached to the decor frame of its parent window.
+ *
+ * @see #setAttachedInDecor(boolean)
+ * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR
+ */
+ public boolean isAttachedInDecor() {
+ return mAttachedInDecor;
+ }
+
+ /**
+ * <p>This will attach the popup window to the decor frame of the parent window to avoid
+ * overlaping with screen decorations like the navigation bar. Overrides the default behavior of
+ * the flag {@link WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR}.
+ *
+ * <p>By default the flag is set on SDK version {@link Build.VERSION_CODES#LOLLIPOP_MR1} or
+ * greater and cleared on lesser SDK versions.
+ *
+ * @param enabled true if the popup should be attached to the decor frame of its parent window.
+ *
+ * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR
+ */
+ public void setAttachedInDecor(boolean enabled) {
+ mAttachedInDecor = enabled;
+ mAttachedInDecorSet = true;
+ }
+
+ /**
+ * Allows the popup window to force the flag
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}, overriding default behavior.
+ * This will cause the popup to inset its content to account for system windows overlaying
+ * the screen, such as the status bar.
+ *
+ * <p>This will often be combined with {@link #setLayoutInScreenEnabled(boolean)}.
+ *
+ * @param enabled true if the popup's views should inset content to account for system windows,
+ * the way that decor views behave for full-screen windows.
+ * @hide
+ */
+ public void setLayoutInsetDecor(boolean enabled) {
+ mLayoutInsetDecor = enabled;
+ }
+
+ /** @hide */
+ protected final boolean isLayoutInsetDecor() {
+ return mLayoutInsetDecor;
+ }
+
+ /**
+ * Set the layout type for this window.
+ * <p>
+ * See {@link WindowManager.LayoutParams#type} for possible values.
+ *
+ * @param layoutType Layout type for this window.
+ *
+ * @see WindowManager.LayoutParams#type
+ */
+ public void setWindowLayoutType(int layoutType) {
+ mWindowLayoutType = layoutType;
+ }
+
+ /**
+ * Returns the layout type for this window.
+ *
+ * @see #setWindowLayoutType(int)
+ */
+ public int getWindowLayoutType() {
+ return mWindowLayoutType;
+ }
+
+ /**
+ * Set whether this window is touch modal or if outside touches will be sent to
+ * other windows behind it.
+ * @hide
+ */
+ public void setTouchModal(boolean touchModal) {
+ mNotTouchModal = !touchModal;
+ }
+
+ /**
+ * <p>Change the width and height measure specs that are given to the
+ * window manager by the popup. By default these are 0, meaning that
+ * the current width or height is requested as an explicit size from
+ * the window manager. You can supply
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or
+ * {@link ViewGroup.LayoutParams#MATCH_PARENT} to have that measure
+ * spec supplied instead, replacing the absolute width and height that
+ * has been set in the popup.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.</p>
+ *
+ * @param widthSpec an explicit width measure spec mode, either
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT},
+ * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute
+ * width.
+ * @param heightSpec an explicit height measure spec mode, either
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT},
+ * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute
+ * height.
+ *
+ * @deprecated Use {@link #setWidth(int)} and {@link #setHeight(int)}.
+ */
+ @Deprecated
+ public void setWindowLayoutMode(int widthSpec, int heightSpec) {
+ mWidthMode = widthSpec;
+ mHeightMode = heightSpec;
+ }
+
+ /**
+ * Returns the popup's requested height. May be a layout constant such as
+ * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+ * <p>
+ * The actual size of the popup may depend on other factors such as
+ * clipping and window layout.
+ *
+ * @return the popup height in pixels or a layout constant
+ * @see #setHeight(int)
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Sets the popup's requested height. May be a layout constant such as
+ * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+ * <p>
+ * The actual size of the popup may depend on other factors such as
+ * clipping and window layout.
+ * <p>
+ * If the popup is showing, calling this method will take effect the next
+ * time the popup is shown.
+ *
+ * @param height the popup height in pixels or a layout constant
+ * @see #getHeight()
+ * @see #isShowing()
+ */
+ public void setHeight(int height) {
+ mHeight = height;
+ }
+
+ /**
+ * Returns the popup's requested width. May be a layout constant such as
+ * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+ * <p>
+ * The actual size of the popup may depend on other factors such as
+ * clipping and window layout.
+ *
+ * @return the popup width in pixels or a layout constant
+ * @see #setWidth(int)
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Sets the popup's requested width. May be a layout constant such as
+ * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+ * <p>
+ * The actual size of the popup may depend on other factors such as
+ * clipping and window layout.
+ * <p>
+ * If the popup is showing, calling this method will take effect the next
+ * time the popup is shown.
+ *
+ * @param width the popup width in pixels or a layout constant
+ * @see #getWidth()
+ * @see #isShowing()
+ */
+ public void setWidth(int width) {
+ mWidth = width;
+ }
+
+ /**
+ * Sets whether the popup window should overlap its anchor view when
+ * displayed as a drop-down.
+ * <p>
+ * If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.
+ *
+ * @param overlapAnchor Whether the popup should overlap its anchor.
+ *
+ * @see #getOverlapAnchor()
+ * @see #isShowing()
+ */
+ public void setOverlapAnchor(boolean overlapAnchor) {
+ mOverlapAnchor = overlapAnchor;
+ }
+
+ /**
+ * Returns whether the popup window should overlap its anchor view when
+ * displayed as a drop-down.
+ *
+ * @return Whether the popup should overlap its anchor.
+ *
+ * @see #setOverlapAnchor(boolean)
+ */
+ public boolean getOverlapAnchor() {
+ return mOverlapAnchor;
+ }
+
+ /**
+ * <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;
+ }
+
+ /** @hide */
+ protected final void setShowing(boolean isShowing) {
+ mIsShowing = isShowing;
+ }
+
+ /** @hide */
+ protected final void setDropDown(boolean isDropDown) {
+ mIsDropdown = isDropDown;
+ }
+
+ /** @hide */
+ protected final void setTransitioningToDismiss(boolean transitioningToDismiss) {
+ mIsTransitioningToDismiss = transitioningToDismiss;
+ }
+
+ /** @hide */
+ protected final boolean isTransitioningToDismiss() {
+ return mIsTransitioningToDismiss;
+ }
+
+ /**
+ * <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) {
+ mParentRootView = new WeakReference<>(parent.getRootView());
+ showAtLocation(parent.getWindowToken(), gravity, x, y);
+ }
+
+ /**
+ * Display the content view in a popup window at the specified location.
+ *
+ * @param token Window token to use for creating the new window
+ * @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
+ *
+ * @hide Internal use only. Applications should use
+ * {@link #showAtLocation(View, int, int, int)} instead.
+ */
+ public void showAtLocation(IBinder token, int gravity, int x, int y) {
+ if (isShowing() || mContentView == null) {
+ return;
+ }
+
+ TransitionManager.endTransitions(mDecorView);
+
+ detachFromAnchor();
+
+ mIsShowing = true;
+ mIsDropdown = false;
+ mGravity = gravity;
+
+ final WindowManager.LayoutParams p = createPopupLayoutParams(token);
+ preparePopup(p);
+
+ p.x = x;
+ p.y = y;
+
+ invokePopup(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.
+ *
+ * @param anchor the view on which to pin the popup window
+ *
+ * @see #dismiss()
+ */
+ public void showAsDropDown(View anchor) {
+ showAsDropDown(anchor, 0, 0);
+ }
+
+ /**
+ * 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>
+ * If the view later scrolls to move <code>anchor</code> to a different
+ * location, the popup will be moved correspondingly.
+ *
+ * @param anchor the view on which to pin the popup window
+ * @param xoff A horizontal offset from the anchor in pixels
+ * @param yoff A vertical offset from the anchor in pixels
+ *
+ * @see #dismiss()
+ */
+ public void showAsDropDown(View anchor, int xoff, int yoff) {
+ showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY);
+ }
+
+ /**
+ * Displays the content view in a popup window anchored to the corner of
+ * another view. The window is positioned according to the specified
+ * gravity and offset by the specified x and y coordinates.
+ * <p>
+ * 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
+ * view can be scrolled, the specified vertical gravity will be ignored and
+ * the popup will anchor itself such that it is visible.
+ * <p>
+ * If the view later scrolls to move <code>anchor</code> to a different
+ * location, the popup will be moved correspondingly.
+ *
+ * @param anchor the view on which to pin the popup window
+ * @param xoff A horizontal offset from the anchor in pixels
+ * @param yoff A vertical offset from the anchor in pixels
+ * @param gravity Alignment of the popup relative to the anchor
+ *
+ * @see #dismiss()
+ */
+ public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
+ if (isShowing() || !hasContentView()) {
+ return;
+ }
+
+ TransitionManager.endTransitions(mDecorView);
+
+ attachToAnchor(anchor, xoff, yoff, gravity);
+
+ mIsShowing = true;
+ mIsDropdown = true;
+
+ final WindowManager.LayoutParams p =
+ createPopupLayoutParams(anchor.getApplicationWindowToken());
+ preparePopup(p);
+
+ final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
+ p.width, p.height, gravity, mAllowScrollingAnchorParent);
+ updateAboveAnchor(aboveAnchor);
+ p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
+
+ invokePopup(p);
+ }
+
+ /** @hide */
+ protected final void updateAboveAnchor(boolean aboveAnchor) {
+ if (aboveAnchor != mAboveAnchor) {
+ mAboveAnchor = aboveAnchor;
+
+ if (mBackground != null && mBackgroundView != null) {
+ // If the background drawable provided was a StateListDrawable
+ // with above-anchor and below-anchor states, use those.
+ // Otherwise, rely on refreshDrawableState to do the job.
+ if (mAboveAnchorBackgroundDrawable != null) {
+ if (mAboveAnchor) {
+ mBackgroundView.setBackground(mAboveAnchorBackgroundDrawable);
+ } else {
+ mBackgroundView.setBackground(mBelowAnchorBackgroundDrawable);
+ }
+ } else {
+ mBackgroundView.refreshDrawableState();
+ }
+ }
+ }
+ }
+
+ /**
+ * Indicates whether the popup is showing above (the y coordinate of the popup's bottom
+ * is less than the y coordinate of the anchor) or below the anchor view (the y coordinate
+ * of the popup is greater than y coordinate of the anchor's bottom).
+ *
+ * The value returned
+ * by this method is meaningful only after {@link #showAsDropDown(android.view.View)}
+ * or {@link #showAsDropDown(android.view.View, int, int)} was invoked.
+ *
+ * @return True if this popup is showing above the anchor view, false otherwise.
+ */
+ public boolean isAboveAnchor() {
+ return mAboveAnchor;
+ }
+
+ /**
+ * Prepare the popup by embedding it into a new ViewGroup if the background
+ * drawable is not null. If embedding is required, the layout parameters'
+ * height is modified to take into account the background's padding.
+ *
+ * @param p the layout parameters of the popup's content view
+ */
+ private void preparePopup(WindowManager.LayoutParams p) {
+ if (mContentView == null || mContext == null || mWindowManager == null) {
+ throw new IllegalStateException("You must specify a valid content view by "
+ + "calling setContentView() before attempting to show the popup.");
+ }
+
+ if (p.accessibilityTitle == null) {
+ p.accessibilityTitle = mContext.getString(R.string.popup_window_default_title);
+ }
+
+ // The old decor view may be transitioning out. Make sure it finishes
+ // and cleans up before we try to create another one.
+ if (mDecorView != null) {
+ mDecorView.cancelTransitions();
+ }
+
+ // When a background is available, we embed the content view within
+ // another view that owns the background drawable.
+ if (mBackground != null) {
+ mBackgroundView = createBackgroundView(mContentView);
+ mBackgroundView.setBackground(mBackground);
+ } else {
+ mBackgroundView = mContentView;
+ }
+
+ mDecorView = createDecorView(mBackgroundView);
+
+ // The background owner should be elevated so that it casts a shadow.
+ mBackgroundView.setElevation(mElevation);
+
+ // We may wrap that in another view, so we'll need to manually specify
+ // the surface insets.
+ p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);
+
+ mPopupViewInitialLayoutDirectionInherited =
+ (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
+ }
+
+ /**
+ * Wraps a content view in a PopupViewContainer.
+ *
+ * @param contentView the content view to wrap
+ * @return a PopupViewContainer that wraps the content view
+ */
+ private PopupBackgroundView createBackgroundView(View contentView) {
+ final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
+ final int height;
+ if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
+ height = WRAP_CONTENT;
+ } else {
+ height = MATCH_PARENT;
+ }
+
+ final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext);
+ final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(
+ MATCH_PARENT, height);
+ backgroundView.addView(contentView, listParams);
+
+ return backgroundView;
+ }
+
+ /**
+ * Wraps a content view in a FrameLayout.
+ *
+ * @param contentView the content view to wrap
+ * @return a FrameLayout that wraps the content view
+ */
+ private PopupDecorView createDecorView(View contentView) {
+ final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
+ final int height;
+ if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
+ height = WRAP_CONTENT;
+ } else {
+ height = MATCH_PARENT;
+ }
+
+ final PopupDecorView decorView = new PopupDecorView(mContext);
+ decorView.addView(contentView, MATCH_PARENT, height);
+ decorView.setClipChildren(false);
+ decorView.setClipToPadding(false);
+
+ return decorView;
+ }
+
+ /**
+ * <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) {
+ if (mContext != null) {
+ p.packageName = mContext.getPackageName();
+ }
+
+ final PopupDecorView decorView = mDecorView;
+ decorView.setFitsSystemWindows(mLayoutInsetDecor);
+
+ setLayoutDirectionFromAnchor();
+
+ mWindowManager.addView(decorView, p);
+
+ if (mEnterTransition != null) {
+ decorView.requestEnterTransition(mEnterTransition);
+ }
+ }
+
+ private void setLayoutDirectionFromAnchor() {
+ if (mAnchor != null) {
+ View anchor = mAnchor.get();
+ if (anchor != null && mPopupViewInitialLayoutDirectionInherited) {
+ mDecorView.setLayoutDirection(anchor.getLayoutDirection());
+ }
+ }
+ }
+
+ private int computeGravity() {
+ int gravity = mGravity == Gravity.NO_GRAVITY ? Gravity.START | Gravity.TOP : mGravity;
+ if (mIsDropdown && (mClipToScreen || mClippingEnabled)) {
+ gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
+ }
+ return gravity;
+ }
+
+ /**
+ * <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
+ *
+ * @hide
+ */
+ protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
+ final 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 = computeGravity();
+ p.flags = computeFlags(p.flags);
+ p.type = mWindowLayoutType;
+ p.token = token;
+ p.softInputMode = mSoftInputMode;
+ p.windowAnimations = computeAnimationResource();
+
+ if (mBackground != null) {
+ p.format = mBackground.getOpacity();
+ } else {
+ p.format = PixelFormat.TRANSLUCENT;
+ }
+
+ if (mHeightMode < 0) {
+ p.height = mLastHeight = mHeightMode;
+ } else {
+ p.height = mLastHeight = mHeight;
+ }
+
+ if (mWidthMode < 0) {
+ p.width = mLastWidth = mWidthMode;
+ } else {
+ p.width = mLastWidth = mWidth;
+ }
+
+ p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
+ | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
+
+ // Used for debugging.
+ p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));
+
+ return p;
+ }
+
+ private int computeFlags(int curFlags) {
+ curFlags &= ~(
+ WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
+ WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
+ if(mIgnoreCheekPress) {
+ curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+ }
+ if (!mFocusable) {
+ curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ if (mInputMethodMode == INPUT_METHOD_NEEDED) {
+ curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ }
+ } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) {
+ curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ }
+ if (!mTouchable) {
+ curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ }
+ if (mOutsideTouchable) {
+ curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ }
+ if (!mClippingEnabled || mClipToScreen) {
+ curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+ }
+ if (isSplitTouchEnabled()) {
+ curFlags |= WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+ }
+ if (mLayoutInScreen) {
+ curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ }
+ if (mLayoutInsetDecor) {
+ curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ }
+ if (mNotTouchModal) {
+ curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ }
+ if (mAttachedInDecor) {
+ curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR;
+ }
+ return curFlags;
+ }
+
+ private int computeAnimationResource() {
+ if (mAnimationStyle == ANIMATION_STYLE_DEFAULT) {
+ if (mIsDropdown) {
+ return mAboveAnchor
+ ? com.android.internal.R.style.Animation_DropDownUp
+ : com.android.internal.R.style.Animation_DropDownDown;
+ }
+ return 0;
+ }
+ return mAnimationStyle;
+ }
+
+ /**
+ * 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>
+ * The results of positioning are placed in {@code outParams}.
+ *
+ * @param anchor the view on which the popup window must be anchored
+ * @param outParams the layout parameters used to display the drop down
+ * @param xOffset absolute horizontal offset from the left of the anchor
+ * @param yOffset absolute vertical offset from the top of the anchor
+ * @param gravity horizontal gravity specifying popup alignment
+ * @param allowScroll whether the anchor view's parent may be scrolled
+ * when the popup window doesn't fit on screen
+ * @return true if the popup is translated upwards to fit on screen
+ *
+ * @hide
+ */
+ protected final boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams,
+ int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) {
+ final int anchorHeight = anchor.getHeight();
+ final int anchorWidth = anchor.getWidth();
+ if (mOverlapAnchor) {
+ yOffset -= anchorHeight;
+ }
+
+ // Initially, align to the bottom-left corner of the anchor plus offsets.
+ final int[] appScreenLocation = mTmpAppLocation;
+ final View appRootView = getAppRootView(anchor);
+ appRootView.getLocationOnScreen(appScreenLocation);
+
+ final int[] screenLocation = mTmpScreenLocation;
+ anchor.getLocationOnScreen(screenLocation);
+
+ final int[] drawingLocation = mTmpDrawingLocation;
+ drawingLocation[0] = screenLocation[0] - appScreenLocation[0];
+ drawingLocation[1] = screenLocation[1] - appScreenLocation[1];
+ outParams.x = drawingLocation[0] + xOffset;
+ outParams.y = drawingLocation[1] + anchorHeight + yOffset;
+
+ final Rect displayFrame = new Rect();
+ appRootView.getWindowVisibleDisplayFrame(displayFrame);
+ if (width == MATCH_PARENT) {
+ width = displayFrame.right - displayFrame.left;
+ }
+ if (height == MATCH_PARENT) {
+ height = displayFrame.bottom - displayFrame.top;
+ }
+
+ // Let the window manager know to align the top to y.
+ outParams.gravity = computeGravity();
+ outParams.width = width;
+ outParams.height = height;
+
+ // If we need to adjust for gravity RIGHT, align to the bottom-right
+ // corner of the anchor (still accounting for offsets).
+ final int hgrav = Gravity.getAbsoluteGravity(gravity, anchor.getLayoutDirection())
+ & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (hgrav == Gravity.RIGHT) {
+ outParams.x -= width - anchorWidth;
+ }
+
+ // First, attempt to fit the popup vertically without resizing.
+ final boolean fitsVertical = tryFitVertical(outParams, yOffset, height,
+ anchorHeight, drawingLocation[1], screenLocation[1], displayFrame.top,
+ displayFrame.bottom, false);
+
+ // Next, attempt to fit the popup horizontally without resizing.
+ final boolean fitsHorizontal = tryFitHorizontal(outParams, xOffset, width,
+ anchorWidth, drawingLocation[0], screenLocation[0], displayFrame.left,
+ displayFrame.right, false);
+
+ // If the popup still doesn't fit, attempt to scroll the parent.
+ if (!fitsVertical || !fitsHorizontal) {
+ final int scrollX = anchor.getScrollX();
+ final int scrollY = anchor.getScrollY();
+ final Rect r = new Rect(scrollX, scrollY, scrollX + width + xOffset,
+ scrollY + height + anchorHeight + yOffset);
+ if (allowScroll && anchor.requestRectangleOnScreen(r, true)) {
+ // Reset for the new anchor position.
+ anchor.getLocationOnScreen(screenLocation);
+ drawingLocation[0] = screenLocation[0] - appScreenLocation[0];
+ drawingLocation[1] = screenLocation[1] - appScreenLocation[1];
+ outParams.x = drawingLocation[0] + xOffset;
+ outParams.y = drawingLocation[1] + anchorHeight + yOffset;
+
+ // Preserve the gravity adjustment.
+ if (hgrav == Gravity.RIGHT) {
+ outParams.x -= width - anchorWidth;
+ }
+ }
+
+ // Try to fit the popup again and allowing resizing.
+ tryFitVertical(outParams, yOffset, height, anchorHeight, drawingLocation[1],
+ screenLocation[1], displayFrame.top, displayFrame.bottom, mClipToScreen);
+ tryFitHorizontal(outParams, xOffset, width, anchorWidth, drawingLocation[0],
+ screenLocation[0], displayFrame.left, displayFrame.right, mClipToScreen);
+ }
+
+ // Return whether the popup's top edge is above the anchor's top edge.
+ return outParams.y < drawingLocation[1];
+ }
+
+ private boolean tryFitVertical(@NonNull LayoutParams outParams, int yOffset, int height,
+ int anchorHeight, int drawingLocationY, int screenLocationY, int displayFrameTop,
+ int displayFrameBottom, boolean allowResize) {
+ final int winOffsetY = screenLocationY - drawingLocationY;
+ final int anchorTopInScreen = outParams.y + winOffsetY;
+ final int spaceBelow = displayFrameBottom - anchorTopInScreen;
+ if (anchorTopInScreen >= 0 && height <= spaceBelow) {
+ return true;
+ }
+
+ final int spaceAbove = anchorTopInScreen - anchorHeight - displayFrameTop;
+ if (height <= spaceAbove) {
+ // Move everything up.
+ if (mOverlapAnchor) {
+ yOffset += anchorHeight;
+ }
+ outParams.y = drawingLocationY - height + yOffset;
+
+ return true;
+ }
+
+ if (positionInDisplayVertical(outParams, height, drawingLocationY, screenLocationY,
+ displayFrameTop, displayFrameBottom, allowResize)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean positionInDisplayVertical(@NonNull LayoutParams outParams, int height,
+ int drawingLocationY, int screenLocationY, int displayFrameTop, int displayFrameBottom,
+ boolean canResize) {
+ boolean fitsInDisplay = true;
+
+ final int winOffsetY = screenLocationY - drawingLocationY;
+ outParams.y += winOffsetY;
+ outParams.height = height;
+
+ final int bottom = outParams.y + height;
+ if (bottom > displayFrameBottom) {
+ // The popup is too far down, move it back in.
+ outParams.y -= bottom - displayFrameBottom;
+ }
+
+ if (outParams.y < displayFrameTop) {
+ // The popup is too far up, move it back in and clip if
+ // it's still too large.
+ outParams.y = displayFrameTop;
+
+ final int displayFrameHeight = displayFrameBottom - displayFrameTop;
+ if (canResize && height > displayFrameHeight) {
+ outParams.height = displayFrameHeight;
+ } else {
+ fitsInDisplay = false;
+ }
+ }
+
+ outParams.y -= winOffsetY;
+
+ return fitsInDisplay;
+ }
+
+ private boolean tryFitHorizontal(@NonNull LayoutParams outParams, int xOffset, int width,
+ int anchorWidth, int drawingLocationX, int screenLocationX, int displayFrameLeft,
+ int displayFrameRight, boolean allowResize) {
+ final int winOffsetX = screenLocationX - drawingLocationX;
+ final int anchorLeftInScreen = outParams.x + winOffsetX;
+ final int spaceRight = displayFrameRight - anchorLeftInScreen;
+ if (anchorLeftInScreen >= 0 && width <= spaceRight) {
+ return true;
+ }
+
+ if (positionInDisplayHorizontal(outParams, width, drawingLocationX, screenLocationX,
+ displayFrameLeft, displayFrameRight, allowResize)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean positionInDisplayHorizontal(@NonNull LayoutParams outParams, int width,
+ int drawingLocationX, int screenLocationX, int displayFrameLeft, int displayFrameRight,
+ boolean canResize) {
+ boolean fitsInDisplay = true;
+
+ // Use screen coordinates for comparison against display frame.
+ final int winOffsetX = screenLocationX - drawingLocationX;
+ outParams.x += winOffsetX;
+
+ final int right = outParams.x + width;
+ if (right > displayFrameRight) {
+ // The popup is too far right, move it back in.
+ outParams.x -= right - displayFrameRight;
+ }
+
+ if (outParams.x < displayFrameLeft) {
+ // The popup is too far left, move it back in and clip if it's
+ // still too large.
+ outParams.x = displayFrameLeft;
+
+ final int displayFrameWidth = displayFrameRight - displayFrameLeft;
+ if (canResize && width > displayFrameWidth) {
+ outParams.width = displayFrameWidth;
+ } else {
+ fitsInDisplay = false;
+ }
+ }
+
+ outParams.x -= winOffsetX;
+
+ return fitsInDisplay;
+ }
+
+ /**
+ * 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(@NonNull View anchor) {
+ return getMaxAvailableHeight(anchor, 0);
+ }
+
+ /**
+ * 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.
+ * @param yOffset y offset from the view's bottom edge
+ * @return The maximum available height for the popup to be completely
+ * shown.
+ */
+ public int getMaxAvailableHeight(@NonNull View anchor, int yOffset) {
+ return getMaxAvailableHeight(anchor, yOffset, false);
+ }
+
+ /**
+ * Returns the maximum height that is available for the popup to be
+ * completely shown, optionally ignoring any bottom decorations such as
+ * the input method. 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.
+ * @param yOffset y offset from the view's bottom edge
+ * @param ignoreBottomDecorations if true, the height returned will be
+ * all the way to the bottom of the display, ignoring any
+ * bottom decorations
+ * @return The maximum available height for the popup to be completely
+ * shown.
+ */
+ public int getMaxAvailableHeight(
+ @NonNull View anchor, int yOffset, boolean ignoreBottomDecorations) {
+ Rect displayFrame = null;
+ final Rect visibleDisplayFrame = new Rect();
+
+ final View appView = getAppRootView(anchor);
+ appView.getWindowVisibleDisplayFrame(visibleDisplayFrame);
+ if (ignoreBottomDecorations) {
+ // In the ignore bottom decorations case we want to
+ // still respect all other decorations so we use the inset visible
+ // frame on the top right and left and take the bottom
+ // value from the full frame.
+ displayFrame = new Rect();
+ anchor.getWindowDisplayFrame(displayFrame);
+ displayFrame.top = visibleDisplayFrame.top;
+ displayFrame.right = visibleDisplayFrame.right;
+ displayFrame.left = visibleDisplayFrame.left;
+ } else {
+ displayFrame = visibleDisplayFrame;
+ }
+
+ final int[] anchorPos = mTmpDrawingLocation;
+ anchor.getLocationOnScreen(anchorPos);
+
+ final int bottomEdge = displayFrame.bottom;
+
+ final int distanceToBottom;
+ if (mOverlapAnchor) {
+ distanceToBottom = bottomEdge - anchorPos[1] - yOffset;
+ } else {
+ distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset;
+ }
+ final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset;
+
+ // anchorPos[1] is distance from anchor to top of screen
+ int returnedHeight = Math.max(distanceToBottom, distanceToTop);
+ if (mBackground != null) {
+ mBackground.getPadding(mTempRect);
+ returnedHeight -= mTempRect.top + mTempRect.bottom;
+ }
+
+ return returnedHeight;
+ }
+
+ /**
+ * Disposes 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.
+ *
+ * @see #showAsDropDown(android.view.View)
+ */
+ public void dismiss() {
+ if (!isShowing() || isTransitioningToDismiss()) {
+ return;
+ }
+
+ final PopupDecorView decorView = mDecorView;
+ final View contentView = mContentView;
+
+ final ViewGroup contentHolder;
+ final ViewParent contentParent = contentView.getParent();
+ if (contentParent instanceof ViewGroup) {
+ contentHolder = ((ViewGroup) contentParent);
+ } else {
+ contentHolder = null;
+ }
+
+ // Ensure any ongoing or pending transitions are canceled.
+ decorView.cancelTransitions();
+
+ mIsShowing = false;
+ mIsTransitioningToDismiss = true;
+
+ // This method may be called as part of window detachment, in which
+ // case the anchor view (and its root) will still return true from
+ // isAttachedToWindow() during execution of this method; however, we
+ // can expect the OnAttachStateChangeListener to have been called prior
+ // to executing this method, so we can rely on that instead.
+ final Transition exitTransition = mExitTransition;
+ if (exitTransition != null && decorView.isLaidOut()
+ && (mIsAnchorRootAttached || mAnchorRoot == null)) {
+ // The decor view is non-interactive and non-IME-focusable during exit transitions.
+ final LayoutParams p = (LayoutParams) decorView.getLayoutParams();
+ p.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
+ p.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+ p.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ mWindowManager.updateViewLayout(decorView, p);
+
+ final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
+ final Rect epicenter = getTransitionEpicenter();
+
+ // Once we start dismissing the decor view, all state (including
+ // the anchor root) needs to be moved to the decor view since we
+ // may open another popup while it's busy exiting.
+ decorView.startExitTransition(exitTransition, anchorRoot, epicenter,
+ new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ dismissImmediate(decorView, contentHolder, contentView);
+ }
+ });
+ } else {
+ dismissImmediate(decorView, contentHolder, contentView);
+ }
+
+ // Clears the anchor view.
+ detachFromAnchor();
+
+ if (mOnDismissListener != null) {
+ mOnDismissListener.onDismiss();
+ }
+ }
+
+ /**
+ * Returns the window-relative epicenter bounds to be used by enter and
+ * exit transitions.
+ * <p>
+ * <strong>Note:</strong> This is distinct from the rect passed to
+ * {@link #setEpicenterBounds(Rect)}, which is anchor-relative.
+ *
+ * @return the window-relative epicenter bounds to be used by enter and
+ * exit transitions
+ *
+ * @hide
+ */
+ protected final Rect getTransitionEpicenter() {
+ final View anchor = mAnchor != null ? mAnchor.get() : null;
+ final View decor = mDecorView;
+ if (anchor == null || decor == null) {
+ return null;
+ }
+
+ final int[] anchorLocation = anchor.getLocationOnScreen();
+ final int[] popupLocation = mDecorView.getLocationOnScreen();
+
+ // Compute the position of the anchor relative to the popup.
+ final Rect bounds = new Rect(0, 0, anchor.getWidth(), anchor.getHeight());
+ bounds.offset(anchorLocation[0] - popupLocation[0], anchorLocation[1] - popupLocation[1]);
+
+ // Use anchor-relative epicenter, if specified.
+ if (mEpicenterBounds != null) {
+ final int offsetX = bounds.left;
+ final int offsetY = bounds.top;
+ bounds.set(mEpicenterBounds);
+ bounds.offset(offsetX, offsetY);
+ }
+
+ return bounds;
+ }
+
+ /**
+ * Removes the popup from the window manager and tears down the supporting
+ * view hierarchy, if necessary.
+ */
+ private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) {
+ // If this method gets called and the decor view doesn't have a parent,
+ // then it was either never added or was already removed. That should
+ // never happen, but it's worth checking to avoid potential crashes.
+ if (decorView.getParent() != null) {
+ mWindowManager.removeViewImmediate(decorView);
+ }
+
+ if (contentHolder != null) {
+ contentHolder.removeView(contentView);
+ }
+
+ // This needs to stay until after all transitions have ended since we
+ // need the reference to cancel transitions in preparePopup().
+ mDecorView = null;
+ mBackgroundView = null;
+ mIsTransitioningToDismiss = false;
+ }
+
+ /**
+ * Sets the listener to be called when the window is dismissed.
+ *
+ * @param onDismissListener The listener.
+ */
+ public void setOnDismissListener(OnDismissListener onDismissListener) {
+ mOnDismissListener = onDismissListener;
+ }
+
+ /** @hide */
+ protected final OnDismissListener getOnDismissListener() {
+ return mOnDismissListener;
+ }
+
+ /**
+ * Updates the state of the popup window, if it is currently being displayed,
+ * from the currently set state.
+ * <p>
+ * This includes:
+ * <ul>
+ * <li>{@link #setClippingEnabled(boolean)}</li>
+ * <li>{@link #setFocusable(boolean)}</li>
+ * <li>{@link #setIgnoreCheekPress()}</li>
+ * <li>{@link #setInputMethodMode(int)}</li>
+ * <li>{@link #setTouchable(boolean)}</li>
+ * <li>{@link #setAnimationStyle(int)}</li>
+ * </ul>
+ */
+ public void update() {
+ if (!isShowing() || !hasContentView()) {
+ return;
+ }
+
+ final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+ boolean update = false;
+
+ final int newAnim = computeAnimationResource();
+ if (newAnim != p.windowAnimations) {
+ p.windowAnimations = newAnim;
+ update = true;
+ }
+
+ final int newFlags = computeFlags(p.flags);
+ if (newFlags != p.flags) {
+ p.flags = newFlags;
+ update = true;
+ }
+
+ final int newGravity = computeGravity();
+ if (newGravity != p.gravity) {
+ p.gravity = newGravity;
+ update = true;
+ }
+
+ if (update) {
+ update(mAnchor != null ? mAnchor.get() : null, p);
+ }
+ }
+
+ /** @hide */
+ protected void update(View anchor, WindowManager.LayoutParams params) {
+ setLayoutDirectionFromAnchor();
+ mWindowManager.updateViewLayout(mDecorView, params);
+ }
+
+ /**
+ * Updates the dimension of the popup window.
+ * <p>
+ * Calling this function also updates the window with the current popup
+ * state as described for {@link #update()}.
+ *
+ * @param width the new width in pixels, must be >= 0 or -1 to ignore
+ * @param height the new height in pixels, must be >= 0 or -1 to ignore
+ */
+ public void update(int width, int height) {
+ final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+ update(p.x, p.y, width, height, false);
+ }
+
+ /**
+ * Updates the position and the dimension of the popup window.
+ * <p>
+ * Width and height can be set to -1 to update location only. Calling this
+ * function also updates the window with the current popup state as
+ * described for {@link #update()}.
+ *
+ * @param x the new x location
+ * @param y the new y location
+ * @param width the new width in pixels, must be >= 0 or -1 to ignore
+ * @param height the new height in pixels, must be >= 0 or -1 to ignore
+ */
+ public void update(int x, int y, int width, int height) {
+ update(x, y, width, height, false);
+ }
+
+ /**
+ * Updates the position and the dimension of the popup window.
+ * <p>
+ * Width and height can be set to -1 to update location only. Calling this
+ * function also updates the window with the current popup state as
+ * described for {@link #update()}.
+ *
+ * @param x the new x location
+ * @param y the new y location
+ * @param width the new width in pixels, must be >= 0 or -1 to ignore
+ * @param height the new height in pixels, must be >= 0 or -1 to ignore
+ * @param force {@code true} to reposition the window even if the specified
+ * position already seems to correspond to the LayoutParams,
+ * {@code false} to only reposition if needed
+ */
+ public void update(int x, int y, int width, int height, boolean force) {
+ if (width >= 0) {
+ mLastWidth = width;
+ setWidth(width);
+ }
+
+ if (height >= 0) {
+ mLastHeight = height;
+ setHeight(height);
+ }
+
+ if (!isShowing() || !hasContentView()) {
+ return;
+ }
+
+ final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+ boolean update = force;
+
+ final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
+ if (width != -1 && p.width != finalWidth) {
+ p.width = mLastWidth = finalWidth;
+ update = true;
+ }
+
+ final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
+ if (height != -1 && p.height != finalHeight) {
+ p.height = mLastHeight = finalHeight;
+ update = true;
+ }
+
+ if (p.x != x) {
+ p.x = x;
+ update = true;
+ }
+
+ if (p.y != y) {
+ p.y = y;
+ update = true;
+ }
+
+ final int newAnim = computeAnimationResource();
+ if (newAnim != p.windowAnimations) {
+ p.windowAnimations = newAnim;
+ update = true;
+ }
+
+ final int newFlags = computeFlags(p.flags);
+ if (newFlags != p.flags) {
+ p.flags = newFlags;
+ update = true;
+ }
+
+ final int newGravity = computeGravity();
+ if (newGravity != p.gravity) {
+ p.gravity = newGravity;
+ update = true;
+ }
+
+ View anchor = null;
+ int newAccessibilityIdOfAnchor = -1;
+
+ if (mAnchor != null && mAnchor.get() != null) {
+ anchor = mAnchor.get();
+ newAccessibilityIdOfAnchor = anchor.getAccessibilityViewId();
+ }
+
+ if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) {
+ p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor;
+ update = true;
+ }
+
+ if (update) {
+ update(anchor, p);
+ }
+ }
+
+ /** @hide */
+ protected boolean hasContentView() {
+ return mContentView != null;
+ }
+
+ /** @hide */
+ protected boolean hasDecorView() {
+ return mDecorView != null;
+ }
+
+ /** @hide */
+ protected WindowManager.LayoutParams getDecorViewLayoutParams() {
+ return (WindowManager.LayoutParams) mDecorView.getLayoutParams();
+ }
+
+ /**
+ * Updates the position and the dimension of the popup window.
+ * <p>
+ * Calling this function also updates the window with the current popup
+ * state as described for {@link #update()}.
+ *
+ * @param anchor the popup's anchor view
+ * @param width the new width in pixels, must be >= 0 or -1 to ignore
+ * @param height the new height in pixels, must be >= 0 or -1 to ignore
+ */
+ public void update(View anchor, int width, int height) {
+ update(anchor, false, 0, 0, width, height);
+ }
+
+ /**
+ * Updates the position and the dimension of the popup window.
+ * <p>
+ * Width and height can be set to -1 to update location only. Calling this
+ * function also updates the window with the current popup state as
+ * described for {@link #update()}.
+ * <p>
+ * If the view later scrolls to move {@code anchor} to a different
+ * location, the popup will be moved correspondingly.
+ *
+ * @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 in pixels, must be >= 0 or -1 to ignore
+ * @param height the new height in pixels, must be >= 0 or -1 to ignore
+ */
+ public void update(View anchor, int xoff, int yoff, int width, int height) {
+ update(anchor, true, xoff, yoff, width, height);
+ }
+
+ private void update(View anchor, boolean updateLocation, int xoff, int yoff,
+ int width, int height) {
+
+ if (!isShowing() || !hasContentView()) {
+ return;
+ }
+
+ final WeakReference<View> oldAnchor = mAnchor;
+ final int gravity = mAnchoredGravity;
+
+ final boolean needsUpdate = updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff);
+ if (oldAnchor == null || oldAnchor.get() != anchor || (needsUpdate && !mIsDropdown)) {
+ attachToAnchor(anchor, xoff, yoff, gravity);
+ } else if (needsUpdate) {
+ // No need to register again if this is a DropDown, showAsDropDown already did.
+ mAnchorXoff = xoff;
+ mAnchorYoff = yoff;
+ }
+
+ final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+ final int oldGravity = p.gravity;
+ final int oldWidth = p.width;
+ final int oldHeight = p.height;
+ final int oldX = p.x;
+ final int oldY = p.y;
+
+ // If an explicit width/height has not specified, use the most recent
+ // explicitly specified value (either from setWidth/Height or update).
+ if (width < 0) {
+ width = mWidth;
+ }
+ if (height < 0) {
+ height = mHeight;
+ }
+
+ final boolean aboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
+ width, height, gravity, mAllowScrollingAnchorParent);
+ updateAboveAnchor(aboveAnchor);
+
+ final boolean paramsChanged = oldGravity != p.gravity || oldX != p.x || oldY != p.y
+ || oldWidth != p.width || oldHeight != p.height;
+
+ // If width and mWidth were both < 0 then we have a MATCH_PARENT or
+ // WRAP_CONTENT case. findDropDownPosition will have resolved this to
+ // absolute values, but we don't want to update mWidth/mHeight to these
+ // absolute values.
+ final int newWidth = width < 0 ? width : p.width;
+ final int newHeight = height < 0 ? height : p.height;
+ update(p.x, p.y, newWidth, newHeight, paramsChanged);
+ }
+
+ /**
+ * Listener that is called when this popup window is dismissed.
+ */
+ public interface OnDismissListener {
+ /**
+ * Called when this popup window is dismissed.
+ */
+ public void onDismiss();
+ }
+
+ /** @hide */
+ protected final void detachFromAnchor() {
+ final View anchor = mAnchor != null ? mAnchor.get() : null;
+ if (anchor != null) {
+ final ViewTreeObserver vto = anchor.getViewTreeObserver();
+ vto.removeOnScrollChangedListener(mOnScrollChangedListener);
+ anchor.removeOnAttachStateChangeListener(mOnAnchorDetachedListener);
+ }
+
+ final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
+ if (anchorRoot != null) {
+ anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+ anchorRoot.removeOnLayoutChangeListener(mOnLayoutChangeListener);
+ }
+
+ mAnchor = null;
+ mAnchorRoot = null;
+ mIsAnchorRootAttached = false;
+ }
+
+ /** @hide */
+ protected final void attachToAnchor(View anchor, int xoff, int yoff, int gravity) {
+ detachFromAnchor();
+
+ final ViewTreeObserver vto = anchor.getViewTreeObserver();
+ if (vto != null) {
+ vto.addOnScrollChangedListener(mOnScrollChangedListener);
+ }
+ anchor.addOnAttachStateChangeListener(mOnAnchorDetachedListener);
+
+ final View anchorRoot = anchor.getRootView();
+ anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+ anchorRoot.addOnLayoutChangeListener(mOnLayoutChangeListener);
+
+ mAnchor = new WeakReference<>(anchor);
+ mAnchorRoot = new WeakReference<>(anchorRoot);
+ mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();
+ mParentRootView = mAnchorRoot;
+
+ mAnchorXoff = xoff;
+ mAnchorYoff = yoff;
+ mAnchoredGravity = gravity;
+ }
+
+ private void alignToAnchor() {
+ final View anchor = mAnchor != null ? mAnchor.get() : null;
+ if (anchor != null && anchor.isAttachedToWindow() && hasDecorView()) {
+ final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+ updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
+ p.width, p.height, mAnchoredGravity, false));
+ update(p.x, p.y, -1, -1, true);
+ }
+ }
+
+ private View getAppRootView(View anchor) {
+ final View appWindowView = WindowManagerGlobal.getInstance().getWindowView(
+ anchor.getApplicationWindowToken());
+ if (appWindowView != null) {
+ return appWindowView;
+ }
+ return anchor.getRootView();
+ }
+
+ private class PopupDecorView extends FrameLayout {
+ /** Runnable used to clean up listeners after exit transition. */
+ private Runnable mCleanupAfterExit;
+
+ public PopupDecorView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ if (getKeyDispatcherState() == null) {
+ return super.dispatchKeyEvent(event);
+ }
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ final KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ final KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null && state.isTracking(event) && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ return super.dispatchKeyEvent(event);
+ } else {
+ return super.dispatchKeyEvent(event);
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
+ return true;
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @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 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ dismiss();
+ return true;
+ } else {
+ return super.onTouchEvent(event);
+ }
+ }
+
+ /**
+ * Requests that an enter transition run after the next layout pass.
+ */
+ public void requestEnterTransition(Transition transition) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null && transition != null) {
+ final Transition enterTransition = transition.clone();
+
+ // Postpone the enter transition after the first layout pass.
+ observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null) {
+ observer.removeOnGlobalLayoutListener(this);
+ }
+
+ final Rect epicenter = getTransitionEpicenter();
+ enterTransition.setEpicenterCallback(new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+ startEnterTransition(enterTransition);
+ }
+ });
+ }
+ }
+
+ /**
+ * Starts the pending enter transition, if one is set.
+ */
+ private void startEnterTransition(Transition enterTransition) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ enterTransition.addTarget(child);
+ child.setVisibility(View.INVISIBLE);
+ }
+
+ TransitionManager.beginDelayedTransition(this, enterTransition);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Starts an exit transition immediately.
+ * <p>
+ * <strong>Note:</strong> The transition listener is guaranteed to have
+ * its {@code onTransitionEnd} method called even if the transition
+ * never starts.
+ */
+ public void startExitTransition(@NonNull Transition transition,
+ @Nullable final View anchorRoot, @Nullable final Rect epicenter,
+ @NonNull final TransitionListener listener) {
+ if (transition == null) {
+ return;
+ }
+
+ // The anchor view's window may go away while we're executing our
+ // transition, in which case we need to end the transition
+ // immediately and execute the listener to remove the popup.
+ if (anchorRoot != null) {
+ anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+ }
+
+ // The cleanup runnable MUST be called even if the transition is
+ // canceled before it starts (and thus can't call onTransitionEnd).
+ mCleanupAfterExit = () -> {
+ listener.onTransitionEnd(transition);
+
+ if (anchorRoot != null) {
+ anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+ }
+
+ // The listener was called. Our job here is done.
+ mCleanupAfterExit = null;
+ };
+
+ final Transition exitTransition = transition.clone();
+ exitTransition.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition t) {
+ t.removeListener(this);
+
+ // This null check shouldn't be necessary, but it's easier
+ // to check here than it is to test every possible case.
+ if (mCleanupAfterExit != null) {
+ mCleanupAfterExit.run();
+ }
+ }
+ });
+ exitTransition.setEpicenterCallback(new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ exitTransition.addTarget(child);
+ }
+
+ TransitionManager.beginDelayedTransition(this, exitTransition);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Cancels all pending or current transitions.
+ */
+ public void cancelTransitions() {
+ TransitionManager.endTransitions(this);
+
+ // If the cleanup runnable is still around, that means the
+ // transition never started. We should run it now to clean up.
+ if (mCleanupAfterExit != null) {
+ mCleanupAfterExit.run();
+ }
+ }
+
+ private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {}
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ v.removeOnAttachStateChangeListener(this);
+
+ TransitionManager.endTransitions(PopupDecorView.this);
+ }
+ };
+
+ @Override
+ public void requestKeyboardShortcuts(List<KeyboardShortcutGroup> list, int deviceId) {
+ if (mParentRootView != null) {
+ View parentRoot = mParentRootView.get();
+ if (parentRoot != null) {
+ parentRoot.requestKeyboardShortcuts(list, deviceId);
+ }
+ }
+ }
+ }
+
+ private class PopupBackgroundView extends FrameLayout {
+ public PopupBackgroundView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mAboveAnchor) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
+ return drawableState;
+ } else {
+ return super.onCreateDrawableState(extraSpace);
+ }
+ }
+ }
+}
diff --git a/android/widget/ProgressBar.java b/android/widget/ProgressBar.java
new file mode 100644
index 0000000..ced66cd
--- /dev/null
+++ b/android/widget/ProgressBar.java
@@ -0,0 +1,2086 @@
+/*
+ * 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.animation.ObjectAnimator;
+import android.annotation.InterpolatorRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Animatable;
+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.StateListDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.util.Pools.SynchronizedPool;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * <p>
+ * A user interface element that indicates the progress of an operation.
+ * Progress bar supports two modes to represent progress: determinate, and indeterminate. For
+ * a visual overview of the difference between determinate and indeterminate progress modes, see
+ * <a href="https://material.io/guidelines/components/progress-activity.html#progress-activity-types-of-indicators">
+ * Progress & activity</a>.
+ * Display progress bars to a user in a non-interruptive way.
+ * Show the progress bar in your app's user interface or in a notification
+ * instead of within a dialog.
+ * </p>
+ * <h3>Indeterminate Progress</h3>
+ * <p>
+ * Use indeterminate mode for the progress bar when you do not know how long an
+ * operation will take.
+ * Indeterminate mode is the default for progress bar and shows a cyclic animation without a
+ * specific amount of progress indicated.
+ * The following example shows an indeterminate progress bar:
+ * <pre>
+ * <ProgressBar
+ * android:id="@+id/indeterminateBar"
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * />
+ * </pre>
+ * </p>
+ * <h3>Determinate Progress</h3>
+ * <p>
+ * Use determinate mode for the progress bar when you want to show that a specific quantity of
+ * progress has occurred.
+ * For example, the percent remaining of a file being retrieved, the amount records in
+ * a batch written to database, or the percent remaining of an audio file that is playing.
+ * <p>
+ * <p>
+ * To indicate determinate progress, you set the style of the progress bar to
+ * {@link android.R.style#Widget_ProgressBar_Horizontal} and set the amount of progress.
+ * The following example shows a determinate progress bar that is 25% complete:
+ * <pre>
+ * <ProgressBar
+ * android:id="@+id/determinateBar"
+ * style="@android:style/Widget.ProgressBar.Horizontal"
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:progress="25"/>
+ * </pre>
+ * You can update the percentage of progress displayed by using the
+ * {@link #setProgress(int)} method, or by calling
+ * {@link #incrementProgressBy(int)} to increase the current progress completed
+ * by a specified amount.
+ * By default, the progress bar is full when the progress value reaches 100.
+ * You can adjust this default by setting the
+ * {@link android.R.styleable#ProgressBar_max android:max} attribute.
+ * </p>
+ * <p>Other progress bar styles provided by the system include:</p>
+ * <ul>
+ * <li>{@link android.R.style#Widget_ProgressBar_Horizontal Widget.ProgressBar.Horizontal}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small Widget.ProgressBar.Small}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large Widget.ProgressBar.Large}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Inverse Widget.ProgressBar.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small_Inverse
+ * Widget.ProgressBar.Small.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large_Inverse
+ * Widget.ProgressBar.Large.Inverse}</li>
+ * </ul>
+ * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary
+ * if your application uses a light colored theme (a white background).</p>
+ *
+ * <p><strong>XML attributes</b></strong>
+ * <p>
+ * See {@link android.R.styleable#ProgressBar ProgressBar Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ *
+ * @attr ref android.R.styleable#ProgressBar_animationResolution
+ * @attr ref android.R.styleable#ProgressBar_indeterminate
+ * @attr ref android.R.styleable#ProgressBar_indeterminateBehavior
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDrawable
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDuration
+ * @attr ref android.R.styleable#ProgressBar_indeterminateOnly
+ * @attr ref android.R.styleable#ProgressBar_interpolator
+ * @attr ref android.R.styleable#ProgressBar_min
+ * @attr ref android.R.styleable#ProgressBar_max
+ * @attr ref android.R.styleable#ProgressBar_maxHeight
+ * @attr ref android.R.styleable#ProgressBar_maxWidth
+ * @attr ref android.R.styleable#ProgressBar_minHeight
+ * @attr ref android.R.styleable#ProgressBar_minWidth
+ * @attr ref android.R.styleable#ProgressBar_mirrorForRtl
+ * @attr ref android.R.styleable#ProgressBar_progress
+ * @attr ref android.R.styleable#ProgressBar_progressDrawable
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgress
+ */
+@RemoteView
+public class ProgressBar extends View {
+
+ private static final int MAX_LEVEL = 10000;
+ private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
+
+ /** Interpolator used for smooth progress animations. */
+ private static final DecelerateInterpolator PROGRESS_ANIM_INTERPOLATOR =
+ new DecelerateInterpolator();
+
+ /** Duration of smooth progress animations. */
+ private static final int PROGRESS_ANIM_DURATION = 80;
+
+ int mMinWidth;
+ int mMaxWidth;
+ int mMinHeight;
+ int mMaxHeight;
+
+ private int mProgress;
+ private int mSecondaryProgress;
+ private int mMin;
+ private boolean mMinInitialized;
+ private int mMax;
+ private boolean mMaxInitialized;
+
+ private int mBehavior;
+ private int mDuration;
+ private boolean mIndeterminate;
+ private boolean mOnlyIndeterminate;
+ private Transformation mTransformation;
+ private AlphaAnimation mAnimation;
+ private boolean mHasAnimation;
+
+ private Drawable mIndeterminateDrawable;
+ private Drawable mProgressDrawable;
+ private Drawable mCurrentDrawable;
+ private ProgressTintInfo mProgressTintInfo;
+
+ int mSampleWidth = 0;
+ private boolean mNoInvalidate;
+ private Interpolator mInterpolator;
+ private RefreshProgressRunnable mRefreshProgressRunnable;
+ private long mUiThreadId;
+ private boolean mShouldStartAnimationDrawable;
+
+ private boolean mInDrawing;
+ private boolean mAttached;
+ private boolean mRefreshIsPosted;
+
+ /** Value used to track progress animation, in the range [0...1]. */
+ private float mVisualProgress;
+
+ boolean mMirrorForRtl = false;
+
+ private boolean mAggregatedIsVisible;
+
+ private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>();
+
+ private AccessibilityEventSender mAccessibilityEventSender;
+
+ /**
+ * 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mUiThreadId = Thread.currentThread().getId();
+ initProgressBar();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes);
+
+ mNoInvalidate = true;
+
+ final Drawable progressDrawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable);
+ if (progressDrawable != null) {
+ // Calling setProgressDrawable can set mMaxHeight, so make sure the
+ // corresponding XML attribute for mMaxHeight is read after calling
+ // this method.
+ if (needsTileify(progressDrawable)) {
+ setProgressDrawableTiled(progressDrawable);
+ } else {
+ setProgressDrawable(progressDrawable);
+ }
+ }
+
+
+ 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,
+ android.R.anim.linear_interpolator); // default to linear interpolator
+ if (resID > 0) {
+ setInterpolator(context, resID);
+ }
+
+ setMin(a.getInt(R.styleable.ProgressBar_min, mMin));
+ setMax(a.getInt(R.styleable.ProgressBar_max, mMax));
+
+ setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress));
+
+ setSecondaryProgress(a.getInt(
+ R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress));
+
+ final Drawable indeterminateDrawable = a.getDrawable(
+ R.styleable.ProgressBar_indeterminateDrawable);
+ if (indeterminateDrawable != null) {
+ if (needsTileify(indeterminateDrawable)) {
+ setIndeterminateDrawableTiled(indeterminateDrawable);
+ } else {
+ setIndeterminateDrawable(indeterminateDrawable);
+ }
+ }
+
+ mOnlyIndeterminate = a.getBoolean(
+ R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate);
+
+ mNoInvalidate = false;
+
+ setIndeterminate(mOnlyIndeterminate || a.getBoolean(
+ R.styleable.ProgressBar_indeterminate, mIndeterminate));
+
+ mMirrorForRtl = a.getBoolean(R.styleable.ProgressBar_mirrorForRtl, mMirrorForRtl);
+
+ if (a.hasValue(R.styleable.ProgressBar_progressTintMode)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.ProgressBar_progressTintMode, -1), null);
+ mProgressTintInfo.mHasProgressTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_progressTint)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressTintList = a.getColorStateList(
+ R.styleable.ProgressBar_progressTint);
+ mProgressTintInfo.mHasProgressTint = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_progressBackgroundTintMode)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressBackgroundTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.ProgressBar_progressBackgroundTintMode, -1), null);
+ mProgressTintInfo.mHasProgressBackgroundTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_progressBackgroundTint)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressBackgroundTintList = a.getColorStateList(
+ R.styleable.ProgressBar_progressBackgroundTint);
+ mProgressTintInfo.mHasProgressBackgroundTint = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_secondaryProgressTintMode)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mSecondaryProgressTintMode = Drawable.parseTintMode(
+ a.getInt(R.styleable.ProgressBar_secondaryProgressTintMode, -1), null);
+ mProgressTintInfo.mHasSecondaryProgressTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_secondaryProgressTint)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mSecondaryProgressTintList = a.getColorStateList(
+ R.styleable.ProgressBar_secondaryProgressTint);
+ mProgressTintInfo.mHasSecondaryProgressTint = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_indeterminateTintMode)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mIndeterminateTintMode = Drawable.parseTintMode(a.getInt(
+ R.styleable.ProgressBar_indeterminateTintMode, -1), null);
+ mProgressTintInfo.mHasIndeterminateTintMode = true;
+ }
+
+ if (a.hasValue(R.styleable.ProgressBar_indeterminateTint)) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mIndeterminateTintList = a.getColorStateList(
+ R.styleable.ProgressBar_indeterminateTint);
+ mProgressTintInfo.mHasIndeterminateTint = true;
+ }
+
+ a.recycle();
+
+ applyProgressTints();
+ applyIndeterminateTint();
+
+ // If not explicitly specified this view is important for accessibility.
+ if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the target drawable needs to be tileified.
+ *
+ * @param dr the drawable to check
+ * @return {@code true} if the target drawable needs to be tileified,
+ * {@code false} otherwise
+ */
+ private static boolean needsTileify(Drawable dr) {
+ if (dr instanceof LayerDrawable) {
+ final LayerDrawable orig = (LayerDrawable) dr;
+ final int N = orig.getNumberOfLayers();
+ for (int i = 0; i < N; i++) {
+ if (needsTileify(orig.getDrawable(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ if (dr instanceof StateListDrawable) {
+ final StateListDrawable in = (StateListDrawable) dr;
+ final int N = in.getStateCount();
+ for (int i = 0; i < N; i++) {
+ if (needsTileify(in.getStateDrawable(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // If there's a bitmap that's not wrapped with a ClipDrawable or
+ // ScaleDrawable, we'll need to wrap it and apply tiling.
+ if (dr instanceof BitmapDrawable) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Converts a drawable to a tiled version of itself. It will recursively
+ * traverse layer and state list drawables.
+ */
+ private Drawable tileify(Drawable drawable, boolean clip) {
+ // TODO: This is a terrible idea that potentially destroys any drawable
+ // that extends any of these classes. We *really* need to remove this.
+
+ if (drawable instanceof LayerDrawable) {
+ final LayerDrawable orig = (LayerDrawable) drawable;
+ final int N = orig.getNumberOfLayers();
+ final Drawable[] outDrawables = new Drawable[N];
+
+ for (int i = 0; i < N; i++) {
+ final int id = orig.getId(i);
+ outDrawables[i] = tileify(orig.getDrawable(i),
+ (id == R.id.progress || id == R.id.secondaryProgress));
+ }
+
+ final LayerDrawable clone = new LayerDrawable(outDrawables);
+ for (int i = 0; i < N; i++) {
+ clone.setId(i, orig.getId(i));
+ clone.setLayerGravity(i, orig.getLayerGravity(i));
+ clone.setLayerWidth(i, orig.getLayerWidth(i));
+ clone.setLayerHeight(i, orig.getLayerHeight(i));
+ clone.setLayerInsetLeft(i, orig.getLayerInsetLeft(i));
+ clone.setLayerInsetRight(i, orig.getLayerInsetRight(i));
+ clone.setLayerInsetTop(i, orig.getLayerInsetTop(i));
+ clone.setLayerInsetBottom(i, orig.getLayerInsetBottom(i));
+ clone.setLayerInsetStart(i, orig.getLayerInsetStart(i));
+ clone.setLayerInsetEnd(i, orig.getLayerInsetEnd(i));
+ }
+
+ return clone;
+ }
+
+ if (drawable instanceof StateListDrawable) {
+ final StateListDrawable in = (StateListDrawable) drawable;
+ final StateListDrawable out = new StateListDrawable();
+ final int N = in.getStateCount();
+ for (int i = 0; i < N; i++) {
+ out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip));
+ }
+
+ return out;
+ }
+
+ if (drawable instanceof BitmapDrawable) {
+ final Drawable.ConstantState cs = drawable.getConstantState();
+ final BitmapDrawable clone = (BitmapDrawable) cs.newDrawable(getResources());
+ clone.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
+
+ if (mSampleWidth <= 0) {
+ mSampleWidth = clone.getIntrinsicWidth();
+ }
+
+ if (clip) {
+ return new ClipDrawable(clone, Gravity.LEFT, ClipDrawable.HORIZONTAL);
+ } else {
+ return clone;
+ }
+ }
+
+ return drawable;
+ }
+
+ 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 = tileify(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() {
+ mMin = 0;
+ 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
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ 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
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setIndeterminate(boolean indeterminate) {
+ if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) {
+ mIndeterminate = indeterminate;
+
+ if (indeterminate) {
+ // swap between indeterminate and regular backgrounds
+ swapCurrentDrawable(mIndeterminateDrawable);
+ startAnimation();
+ } else {
+ swapCurrentDrawable(mProgressDrawable);
+ stopAnimation();
+ }
+ }
+ }
+
+ private void swapCurrentDrawable(Drawable newDrawable) {
+ final Drawable oldDrawable = mCurrentDrawable;
+ mCurrentDrawable = newDrawable;
+
+ if (oldDrawable != mCurrentDrawable) {
+ if (oldDrawable != null) {
+ oldDrawable.setVisible(false, false);
+ }
+ if (mCurrentDrawable != null) {
+ mCurrentDrawable.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+ }
+ }
+ }
+
+ /**
+ * <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;
+ }
+
+ /**
+ * Define the drawable used to draw the progress bar in indeterminate mode.
+ *
+ * @param d the new drawable
+ * @see #getIndeterminateDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setIndeterminateDrawable(Drawable d) {
+ if (mIndeterminateDrawable != d) {
+ if (mIndeterminateDrawable != null) {
+ mIndeterminateDrawable.setCallback(null);
+ unscheduleDrawable(mIndeterminateDrawable);
+ }
+
+ mIndeterminateDrawable = d;
+
+ if (d != null) {
+ d.setCallback(this);
+ d.setLayoutDirection(getLayoutDirection());
+ if (d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ applyIndeterminateTint();
+ }
+
+ if (mIndeterminate) {
+ swapCurrentDrawable(d);
+ postInvalidate();
+ }
+ }
+ }
+
+ /**
+ * Applies a tint to the indeterminate drawable. Does not modify the
+ * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setIndeterminateDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and
+ * tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#ProgressBar_indeterminateTint
+ * @see #getIndeterminateTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ @RemotableViewMethod
+ public void setIndeterminateTintList(@Nullable ColorStateList tint) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mIndeterminateTintList = tint;
+ mProgressTintInfo.mHasIndeterminateTint = true;
+
+ applyIndeterminateTint();
+ }
+
+ /**
+ * @return the tint applied to the indeterminate drawable
+ * @attr ref android.R.styleable#ProgressBar_indeterminateTint
+ * @see #setIndeterminateTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getIndeterminateTintList() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mIndeterminateTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setIndeterminateTintList(ColorStateList)} to the indeterminate
+ * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#ProgressBar_indeterminateTintMode
+ * @see #setIndeterminateTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setIndeterminateTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mIndeterminateTintMode = tintMode;
+ mProgressTintInfo.mHasIndeterminateTintMode = true;
+
+ applyIndeterminateTint();
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the indeterminate
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the indeterminate
+ * drawable
+ * @attr ref android.R.styleable#ProgressBar_indeterminateTintMode
+ * @see #setIndeterminateTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getIndeterminateTintMode() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mIndeterminateTintMode : null;
+ }
+
+ private void applyIndeterminateTint() {
+ if (mIndeterminateDrawable != null && mProgressTintInfo != null) {
+ final ProgressTintInfo tintInfo = mProgressTintInfo;
+ if (tintInfo.mHasIndeterminateTint || tintInfo.mHasIndeterminateTintMode) {
+ mIndeterminateDrawable = mIndeterminateDrawable.mutate();
+
+ if (tintInfo.mHasIndeterminateTint) {
+ mIndeterminateDrawable.setTintList(tintInfo.mIndeterminateTintList);
+ }
+
+ if (tintInfo.mHasIndeterminateTintMode) {
+ mIndeterminateDrawable.setTintMode(tintInfo.mIndeterminateTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mIndeterminateDrawable.isStateful()) {
+ mIndeterminateDrawable.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Define the tileable drawable used to draw the progress bar in
+ * indeterminate mode.
+ * <p>
+ * If the drawable is a BitmapDrawable or contains BitmapDrawables, a
+ * tiled copy will be generated for display as a progress bar.
+ *
+ * @param d the new drawable
+ * @see #getIndeterminateDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setIndeterminateDrawableTiled(Drawable d) {
+ if (d != null) {
+ d = tileifyIndeterminate(d);
+ }
+
+ setIndeterminateDrawable(d);
+ }
+
+ /**
+ * <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;
+ }
+
+ /**
+ * Define the drawable used to draw the progress bar in progress mode.
+ *
+ * @param d the new drawable
+ * @see #getProgressDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setProgressDrawable(Drawable d) {
+ if (mProgressDrawable != d) {
+ if (mProgressDrawable != null) {
+ mProgressDrawable.setCallback(null);
+ unscheduleDrawable(mProgressDrawable);
+ }
+
+ mProgressDrawable = d;
+
+ if (d != null) {
+ d.setCallback(this);
+ d.setLayoutDirection(getLayoutDirection());
+ if (d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+
+ // Make sure the ProgressBar is always tall enough
+ int drawableHeight = d.getMinimumHeight();
+ if (mMaxHeight < drawableHeight) {
+ mMaxHeight = drawableHeight;
+ requestLayout();
+ }
+
+ applyProgressTints();
+ }
+
+ if (!mIndeterminate) {
+ swapCurrentDrawable(d);
+ postInvalidate();
+ }
+
+ updateDrawableBounds(getWidth(), getHeight());
+ updateDrawableState();
+
+ doRefreshProgress(R.id.progress, mProgress, false, false, false);
+ doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false, false);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean getMirrorForRtl() {
+ return mMirrorForRtl;
+ }
+
+ /**
+ * Applies the progress tints in order of increasing specificity.
+ */
+ private void applyProgressTints() {
+ if (mProgressDrawable != null && mProgressTintInfo != null) {
+ applyPrimaryProgressTint();
+ applyProgressBackgroundTint();
+ applySecondaryProgressTint();
+ }
+ }
+
+ /**
+ * Should only be called if we've already verified that mProgressDrawable
+ * and mProgressTintInfo are non-null.
+ */
+ private void applyPrimaryProgressTint() {
+ if (mProgressTintInfo.mHasProgressTint
+ || mProgressTintInfo.mHasProgressTintMode) {
+ final Drawable target = getTintTarget(R.id.progress, true);
+ if (target != null) {
+ if (mProgressTintInfo.mHasProgressTint) {
+ target.setTintList(mProgressTintInfo.mProgressTintList);
+ }
+ if (mProgressTintInfo.mHasProgressTintMode) {
+ target.setTintMode(mProgressTintInfo.mProgressTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (target.isStateful()) {
+ target.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Should only be called if we've already verified that mProgressDrawable
+ * and mProgressTintInfo are non-null.
+ */
+ private void applyProgressBackgroundTint() {
+ if (mProgressTintInfo.mHasProgressBackgroundTint
+ || mProgressTintInfo.mHasProgressBackgroundTintMode) {
+ final Drawable target = getTintTarget(R.id.background, false);
+ if (target != null) {
+ if (mProgressTintInfo.mHasProgressBackgroundTint) {
+ target.setTintList(mProgressTintInfo.mProgressBackgroundTintList);
+ }
+ if (mProgressTintInfo.mHasProgressBackgroundTintMode) {
+ target.setTintMode(mProgressTintInfo.mProgressBackgroundTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (target.isStateful()) {
+ target.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Should only be called if we've already verified that mProgressDrawable
+ * and mProgressTintInfo are non-null.
+ */
+ private void applySecondaryProgressTint() {
+ if (mProgressTintInfo.mHasSecondaryProgressTint
+ || mProgressTintInfo.mHasSecondaryProgressTintMode) {
+ final Drawable target = getTintTarget(R.id.secondaryProgress, false);
+ if (target != null) {
+ if (mProgressTintInfo.mHasSecondaryProgressTint) {
+ target.setTintList(mProgressTintInfo.mSecondaryProgressTintList);
+ }
+ if (mProgressTintInfo.mHasSecondaryProgressTintMode) {
+ target.setTintMode(mProgressTintInfo.mSecondaryProgressTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (target.isStateful()) {
+ target.setState(getDrawableState());
+ }
+ }
+ }
+ }
+
+ /**
+ * Applies a tint to the progress indicator, if one exists, or to the
+ * entire progress drawable otherwise. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * The progress indicator should be specified as a layer with
+ * id {@link android.R.id#progress} in a {@link LayerDrawable}
+ * used as the progress drawable.
+ * <p>
+ * Subsequent calls to {@link #setProgressDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and
+ * tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#ProgressBar_progressTint
+ * @see #getProgressTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ @RemotableViewMethod
+ public void setProgressTintList(@Nullable ColorStateList tint) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressTintList = tint;
+ mProgressTintInfo.mHasProgressTint = true;
+
+ if (mProgressDrawable != null) {
+ applyPrimaryProgressTint();
+ }
+ }
+
+ /**
+ * Returns the tint applied to the progress drawable, if specified.
+ *
+ * @return the tint applied to the progress drawable
+ * @attr ref android.R.styleable#ProgressBar_progressTint
+ * @see #setProgressTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getProgressTintList() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setProgressTintList(ColorStateList)}} to the progress
+ * indicator. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#ProgressBar_progressTintMode
+ * @see #getProgressTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressTintMode = tintMode;
+ mProgressTintInfo.mHasProgressTintMode = true;
+
+ if (mProgressDrawable != null) {
+ applyPrimaryProgressTint();
+ }
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the progress
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the progress
+ * drawable
+ * @attr ref android.R.styleable#ProgressBar_progressTintMode
+ * @see #setProgressTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getProgressTintMode() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintMode : null;
+ }
+
+ /**
+ * Applies a tint to the progress background, if one exists. Does not
+ * modify the current tint mode, which is
+ * {@link PorterDuff.Mode#SRC_ATOP} by default.
+ * <p>
+ * The progress background must be specified as a layer with
+ * id {@link android.R.id#background} in a {@link LayerDrawable}
+ * used as the progress drawable.
+ * <p>
+ * Subsequent calls to {@link #setProgressDrawable(Drawable)} where the
+ * drawable contains a progress background will automatically mutate the
+ * drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#ProgressBar_progressBackgroundTint
+ * @see #getProgressBackgroundTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ @RemotableViewMethod
+ public void setProgressBackgroundTintList(@Nullable ColorStateList tint) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressBackgroundTintList = tint;
+ mProgressTintInfo.mHasProgressBackgroundTint = true;
+
+ if (mProgressDrawable != null) {
+ applyProgressBackgroundTint();
+ }
+ }
+
+ /**
+ * Returns the tint applied to the progress background, if specified.
+ *
+ * @return the tint applied to the progress background
+ * @attr ref android.R.styleable#ProgressBar_progressBackgroundTint
+ * @see #setProgressBackgroundTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getProgressBackgroundTintList() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setProgressBackgroundTintList(ColorStateList)}} to the progress
+ * background. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#ProgressBar_progressBackgroundTintMode
+ * @see #setProgressBackgroundTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setProgressBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mProgressBackgroundTintMode = tintMode;
+ mProgressTintInfo.mHasProgressBackgroundTintMode = true;
+
+ if (mProgressDrawable != null) {
+ applyProgressBackgroundTint();
+ }
+ }
+
+ /**
+ * @return the blending mode used to apply the tint to the progress
+ * background
+ * @attr ref android.R.styleable#ProgressBar_progressBackgroundTintMode
+ * @see #setProgressBackgroundTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getProgressBackgroundTintMode() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintMode : null;
+ }
+
+ /**
+ * Applies a tint to the secondary progress indicator, if one exists.
+ * Does not modify the current tint mode, which is
+ * {@link PorterDuff.Mode#SRC_ATOP} by default.
+ * <p>
+ * The secondary progress indicator must be specified as a layer with
+ * id {@link android.R.id#secondaryProgress} in a {@link LayerDrawable}
+ * used as the progress drawable.
+ * <p>
+ * Subsequent calls to {@link #setProgressDrawable(Drawable)} where the
+ * drawable contains a secondary progress indicator will automatically
+ * mutate the drawable and apply the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgressTint
+ * @see #getSecondaryProgressTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setSecondaryProgressTintList(@Nullable ColorStateList tint) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mSecondaryProgressTintList = tint;
+ mProgressTintInfo.mHasSecondaryProgressTint = true;
+
+ if (mProgressDrawable != null) {
+ applySecondaryProgressTint();
+ }
+ }
+
+ /**
+ * Returns the tint applied to the secondary progress drawable, if
+ * specified.
+ *
+ * @return the tint applied to the secondary progress drawable
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgressTint
+ * @see #setSecondaryProgressTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getSecondaryProgressTintList() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mSecondaryProgressTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setSecondaryProgressTintList(ColorStateList)}} to the secondary
+ * progress indicator. The default mode is
+ * {@link PorterDuff.Mode#SRC_ATOP}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgressTintMode
+ * @see #setSecondaryProgressTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setSecondaryProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mProgressTintInfo == null) {
+ mProgressTintInfo = new ProgressTintInfo();
+ }
+ mProgressTintInfo.mSecondaryProgressTintMode = tintMode;
+ mProgressTintInfo.mHasSecondaryProgressTintMode = true;
+
+ if (mProgressDrawable != null) {
+ applySecondaryProgressTint();
+ }
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the secondary
+ * progress drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the secondary
+ * progress drawable
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgressTintMode
+ * @see #setSecondaryProgressTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getSecondaryProgressTintMode() {
+ return mProgressTintInfo != null ? mProgressTintInfo.mSecondaryProgressTintMode : null;
+ }
+
+ /**
+ * Returns the drawable to which a tint or tint mode should be applied.
+ *
+ * @param layerId id of the layer to modify
+ * @param shouldFallback whether the base drawable should be returned
+ * if the id does not exist
+ * @return the drawable to modify
+ */
+ @Nullable
+ private Drawable getTintTarget(int layerId, boolean shouldFallback) {
+ Drawable layer = null;
+
+ final Drawable d = mProgressDrawable;
+ if (d != null) {
+ mProgressDrawable = d.mutate();
+
+ if (d instanceof LayerDrawable) {
+ layer = ((LayerDrawable) d).findDrawableByLayerId(layerId);
+ }
+
+ if (shouldFallback && layer == null) {
+ layer = d;
+ }
+ }
+
+ return layer;
+ }
+
+ /**
+ * Define the tileable drawable used to draw the progress bar in
+ * progress mode.
+ * <p>
+ * If the drawable is a BitmapDrawable or contains BitmapDrawables, a
+ * tiled copy will be generated for display as a progress bar.
+ *
+ * @param d the new drawable
+ * @see #getProgressDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setProgressDrawableTiled(Drawable d) {
+ if (d != null) {
+ d = tileify(d, false);
+ }
+
+ setProgressDrawable(d);
+ }
+
+ /**
+ * @return The drawable currently used to draw the progress bar
+ */
+ Drawable getCurrentDrawable() {
+ return mCurrentDrawable;
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return who == mProgressDrawable || who == mIndeterminateDrawable
+ || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mProgressDrawable != null) mProgressDrawable.jumpToCurrentState();
+ if (mIndeterminateDrawable != null) mIndeterminateDrawable.jumpToCurrentState();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onResolveDrawables(int layoutDirection) {
+ final Drawable d = mCurrentDrawable;
+ if (d != null) {
+ d.setLayoutDirection(layoutDirection);
+ }
+ if (mIndeterminateDrawable != null) {
+ mIndeterminateDrawable.setLayoutDirection(layoutDirection);
+ }
+ if (mProgressDrawable != null) {
+ mProgressDrawable.setLayoutDirection(layoutDirection);
+ }
+ }
+
+ @Override
+ public void postInvalidate() {
+ if (!mNoInvalidate) {
+ super.postInvalidate();
+ }
+ }
+
+ private class RefreshProgressRunnable implements Runnable {
+ public void run() {
+ synchronized (ProgressBar.this) {
+ final int count = mRefreshData.size();
+ for (int i = 0; i < count; i++) {
+ final RefreshData rd = mRefreshData.get(i);
+ doRefreshProgress(rd.id, rd.progress, rd.fromUser, true, rd.animate);
+ rd.recycle();
+ }
+ mRefreshData.clear();
+ mRefreshIsPosted = false;
+ }
+ }
+ }
+
+ private static class RefreshData {
+ private static final int POOL_MAX = 24;
+ private static final SynchronizedPool<RefreshData> sPool =
+ new SynchronizedPool<RefreshData>(POOL_MAX);
+
+ public int id;
+ public int progress;
+ public boolean fromUser;
+ public boolean animate;
+
+ public static RefreshData obtain(int id, int progress, boolean fromUser, boolean animate) {
+ RefreshData rd = sPool.acquire();
+ if (rd == null) {
+ rd = new RefreshData();
+ }
+ rd.id = id;
+ rd.progress = progress;
+ rd.fromUser = fromUser;
+ rd.animate = animate;
+ return rd;
+ }
+
+ public void recycle() {
+ sPool.release(this);
+ }
+ }
+
+ private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,
+ boolean callBackToApp, boolean animate) {
+ int range = mMax - mMin;
+ final float scale = range > 0 ? (progress - mMin) / (float) range : 0;
+ final boolean isPrimary = id == R.id.progress;
+
+ if (isPrimary && animate) {
+ final ObjectAnimator animator = ObjectAnimator.ofFloat(this, VISUAL_PROGRESS, scale);
+ animator.setAutoCancel(true);
+ animator.setDuration(PROGRESS_ANIM_DURATION);
+ animator.setInterpolator(PROGRESS_ANIM_INTERPOLATOR);
+ animator.start();
+ } else {
+ setVisualProgress(id, scale);
+ }
+
+ if (isPrimary && callBackToApp) {
+ onProgressRefresh(scale, fromUser, progress);
+ }
+ }
+
+ void onProgressRefresh(float scale, boolean fromUser, int progress) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ scheduleAccessibilityEventSender();
+ }
+ }
+
+ /**
+ * Sets the visual state of a progress indicator.
+ *
+ * @param id the identifier of the progress indicator
+ * @param progress the visual progress in the range [0...1]
+ */
+ private void setVisualProgress(int id, float progress) {
+ mVisualProgress = progress;
+
+ Drawable d = mCurrentDrawable;
+
+ if (d instanceof LayerDrawable) {
+ d = ((LayerDrawable) d).findDrawableByLayerId(id);
+ if (d == null) {
+ // If we can't find the requested layer, fall back to setting
+ // the level of the entire drawable. This will break if
+ // progress is set on multiple elements, but the theme-default
+ // drawable will always have all layer IDs present.
+ d = mCurrentDrawable;
+ }
+ }
+
+ if (d != null) {
+ final int level = (int) (progress * MAX_LEVEL);
+ d.setLevel(level);
+ } else {
+ invalidate();
+ }
+
+ onVisualProgressChanged(id, progress);
+ }
+
+ /**
+ * Called when the visual state of a progress indicator changes.
+ *
+ * @param id the identifier of the progress indicator
+ * @param progress the visual progress in the range [0...1]
+ */
+ void onVisualProgressChanged(int id, float progress) {
+ // Stub method.
+ }
+
+ private synchronized void refreshProgress(int id, int progress, boolean fromUser,
+ boolean animate) {
+ if (mUiThreadId == Thread.currentThread().getId()) {
+ doRefreshProgress(id, progress, fromUser, true, animate);
+ } else {
+ if (mRefreshProgressRunnable == null) {
+ mRefreshProgressRunnable = new RefreshProgressRunnable();
+ }
+
+ final RefreshData rd = RefreshData.obtain(id, progress, fromUser, animate);
+ mRefreshData.add(rd);
+ if (mAttached && !mRefreshIsPosted) {
+ post(mRefreshProgressRunnable);
+ mRefreshIsPosted = true;
+ }
+ }
+ }
+
+ /**
+ * Sets the current progress to the specified value. Does not do anything
+ * if the progress bar is in indeterminate mode.
+ * <p>
+ * This method will immediately update the visual position of the progress
+ * indicator. To animate the visual position to the target value, use
+ * {@link #setProgress(int, boolean)}}.
+ *
+ * @param progress the new progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #getProgress()
+ * @see #incrementProgressBy(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setProgress(int progress) {
+ setProgressInternal(progress, false, false);
+ }
+
+ /**
+ * Sets the current progress to the specified value, optionally animating
+ * the visual position between the current and target values.
+ * <p>
+ * Animation does not affect the result of {@link #getProgress()}, which
+ * will return the target value immediately after this method is called.
+ *
+ * @param progress the new progress value, between 0 and {@link #getMax()}
+ * @param animate {@code true} to animate between the current and target
+ * values or {@code false} to not animate
+ */
+ public void setProgress(int progress, boolean animate) {
+ setProgressInternal(progress, false, animate);
+ }
+
+ @android.view.RemotableViewMethod
+ synchronized boolean setProgressInternal(int progress, boolean fromUser, boolean animate) {
+ if (mIndeterminate) {
+ // Not applicable.
+ return false;
+ }
+
+ progress = MathUtils.constrain(progress, mMin, mMax);
+
+ if (progress == mProgress) {
+ // No change from current.
+ return false;
+ }
+
+ mProgress = progress;
+ refreshProgress(R.id.progress, mProgress, fromUser, animate);
+ return true;
+ }
+
+ /**
+ * <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)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setSecondaryProgress(int secondaryProgress) {
+ if (mIndeterminate) {
+ return;
+ }
+
+ if (secondaryProgress < mMin) {
+ secondaryProgress = mMin;
+ }
+
+ if (secondaryProgress > mMax) {
+ secondaryProgress = mMax;
+ }
+
+ if (secondaryProgress != mSecondaryProgress) {
+ mSecondaryProgress = secondaryProgress;
+ refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, 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()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ 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()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getSecondaryProgress() {
+ return mIndeterminate ? 0 : mSecondaryProgress;
+ }
+
+ /**
+ * <p>Return the lower limit of this progress bar's range.</p>
+ *
+ * @return a positive integer
+ *
+ * @see #setMin(int)
+ * @see #getProgress()
+ * @see #getSecondaryProgress()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getMin() {
+ return mMin;
+ }
+
+ /**
+ * <p>Return the upper limit of this progress bar's range.</p>
+ *
+ * @return a positive integer
+ *
+ * @see #setMax(int)
+ * @see #getProgress()
+ * @see #getSecondaryProgress()
+ */
+ @ViewDebug.ExportedProperty(category = "progress")
+ public synchronized int getMax() {
+ return mMax;
+ }
+
+ /**
+ * <p>Set the lower range of the progress bar to <tt>min</tt>.</p>
+ *
+ * @param min the lower range of this progress bar
+ *
+ * @see #getMin()
+ * @see #setProgress(int)
+ * @see #setSecondaryProgress(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setMin(int min) {
+ if (mMaxInitialized) {
+ if (min > mMax) {
+ min = mMax;
+ }
+ }
+ mMinInitialized = true;
+ if (mMaxInitialized && min != mMin) {
+ mMin = min;
+ postInvalidate();
+
+ if (mProgress < min) {
+ mProgress = min;
+ }
+ refreshProgress(R.id.progress, mProgress, false, false);
+ } else {
+ mMin = min;
+ }
+ }
+
+ /**
+ * <p>Set the upper range of the progress bar <tt>max</tt>.</p>
+ *
+ * @param max the upper range of this progress bar
+ *
+ * @see #getMax()
+ * @see #setProgress(int)
+ * @see #setSecondaryProgress(int)
+ */
+ @android.view.RemotableViewMethod
+ public synchronized void setMax(int max) {
+ if (mMinInitialized) {
+ if (max < mMin) {
+ max = mMin;
+ }
+ }
+ mMaxInitialized = true;
+ if (mMinInitialized && max != mMax) {
+ mMax = max;
+ postInvalidate();
+
+ if (mProgress > max) {
+ mProgress = max;
+ }
+ refreshProgress(R.id.progress, mProgress, false, false);
+ } else {
+ mMax = 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() {
+ if (getVisibility() != VISIBLE || getWindowVisibility() != VISIBLE) {
+ return;
+ }
+
+ if (mIndeterminateDrawable instanceof Animatable) {
+ mShouldStartAnimationDrawable = true;
+ mHasAnimation = false;
+ } else {
+ mHasAnimation = true;
+
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ if (mTransformation == null) {
+ mTransformation = new Transformation();
+ } else {
+ mTransformation.clear();
+ }
+
+ if (mAnimation == null) {
+ mAnimation = new AlphaAnimation(0.0f, 1.0f);
+ } else {
+ mAnimation.reset();
+ }
+
+ 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() {
+ mHasAnimation = false;
+ if (mIndeterminateDrawable instanceof Animatable) {
+ ((Animatable) mIndeterminateDrawable).stop();
+ mShouldStartAnimationDrawable = false;
+ }
+ postInvalidate();
+ }
+
+ /**
+ * 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, @InterpolatorRes 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 onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+
+ if (isVisible != mAggregatedIsVisible) {
+ mAggregatedIsVisible = isVisible;
+
+ if (mIndeterminate) {
+ // let's be nice with the UI thread
+ if (isVisible) {
+ startAnimation();
+ } else {
+ stopAnimation();
+ }
+ }
+
+ if (mCurrentDrawable != null) {
+ mCurrentDrawable.setVisible(isVisible, false);
+ }
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable dr) {
+ if (!mInDrawing) {
+ if (verifyDrawable(dr)) {
+ final Rect dirty = dr.getBounds();
+ final int scrollX = mScrollX + mPaddingLeft;
+ final int scrollY = mScrollY + mPaddingTop;
+
+ invalidate(dirty.left + scrollX, dirty.top + scrollY,
+ dirty.right + scrollX, dirty.bottom + scrollY);
+ } else {
+ super.invalidateDrawable(dr);
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateDrawableBounds(w, h);
+ }
+
+ private void updateDrawableBounds(int w, int h) {
+ // onDraw will translate the canvas so we draw starting at 0,0.
+ // Subtract out padding for the purposes of the calculations below.
+ w -= mPaddingRight + mPaddingLeft;
+ h -= mPaddingTop + mPaddingBottom;
+
+ int right = w;
+ int bottom = h;
+ int top = 0;
+ int left = 0;
+
+ if (mIndeterminateDrawable != null) {
+ // Aspect ratio logic does not apply to AnimationDrawables
+ if (mOnlyIndeterminate && !(mIndeterminateDrawable instanceof AnimationDrawable)) {
+ // Maintain aspect ratio. Certain kinds of animated drawables
+ // get very confused otherwise.
+ final int intrinsicWidth = mIndeterminateDrawable.getIntrinsicWidth();
+ final int intrinsicHeight = mIndeterminateDrawable.getIntrinsicHeight();
+ final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
+ final float boundAspect = (float) w / h;
+ if (intrinsicAspect != boundAspect) {
+ if (boundAspect > intrinsicAspect) {
+ // New width is larger. Make it smaller to match height.
+ final int width = (int) (h * intrinsicAspect);
+ left = (w - width) / 2;
+ right = left + width;
+ } else {
+ // New height is larger. Make it smaller to match width.
+ final int height = (int) (w * (1 / intrinsicAspect));
+ top = (h - height) / 2;
+ bottom = top + height;
+ }
+ }
+ }
+ if (isLayoutRtl() && mMirrorForRtl) {
+ int tempLeft = left;
+ left = w - right;
+ right = w - tempLeft;
+ }
+ mIndeterminateDrawable.setBounds(left, top, right, bottom);
+ }
+
+ if (mProgressDrawable != null) {
+ mProgressDrawable.setBounds(0, 0, right, bottom);
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawTrack(canvas);
+ }
+
+ /**
+ * Draws the progress bar track.
+ */
+ void drawTrack(Canvas canvas) {
+ final Drawable d = mCurrentDrawable;
+ if (d != null) {
+ // Translate canvas so a indeterminate circular progress bar with padding
+ // rotates properly in its animation
+ final int saveCount = canvas.save();
+
+ if (isLayoutRtl() && mMirrorForRtl) {
+ canvas.translate(getWidth() - mPaddingRight, mPaddingTop);
+ canvas.scale(-1.0f, 1.0f);
+ } else {
+ canvas.translate(mPaddingLeft, mPaddingTop);
+ }
+
+ final long time = getDrawingTime();
+ if (mHasAnimation) {
+ mAnimation.getTransformation(time, mTransformation);
+ final float scale = mTransformation.getAlpha();
+ try {
+ mInDrawing = true;
+ d.setLevel((int) (scale * MAX_LEVEL));
+ } finally {
+ mInDrawing = false;
+ }
+ postInvalidateOnAnimation();
+ }
+
+ d.draw(canvas);
+ canvas.restoreToCount(saveCount);
+
+ if (mShouldStartAnimationDrawable && d instanceof Animatable) {
+ ((Animatable) d).start();
+ mShouldStartAnimationDrawable = false;
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int dw = 0;
+ int dh = 0;
+
+ final Drawable d = mCurrentDrawable;
+ if (d != null) {
+ dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+ dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+ }
+
+ updateDrawableState();
+
+ dw += mPaddingLeft + mPaddingRight;
+ dh += mPaddingTop + mPaddingBottom;
+
+ final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
+ final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ updateDrawableState();
+ }
+
+ private void updateDrawableState() {
+ final int[] state = getDrawableState();
+ boolean changed = false;
+
+ final Drawable progressDrawable = mProgressDrawable;
+ if (progressDrawable != null && progressDrawable.isStateful()) {
+ changed |= progressDrawable.setState(state);
+ }
+
+ final Drawable indeterminateDrawable = mIndeterminateDrawable;
+ if (indeterminateDrawable != null && indeterminateDrawable.isStateful()) {
+ changed |= indeterminateDrawable.setState(state);
+ }
+
+ if (changed) {
+ invalidate();
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mProgressDrawable != null) {
+ mProgressDrawable.setHotspot(x, y);
+ }
+
+ if (mIndeterminateDrawable != null) {
+ mIndeterminateDrawable.setHotspot(x, y);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ int progress;
+ int secondaryProgress;
+
+ /**
+ * Constructor called from {@link ProgressBar#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ progress = in.readInt();
+ secondaryProgress = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(progress);
+ out.writeInt(secondaryProgress);
+ }
+
+ 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
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+
+ ss.progress = mProgress;
+ ss.secondaryProgress = mSecondaryProgress;
+
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ setProgress(ss.progress);
+ setSecondaryProgress(ss.secondaryProgress);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mIndeterminate) {
+ startAnimation();
+ }
+ if (mRefreshData != null) {
+ synchronized (this) {
+ final int count = mRefreshData.size();
+ for (int i = 0; i < count; i++) {
+ final RefreshData rd = mRefreshData.get(i);
+ doRefreshProgress(rd.id, rd.progress, rd.fromUser, true, rd.animate);
+ rd.recycle();
+ }
+ mRefreshData.clear();
+ }
+ }
+ mAttached = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mIndeterminate) {
+ stopAnimation();
+ }
+ if (mRefreshProgressRunnable != null) {
+ removeCallbacks(mRefreshProgressRunnable);
+ mRefreshIsPosted = false;
+ }
+ if (mAccessibilityEventSender != null) {
+ removeCallbacks(mAccessibilityEventSender);
+ }
+ // This should come after stopAnimation(), otherwise an invalidate message remains in the
+ // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
+ super.onDetachedFromWindow();
+ mAttached = false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ProgressBar.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setItemCount(mMax - mMin);
+ event.setCurrentItemIndex(mProgress);
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (!isIndeterminate()) {
+ AccessibilityNodeInfo.RangeInfo rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(
+ AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT, 0, getMax(), getProgress());
+ info.setRangeInfo(rangeInfo);
+ }
+ }
+
+ /**
+ * Schedule a command for sending an accessibility event.
+ * </br>
+ * Note: A command is used to ensure that accessibility events
+ * are sent at most one in a given time frame to save
+ * system resources while the progress changes quickly.
+ */
+ private void scheduleAccessibilityEventSender() {
+ if (mAccessibilityEventSender == null) {
+ mAccessibilityEventSender = new AccessibilityEventSender();
+ } else {
+ removeCallbacks(mAccessibilityEventSender);
+ }
+ postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+
+ stream.addProperty("progress:max", getMax());
+ stream.addProperty("progress:progress", getProgress());
+ stream.addProperty("progress:secondaryProgress", getSecondaryProgress());
+ stream.addProperty("progress:indeterminate", isIndeterminate());
+ }
+
+ /**
+ * Returns whether the ProgressBar is animating or not. This is essentially the same
+ * as whether the ProgressBar is {@link #isIndeterminate() indeterminate} and visible,
+ * as indeterminate ProgressBars are always animating, and non-indeterminate
+ * ProgressBars are not animating.
+ *
+ * @return true if the ProgressBar is animating, false otherwise.
+ */
+ public boolean isAnimating() {
+ return isIndeterminate() && getWindowVisibility() == VISIBLE && isShown();
+ }
+
+ /**
+ * Command for sending an accessibility event.
+ */
+ private class AccessibilityEventSender implements Runnable {
+ public void run() {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ }
+ }
+
+ private static class ProgressTintInfo {
+ ColorStateList mIndeterminateTintList;
+ PorterDuff.Mode mIndeterminateTintMode;
+ boolean mHasIndeterminateTint;
+ boolean mHasIndeterminateTintMode;
+
+ ColorStateList mProgressTintList;
+ PorterDuff.Mode mProgressTintMode;
+ boolean mHasProgressTint;
+ boolean mHasProgressTintMode;
+
+ ColorStateList mProgressBackgroundTintList;
+ PorterDuff.Mode mProgressBackgroundTintMode;
+ boolean mHasProgressBackgroundTint;
+ boolean mHasProgressBackgroundTintMode;
+
+ ColorStateList mSecondaryProgressTintList;
+ PorterDuff.Mode mSecondaryProgressTintMode;
+ boolean mHasSecondaryProgressTint;
+ boolean mHasSecondaryProgressTintMode;
+ }
+
+ /**
+ * Property wrapper around the visual state of the {@code progress} functionality
+ * handled by the {@link ProgressBar#setProgress(int, boolean)} method. This does
+ * not correspond directly to the actual progress -- only the visual state.
+ */
+ private final FloatProperty<ProgressBar> VISUAL_PROGRESS =
+ new FloatProperty<ProgressBar>("visual_progress") {
+ @Override
+ public void setValue(ProgressBar object, float value) {
+ object.setVisualProgress(R.id.progress, value);
+ object.mVisualProgress = value;
+ }
+
+ @Override
+ public Float get(ProgressBar object) {
+ return object.mVisualProgress;
+ }
+ };
+}
diff --git a/android/widget/QuickContactBadge.java b/android/widget/QuickContactBadge.java
new file mode 100644
index 0000000..8f6b0d5
--- /dev/null
+++ b/android/widget/QuickContactBadge.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2009 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.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.QuickContact;
+import android.provider.ContactsContract.RawContacts;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.android.internal.R;
+
+/**
+ * Widget used to show an image with the standard QuickContact badge
+ * and on-click behavior.
+ */
+public class QuickContactBadge extends ImageView implements OnClickListener {
+ private Uri mContactUri;
+ private String mContactEmail;
+ private String mContactPhone;
+ private Drawable mOverlay;
+ private QueryHandler mQueryHandler;
+ private Drawable mDefaultAvatar;
+ private Bundle mExtras = null;
+ private String mPrioritizedMimeType;
+
+ protected String[] mExcludeMimes = null;
+
+ static final private int TOKEN_EMAIL_LOOKUP = 0;
+ static final private int TOKEN_PHONE_LOOKUP = 1;
+ static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2;
+ static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3;
+
+ static final private String EXTRA_URI_CONTENT = "uri_content";
+
+ static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
+ RawContacts.CONTACT_ID,
+ Contacts.LOOKUP_KEY,
+ };
+ static final int EMAIL_ID_COLUMN_INDEX = 0;
+ static final int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1;
+
+ static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.LOOKUP_KEY,
+ };
+ static final int PHONE_ID_COLUMN_INDEX = 0;
+ static final int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1;
+
+ public QuickContactBadge(Context context) {
+ this(context, null);
+ }
+
+ public QuickContactBadge(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public QuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public QuickContactBadge(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
+ mOverlay = styledAttributes.getDrawable(
+ com.android.internal.R.styleable.Theme_quickContactBadgeOverlay);
+ styledAttributes.recycle();
+
+ setOnClickListener(this);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (!isInEditMode()) {
+ mQueryHandler = new QueryHandler(mContext.getContentResolver());
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final Drawable overlay = mOverlay;
+ if (overlay != null && overlay.isStateful()
+ && overlay.setState(getDrawableState())) {
+ invalidateDrawable(overlay);
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mOverlay != null) {
+ mOverlay.setHotspot(x, y);
+ }
+ }
+
+ /** This call has no effect anymore, as there is only one QuickContact mode */
+ @SuppressWarnings("unused")
+ public void setMode(int size) {
+ }
+
+ /**
+ * Set which mimetype should be prioritized in the QuickContacts UI. For example, passing the
+ * value {@link Email#CONTENT_ITEM_TYPE} can cause emails to be displayed more prominently in
+ * QuickContacts.
+ */
+ public void setPrioritizedMimeType(String prioritizedMimeType) {
+ mPrioritizedMimeType = prioritizedMimeType;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!isEnabled()) {
+ // not clickable? don't show triangle
+ return;
+ }
+
+ if (mOverlay == null || mOverlay.getIntrinsicWidth() == 0 ||
+ mOverlay.getIntrinsicHeight() == 0) {
+ // nothing to draw
+ return;
+ }
+
+ mOverlay.setBounds(0, 0, getWidth(), getHeight());
+
+ if (mPaddingTop == 0 && mPaddingLeft == 0) {
+ mOverlay.draw(canvas);
+ } else {
+ int saveCount = canvas.getSaveCount();
+ canvas.save();
+ canvas.translate(mPaddingLeft, mPaddingTop);
+ mOverlay.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ /** True if a contact, an email address or a phone number has been assigned */
+ private boolean isAssigned() {
+ return mContactUri != null || mContactEmail != null || mContactPhone != null;
+ }
+
+ /**
+ * Resets the contact photo to the default state.
+ */
+ public void setImageToDefault() {
+ if (mDefaultAvatar == null) {
+ mDefaultAvatar = mContext.getDrawable(R.drawable.ic_contact_picture);
+ }
+ setImageDrawable(mDefaultAvatar);
+ }
+
+ /**
+ * Assign the contact uri that this QuickContactBadge should be associated
+ * with. Note that this is only used for displaying the QuickContact window and
+ * won't bind the contact's photo for you. Call {@link #setImageDrawable(Drawable)} to set the
+ * photo.
+ *
+ * @param contactUri Either a {@link Contacts#CONTENT_URI} or
+ * {@link Contacts#CONTENT_LOOKUP_URI} style URI.
+ */
+ public void assignContactUri(Uri contactUri) {
+ mContactUri = contactUri;
+ mContactEmail = null;
+ mContactPhone = null;
+ onContactUriChanged();
+ }
+
+ /**
+ * Assign a contact based on an email address. This should only be used when
+ * the contact's URI is not available, as an extra query will have to be
+ * performed to lookup the URI based on the email.
+ *
+ * @param emailAddress The email address of the contact.
+ * @param lazyLookup If this is true, the lookup query will not be performed
+ * until this view is clicked.
+ */
+ public void assignContactFromEmail(String emailAddress, boolean lazyLookup) {
+ assignContactFromEmail(emailAddress, lazyLookup, null);
+ }
+
+ /**
+ * Assign a contact based on an email address. This should only be used when
+ * the contact's URI is not available, as an extra query will have to be
+ * performed to lookup the URI based on the email.
+
+ @param emailAddress The email address of the contact.
+ @param lazyLookup If this is true, the lookup query will not be performed
+ until this view is clicked.
+ @param extras A bundle of extras to populate the contact edit page with if the contact
+ is not found and the user chooses to add the email address to an existing contact or
+ create a new contact. Uses the same string constants as those found in
+ {@link android.provider.ContactsContract.Intents.Insert}
+ */
+
+ public void assignContactFromEmail(String emailAddress, boolean lazyLookup, Bundle extras) {
+ mContactEmail = emailAddress;
+ mExtras = extras;
+ if (!lazyLookup && mQueryHandler != null) {
+ mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, null,
+ Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
+ EMAIL_LOOKUP_PROJECTION, null, null, null);
+ } else {
+ mContactUri = null;
+ onContactUriChanged();
+ }
+ }
+
+
+ /**
+ * Assign a contact based on a phone number. This should only be used when
+ * the contact's URI is not available, as an extra query will have to be
+ * performed to lookup the URI based on the phone number.
+ *
+ * @param phoneNumber The phone number of the contact.
+ * @param lazyLookup If this is true, the lookup query will not be performed
+ * until this view is clicked.
+ */
+ public void assignContactFromPhone(String phoneNumber, boolean lazyLookup) {
+ assignContactFromPhone(phoneNumber, lazyLookup, new Bundle());
+ }
+
+ /**
+ * Assign a contact based on a phone number. This should only be used when
+ * the contact's URI is not available, as an extra query will have to be
+ * performed to lookup the URI based on the phone number.
+ *
+ * @param phoneNumber The phone number of the contact.
+ * @param lazyLookup If this is true, the lookup query will not be performed
+ * until this view is clicked.
+ * @param extras A bundle of extras to populate the contact edit page with if the contact
+ * is not found and the user chooses to add the phone number to an existing contact or
+ * create a new contact. Uses the same string constants as those found in
+ * {@link android.provider.ContactsContract.Intents.Insert}
+ */
+ public void assignContactFromPhone(String phoneNumber, boolean lazyLookup, Bundle extras) {
+ mContactPhone = phoneNumber;
+ mExtras = extras;
+ if (!lazyLookup && mQueryHandler != null) {
+ mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, null,
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
+ PHONE_LOOKUP_PROJECTION, null, null, null);
+ } else {
+ mContactUri = null;
+ onContactUriChanged();
+ }
+ }
+
+ /**
+ * Assigns the drawable that is to be drawn on top of the assigned contact photo.
+ *
+ * @param overlay Drawable to be drawn over the assigned contact photo. Must have a non-zero
+ * instrinsic width and height.
+ */
+ public void setOverlay(Drawable overlay) {
+ mOverlay = overlay;
+ }
+
+ private void onContactUriChanged() {
+ setEnabled(isAssigned());
+ }
+
+ @Override
+ public void onClick(View v) {
+ // If contact has been assigned, mExtras should no longer be null, but do a null check
+ // anyway just in case assignContactFromPhone or Email was called with a null bundle or
+ // wasn't assigned previously.
+ final Bundle extras = (mExtras == null) ? new Bundle() : mExtras;
+ if (mContactUri != null) {
+ QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
+ mExcludeMimes, mPrioritizedMimeType);
+ } else if (mContactEmail != null && mQueryHandler != null) {
+ extras.putString(EXTRA_URI_CONTENT, mContactEmail);
+ mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras,
+ Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
+ EMAIL_LOOKUP_PROJECTION, null, null, null);
+ } else if (mContactPhone != null && mQueryHandler != null) {
+ extras.putString(EXTRA_URI_CONTENT, mContactPhone);
+ mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, extras,
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
+ PHONE_LOOKUP_PROJECTION, null, null, null);
+ } else {
+ // If a contact hasn't been assigned, don't react to click.
+ return;
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return QuickContactBadge.class.getName();
+ }
+
+ /**
+ * Set a list of specific MIME-types to exclude and not display. For
+ * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
+ * profile icon.
+ */
+ public void setExcludeMimes(String[] excludeMimes) {
+ mExcludeMimes = excludeMimes;
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ Uri lookupUri = null;
+ Uri createUri = null;
+ boolean trigger = false;
+ Bundle extras = (cookie != null) ? (Bundle) cookie : new Bundle();
+ try {
+ switch(token) {
+ case TOKEN_PHONE_LOOKUP_AND_TRIGGER:
+ trigger = true;
+ createUri = Uri.fromParts("tel", extras.getString(EXTRA_URI_CONTENT), null);
+
+ //$FALL-THROUGH$
+ case TOKEN_PHONE_LOOKUP: {
+ if (cursor != null && cursor.moveToFirst()) {
+ long contactId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(PHONE_LOOKUP_STRING_COLUMN_INDEX);
+ lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+ }
+
+ break;
+ }
+ case TOKEN_EMAIL_LOOKUP_AND_TRIGGER:
+ trigger = true;
+ createUri = Uri.fromParts("mailto",
+ extras.getString(EXTRA_URI_CONTENT), null);
+
+ //$FALL-THROUGH$
+ case TOKEN_EMAIL_LOOKUP: {
+ if (cursor != null && cursor.moveToFirst()) {
+ long contactId = cursor.getLong(EMAIL_ID_COLUMN_INDEX);
+ String lookupKey = cursor.getString(EMAIL_LOOKUP_STRING_COLUMN_INDEX);
+ lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+ }
+ break;
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ mContactUri = lookupUri;
+ onContactUriChanged();
+
+ if (trigger && mContactUri != null) {
+ // Found contact, so trigger QuickContact
+ QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
+ mExcludeMimes, mPrioritizedMimeType);
+ } else if (createUri != null) {
+ // Prompt user to add this person to contacts
+ final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri);
+ if (extras != null) {
+ extras.remove(EXTRA_URI_CONTENT);
+ intent.putExtras(extras);
+ }
+ getContext().startActivity(intent);
+ }
+ }
+ }
+}
diff --git a/android/widget/RadialTimePickerView.java b/android/widget/RadialTimePickerView.java
new file mode 100644
index 0000000..757a4ca
--- /dev/null
+++ b/android/widget/RadialTimePickerView.java
@@ -0,0 +1,1381 @@
+/*
+ * Copyright (C) 2013 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.animation.ObjectAnimator;
+import android.annotation.IntDef;
+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.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import com.android.internal.R;
+import com.android.internal.widget.ExploreByTouchHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * View to show a clock circle picker (with one or two picking circles)
+ *
+ * @hide
+ */
+public class RadialTimePickerView extends View {
+ private static final String TAG = "RadialTimePickerView";
+
+ public static final int HOURS = 0;
+ public static final int MINUTES = 1;
+
+ /** @hide */
+ @IntDef({HOURS, MINUTES})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface PickerType {}
+
+ private static final int HOURS_INNER = 2;
+
+ private static final int SELECTOR_CIRCLE = 0;
+ private static final int SELECTOR_DOT = 1;
+ private static final int SELECTOR_LINE = 2;
+
+ private static final int AM = 0;
+ private static final int PM = 1;
+
+ private static final int HOURS_IN_CIRCLE = 12;
+ private static final int MINUTES_IN_CIRCLE = 60;
+ private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE;
+ private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE;
+
+ private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+ private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
+ private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
+
+ private static final int ANIM_DURATION_NORMAL = 500;
+ private static final int ANIM_DURATION_TOUCH = 60;
+
+ private static final int[] SNAP_PREFER_30S_MAP = new int[361];
+
+ private static final int NUM_POSITIONS = 12;
+ private static final float[] COS_30 = new float[NUM_POSITIONS];
+ private static final float[] SIN_30 = new float[NUM_POSITIONS];
+
+ /** "Something is wrong" color used when a color attribute is missing. */
+ private static final int MISSING_COLOR = Color.MAGENTA;
+
+ static {
+ // Prepare mapping to snap touchable degrees to selectable degrees.
+ preparePrefer30sMap();
+
+ final double increment = 2.0 * Math.PI / NUM_POSITIONS;
+ double angle = Math.PI / 2.0;
+ for (int i = 0; i < NUM_POSITIONS; i++) {
+ COS_30[i] = (float) Math.cos(angle);
+ SIN_30[i] = (float) Math.sin(angle);
+ angle += increment;
+ }
+ }
+
+ private final FloatProperty<RadialTimePickerView> HOURS_TO_MINUTES =
+ new FloatProperty<RadialTimePickerView>("hoursToMinutes") {
+ @Override
+ public Float get(RadialTimePickerView radialTimePickerView) {
+ return radialTimePickerView.mHoursToMinutes;
+ }
+
+ @Override
+ public void setValue(RadialTimePickerView object, float value) {
+ object.mHoursToMinutes = value;
+ object.invalidate();
+ }
+ };
+
+ private final String[] mHours12Texts = new String[12];
+ private final String[] mOuterHours24Texts = new String[12];
+ private final String[] mInnerHours24Texts = new String[12];
+ private final String[] mMinutesTexts = new String[12];
+
+ private final Paint[] mPaint = new Paint[2];
+ private final Paint mPaintCenter = new Paint();
+ private final Paint[] mPaintSelector = new Paint[3];
+ private final Paint mPaintBackground = new Paint();
+
+ private final Typeface mTypeface;
+
+ private final ColorStateList[] mTextColor = new ColorStateList[3];
+ private final int[] mTextSize = new int[3];
+ private final int[] mTextInset = new int[3];
+
+ private final float[][] mOuterTextX = new float[2][12];
+ private final float[][] mOuterTextY = new float[2][12];
+
+ private final float[] mInnerTextX = new float[12];
+ private final float[] mInnerTextY = new float[12];
+
+ private final int[] mSelectionDegrees = new int[2];
+
+ private final RadialPickerTouchHelper mTouchHelper;
+
+ private final Path mSelectorPath = new Path();
+
+ private boolean mIs24HourMode;
+ private boolean mShowHours;
+
+ private ObjectAnimator mHoursToMinutesAnimator;
+ private float mHoursToMinutes;
+
+ /**
+ * When in 24-hour mode, indicates that the current hour is between
+ * 1 and 12 (inclusive).
+ */
+ private boolean mIsOnInnerCircle;
+
+ private int mSelectorRadius;
+ private int mSelectorStroke;
+ private int mSelectorDotRadius;
+ private int mCenterDotRadius;
+
+ private int mSelectorColor;
+ private int mSelectorDotColor;
+
+ private int mXCenter;
+ private int mYCenter;
+ private int mCircleRadius;
+
+ private int mMinDistForInnerNumber;
+ private int mMaxDistForOuterNumber;
+ private int mHalfwayDist;
+
+ private String[] mOuterTextHours;
+ private String[] mInnerTextHours;
+ private String[] mMinutesText;
+
+ private int mAmOrPm;
+
+ private float mDisabledAlpha;
+
+ private OnValueSelectedListener mListener;
+
+ private boolean mInputEnabled = true;
+
+ interface OnValueSelectedListener {
+ /**
+ * Called when the selected value at a given picker index has changed.
+ *
+ * @param pickerType the type of value that has changed, one of:
+ * <ul>
+ * <li>{@link #MINUTES}
+ * <li>{@link #HOURS}
+ * </ul>
+ * @param newValue the new value as minute in hour (0-59) or hour in
+ * day (0-23)
+ * @param autoAdvance when the picker type is {@link #HOURS},
+ * {@code true} to switch to the {@link #MINUTES}
+ * picker or {@code false} to stay on the current
+ * picker. No effect when picker type is
+ * {@link #MINUTES}.
+ */
+ void onValueSelected(@PickerType int pickerType, int newValue, boolean autoAdvance);
+ }
+
+ /**
+ * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
+ * selectable area to each of the 12 visible values, such that the ratio of space apportioned
+ * to a visible value : space apportioned to a non-visible value will be 14 : 4.
+ * E.g. the output of 30 degrees should have a higher range of input associated with it than
+ * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
+ * circle (5 on the minutes, 1 or 13 on the hours).
+ */
+ private static void preparePrefer30sMap() {
+ // We'll split up the visible output and the non-visible output such that each visible
+ // output will correspond to a range of 14 associated input degrees, and each non-visible
+ // output will correspond to a range of 4 associate input degrees, so visible numbers
+ // are more than 3 times easier to get than non-visible numbers:
+ // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
+ //
+ // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
+ // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
+ // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
+ // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
+ // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
+ // ability to aggressively prefer the visible values by a factor of more than 3:1, which
+ // greatly contributes to the selectability of these values.
+
+ // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
+ int snappedOutputDegrees = 0;
+ // Count of how many inputs we've designated to the specified output.
+ int count = 1;
+ // How many input we expect for a specified output. This will be 14 for output divisible
+ // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
+ // the caller can decide which they need.
+ int expectedCount = 8;
+ // Iterate through the input.
+ for (int degrees = 0; degrees < 361; degrees++) {
+ // Save the input-output mapping.
+ SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees;
+ // If this is the last input for the specified output, calculate the next output and
+ // the next expected count.
+ if (count == expectedCount) {
+ snappedOutputDegrees += 6;
+ if (snappedOutputDegrees == 360) {
+ expectedCount = 7;
+ } else if (snappedOutputDegrees % 30 == 0) {
+ expectedCount = 14;
+ } else {
+ expectedCount = 4;
+ }
+ count = 1;
+ } else {
+ count++;
+ }
+ }
+ }
+
+ /**
+ * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
+ * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
+ * weighted heavier than the degrees corresponding to non-visible numbers.
+ * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
+ * mapping.
+ */
+ private static int snapPrefer30s(int degrees) {
+ if (SNAP_PREFER_30S_MAP == null) {
+ return -1;
+ }
+ return SNAP_PREFER_30S_MAP[degrees];
+ }
+
+ /**
+ * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
+ * multiples of 30), where the input will be "snapped" to the closest visible degrees.
+ * @param degrees The input degrees
+ * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
+ * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
+ * strictly lower, and 0 to snap to the closer one.
+ * @return output degrees, will be a multiple of 30
+ */
+ private static int snapOnly30s(int degrees, int forceHigherOrLower) {
+ final int stepSize = DEGREES_FOR_ONE_HOUR;
+ int floor = (degrees / stepSize) * stepSize;
+ final int ceiling = floor + stepSize;
+ if (forceHigherOrLower == 1) {
+ degrees = ceiling;
+ } else if (forceHigherOrLower == -1) {
+ if (degrees == floor) {
+ floor -= stepSize;
+ }
+ degrees = floor;
+ } else {
+ if ((degrees - floor) < (ceiling - degrees)) {
+ degrees = floor;
+ } else {
+ degrees = ceiling;
+ }
+ }
+ return degrees;
+ }
+
+ @SuppressWarnings("unused")
+ public RadialTimePickerView(Context context) {
+ this(context, null);
+ }
+
+ public RadialTimePickerView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.timePickerStyle);
+ }
+
+ public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RadialTimePickerView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs);
+
+ applyAttributes(attrs, defStyleAttr, defStyleRes);
+
+ // Pull disabled alpha from theme.
+ final TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
+ mDisabledAlpha = outValue.getFloat();
+
+ mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
+
+ mPaint[HOURS] = new Paint();
+ mPaint[HOURS].setAntiAlias(true);
+ mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
+
+ mPaint[MINUTES] = new Paint();
+ mPaint[MINUTES].setAntiAlias(true);
+ mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
+
+ mPaintCenter.setAntiAlias(true);
+
+ mPaintSelector[SELECTOR_CIRCLE] = new Paint();
+ mPaintSelector[SELECTOR_CIRCLE].setAntiAlias(true);
+
+ mPaintSelector[SELECTOR_DOT] = new Paint();
+ mPaintSelector[SELECTOR_DOT].setAntiAlias(true);
+
+ mPaintSelector[SELECTOR_LINE] = new Paint();
+ mPaintSelector[SELECTOR_LINE].setAntiAlias(true);
+ mPaintSelector[SELECTOR_LINE].setStrokeWidth(2);
+
+ mPaintBackground.setAntiAlias(true);
+
+ final Resources res = getResources();
+ mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius);
+ mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke);
+ mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius);
+ mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius);
+
+ mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
+ mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
+ mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner);
+
+ mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
+ mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
+ mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner);
+
+ mShowHours = true;
+ mHoursToMinutes = HOURS;
+ mIs24HourMode = false;
+ mAmOrPm = AM;
+
+ // Set up accessibility components.
+ mTouchHelper = new RadialPickerTouchHelper();
+ setAccessibilityDelegate(mTouchHelper);
+
+ if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ initHoursAndMinutesText();
+ initData();
+
+ // Initial values
+ final Calendar calendar = Calendar.getInstance(Locale.getDefault());
+ final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
+ final int currentMinute = calendar.get(Calendar.MINUTE);
+
+ setCurrentHourInternal(currentHour, false, false);
+ setCurrentMinuteInternal(currentMinute, false);
+
+ setHapticFeedbackEnabled(true);
+ }
+
+ void applyAttributes(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ final Context context = getContext();
+ final TypedArray a = getContext().obtainStyledAttributes(attrs,
+ R.styleable.TimePicker, defStyleAttr, defStyleRes);
+
+ final ColorStateList numbersTextColor = a.getColorStateList(
+ R.styleable.TimePicker_numbersTextColor);
+ final ColorStateList numbersInnerTextColor = a.getColorStateList(
+ R.styleable.TimePicker_numbersInnerTextColor);
+ mTextColor[HOURS] = numbersTextColor == null ?
+ ColorStateList.valueOf(MISSING_COLOR) : numbersTextColor;
+ mTextColor[HOURS_INNER] = numbersInnerTextColor == null ?
+ ColorStateList.valueOf(MISSING_COLOR) : numbersInnerTextColor;
+ mTextColor[MINUTES] = mTextColor[HOURS];
+
+ // Set up various colors derived from the selector "activated" state.
+ final ColorStateList selectorColors = a.getColorStateList(
+ R.styleable.TimePicker_numbersSelectorColor);
+ final int selectorActivatedColor;
+ if (selectorColors != null) {
+ final int[] stateSetEnabledActivated = StateSet.get(
+ StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
+ selectorActivatedColor = selectorColors.getColorForState(
+ stateSetEnabledActivated, 0);
+ } else {
+ selectorActivatedColor = MISSING_COLOR;
+ }
+
+ mPaintCenter.setColor(selectorActivatedColor);
+
+ final int[] stateSetActivated = StateSet.get(
+ StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
+
+ mSelectorColor = selectorActivatedColor;
+ mSelectorDotColor = mTextColor[HOURS].getColorForState(stateSetActivated, 0);
+
+ mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor,
+ context.getColor(R.color.timepicker_default_numbers_background_color_material)));
+
+ a.recycle();
+ }
+
+ public void initialize(int hour, int minute, boolean is24HourMode) {
+ if (mIs24HourMode != is24HourMode) {
+ mIs24HourMode = is24HourMode;
+ initData();
+ }
+
+ setCurrentHourInternal(hour, false, false);
+ setCurrentMinuteInternal(minute, false);
+ }
+
+ public void setCurrentItemShowing(int item, boolean animate) {
+ switch (item){
+ case HOURS:
+ showHours(animate);
+ break;
+ case MINUTES:
+ showMinutes(animate);
+ break;
+ default:
+ Log.e(TAG, "ClockView does not support showing item " + item);
+ }
+ }
+
+ public int getCurrentItemShowing() {
+ return mShowHours ? HOURS : MINUTES;
+ }
+
+ public void setOnValueSelectedListener(OnValueSelectedListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the current hour in 24-hour time.
+ *
+ * @param hour the current hour between 0 and 23 (inclusive)
+ */
+ public void setCurrentHour(int hour) {
+ setCurrentHourInternal(hour, true, false);
+ }
+
+ /**
+ * Sets the current hour.
+ *
+ * @param hour The current hour
+ * @param callback Whether the value listener should be invoked
+ * @param autoAdvance Whether the listener should auto-advance to the next
+ * selection mode, e.g. hour to minutes
+ */
+ private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {
+ final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
+ mSelectionDegrees[HOURS] = degrees;
+
+ // 0 is 12 AM (midnight) and 12 is 12 PM (noon).
+ final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
+ final boolean isOnInnerCircle = getInnerCircleForHour(hour);
+ if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) {
+ mAmOrPm = amOrPm;
+ mIsOnInnerCircle = isOnInnerCircle;
+
+ initData();
+ mTouchHelper.invalidateRoot();
+ }
+
+ invalidate();
+
+ if (callback && mListener != null) {
+ mListener.onValueSelected(HOURS, hour, autoAdvance);
+ }
+ }
+
+ /**
+ * Returns the current hour in 24-hour time.
+ *
+ * @return the current hour between 0 and 23 (inclusive)
+ */
+ public int getCurrentHour() {
+ return getHourForDegrees(mSelectionDegrees[HOURS], mIsOnInnerCircle);
+ }
+
+ private int getHourForDegrees(int degrees, boolean innerCircle) {
+ int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;
+ if (mIs24HourMode) {
+ // Convert the 12-hour value into 24-hour time based on where the
+ // selector is positioned.
+ if (!innerCircle && hour == 0) {
+ // Outer circle is 1 through 12.
+ hour = 12;
+ } else if (innerCircle && hour != 0) {
+ // Inner circle is 13 through 23 and 0.
+ hour += 12;
+ }
+ } else if (mAmOrPm == PM) {
+ hour += 12;
+ }
+ return hour;
+ }
+
+ /**
+ * @param hour the hour in 24-hour time or 12-hour time
+ */
+ private int getDegreesForHour(int hour) {
+ // Convert to be 0-11.
+ if (mIs24HourMode) {
+ if (hour >= 12) {
+ hour -= 12;
+ }
+ } else if (hour == 12) {
+ hour = 0;
+ }
+ return hour * DEGREES_FOR_ONE_HOUR;
+ }
+
+ /**
+ * @param hour the hour in 24-hour time or 12-hour time
+ */
+ private boolean getInnerCircleForHour(int hour) {
+ return mIs24HourMode && (hour == 0 || hour > 12);
+ }
+
+ public void setCurrentMinute(int minute) {
+ setCurrentMinuteInternal(minute, true);
+ }
+
+ private void setCurrentMinuteInternal(int minute, boolean callback) {
+ mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE;
+
+ invalidate();
+
+ if (callback && mListener != null) {
+ mListener.onValueSelected(MINUTES, minute, false);
+ }
+ }
+
+ // Returns minutes in 0-59 range
+ public int getCurrentMinute() {
+ return getMinuteForDegrees(mSelectionDegrees[MINUTES]);
+ }
+
+ private int getMinuteForDegrees(int degrees) {
+ return degrees / DEGREES_FOR_ONE_MINUTE;
+ }
+
+ private int getDegreesForMinute(int minute) {
+ return minute * DEGREES_FOR_ONE_MINUTE;
+ }
+
+ /**
+ * Sets whether the picker is showing AM or PM hours. Has no effect when
+ * in 24-hour mode.
+ *
+ * @param amOrPm {@link #AM} or {@link #PM}
+ * @return {@code true} if the value changed from what was previously set,
+ * or {@code false} otherwise
+ */
+ public boolean setAmOrPm(int amOrPm) {
+ if (mAmOrPm == amOrPm || mIs24HourMode) {
+ return false;
+ }
+
+ mAmOrPm = amOrPm;
+ invalidate();
+ mTouchHelper.invalidateRoot();
+ return true;
+ }
+
+ public int getAmOrPm() {
+ return mAmOrPm;
+ }
+
+ public void showHours(boolean animate) {
+ showPicker(true, animate);
+ }
+
+ public void showMinutes(boolean animate) {
+ showPicker(false, animate);
+ }
+
+ private void initHoursAndMinutesText() {
+ // Initialize the hours and minutes numbers.
+ for (int i = 0; i < 12; i++) {
+ mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+ mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
+ mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+ mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
+ }
+ }
+
+ private void initData() {
+ if (mIs24HourMode) {
+ mOuterTextHours = mOuterHours24Texts;
+ mInnerTextHours = mInnerHours24Texts;
+ } else {
+ mOuterTextHours = mHours12Texts;
+ mInnerTextHours = mHours12Texts;
+ }
+
+ mMinutesText = mMinutesTexts;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) {
+ return;
+ }
+
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = Math.min(mXCenter, mYCenter);
+
+ mMinDistForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius;
+ mMaxDistForOuterNumber = mCircleRadius - mTextInset[HOURS] + mSelectorRadius;
+ mHalfwayDist = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2;
+
+ calculatePositionsHours();
+ calculatePositionsMinutes();
+
+ mTouchHelper.invalidateRoot();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha;
+
+ drawCircleBackground(canvas);
+
+ final Path selectorPath = mSelectorPath;
+ drawSelector(canvas, selectorPath);
+ drawHours(canvas, selectorPath, alphaMod);
+ drawMinutes(canvas, selectorPath, alphaMod);
+ drawCenter(canvas, alphaMod);
+ }
+
+ private void showPicker(boolean hours, boolean animate) {
+ if (mShowHours == hours) {
+ return;
+ }
+
+ mShowHours = hours;
+
+ if (animate) {
+ animatePicker(hours, ANIM_DURATION_NORMAL);
+ } else {
+ // If we have a pending or running animator, cancel it.
+ if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
+ mHoursToMinutesAnimator.cancel();
+ mHoursToMinutesAnimator = null;
+ }
+ mHoursToMinutes = hours ? 0.0f : 1.0f;
+ }
+
+ initData();
+ invalidate();
+ mTouchHelper.invalidateRoot();
+ }
+
+ private void animatePicker(boolean hoursToMinutes, long duration) {
+ final float target = hoursToMinutes ? HOURS : MINUTES;
+ if (mHoursToMinutes == target) {
+ // If we have a pending or running animator, cancel it.
+ if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
+ mHoursToMinutesAnimator.cancel();
+ mHoursToMinutesAnimator = null;
+ }
+
+ // We're already showing the correct picker.
+ return;
+ }
+
+ mHoursToMinutesAnimator = ObjectAnimator.ofFloat(this, HOURS_TO_MINUTES, target);
+ mHoursToMinutesAnimator.setAutoCancel(true);
+ mHoursToMinutesAnimator.setDuration(duration);
+ mHoursToMinutesAnimator.start();
+ }
+
+ private void drawCircleBackground(Canvas canvas) {
+ canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground);
+ }
+
+ private void drawHours(Canvas canvas, Path selectorPath, float alphaMod) {
+ final int hoursAlpha = (int) (255f * (1f - mHoursToMinutes) * alphaMod + 0.5f);
+ if (hoursAlpha > 0) {
+ // Exclude the selector region, then draw inner/outer hours with no
+ // activated states.
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
+ drawHoursClipped(canvas, hoursAlpha, false);
+ canvas.restore();
+
+ // Intersect the selector region, then draw minutes with only
+ // activated states.
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipPath(selectorPath, Region.Op.INTERSECT);
+ drawHoursClipped(canvas, hoursAlpha, true);
+ canvas.restore();
+ }
+ }
+
+ private void drawHoursClipped(Canvas canvas, int hoursAlpha, boolean showActivated) {
+ // Draw outer hours.
+ drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS], mOuterTextHours,
+ mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], hoursAlpha,
+ showActivated && !mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
+
+ // Draw inner hours (13-00) for 24-hour time.
+ if (mIs24HourMode && mInnerTextHours != null) {
+ drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER],
+ mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha,
+ showActivated && mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
+ }
+ }
+
+ private void drawMinutes(Canvas canvas, Path selectorPath, float alphaMod) {
+ final int minutesAlpha = (int) (255f * mHoursToMinutes * alphaMod + 0.5f);
+ if (minutesAlpha > 0) {
+ // Exclude the selector region, then draw minutes with no
+ // activated states.
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
+ drawMinutesClipped(canvas, minutesAlpha, false);
+ canvas.restore();
+
+ // Intersect the selector region, then draw minutes with only
+ // activated states.
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipPath(selectorPath, Region.Op.INTERSECT);
+ drawMinutesClipped(canvas, minutesAlpha, true);
+ canvas.restore();
+ }
+ }
+
+ private void drawMinutesClipped(Canvas canvas, int minutesAlpha, boolean showActivated) {
+ drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], mMinutesText,
+ mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], minutesAlpha,
+ showActivated, mSelectionDegrees[MINUTES], showActivated);
+ }
+
+ private void drawCenter(Canvas canvas, float alphaMod) {
+ mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f));
+ canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter);
+ }
+
+ private int getMultipliedAlpha(int argb, int alpha) {
+ return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5);
+ }
+
+ private void drawSelector(Canvas canvas, Path selectorPath) {
+ // Determine the current length, angle, and dot scaling factor.
+ final int hoursIndex = mIsOnInnerCircle ? HOURS_INNER : HOURS;
+ final int hoursInset = mTextInset[hoursIndex];
+ final int hoursAngleDeg = mSelectionDegrees[hoursIndex % 2];
+ final float hoursDotScale = mSelectionDegrees[hoursIndex % 2] % 30 != 0 ? 1 : 0;
+
+ final int minutesIndex = MINUTES;
+ final int minutesInset = mTextInset[minutesIndex];
+ final int minutesAngleDeg = mSelectionDegrees[minutesIndex];
+ final float minutesDotScale = mSelectionDegrees[minutesIndex] % 30 != 0 ? 1 : 0;
+
+ // Calculate the current radius at which to place the selection circle.
+ final int selRadius = mSelectorRadius;
+ final float selLength =
+ mCircleRadius - MathUtils.lerp(hoursInset, minutesInset, mHoursToMinutes);
+ final double selAngleRad =
+ Math.toRadians(MathUtils.lerpDeg(hoursAngleDeg, minutesAngleDeg, mHoursToMinutes));
+ final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad);
+ final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad);
+
+ // Draw the selection circle.
+ final Paint paint = mPaintSelector[SELECTOR_CIRCLE];
+ paint.setColor(mSelectorColor);
+ canvas.drawCircle(selCenterX, selCenterY, selRadius, paint);
+
+ // If needed, set up the clip path for later.
+ if (selectorPath != null) {
+ selectorPath.reset();
+ selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW);
+ }
+
+ // Draw the dot if we're between two items.
+ final float dotScale = MathUtils.lerp(hoursDotScale, minutesDotScale, mHoursToMinutes);
+ if (dotScale > 0) {
+ final Paint dotPaint = mPaintSelector[SELECTOR_DOT];
+ dotPaint.setColor(mSelectorDotColor);
+ canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius * dotScale, dotPaint);
+ }
+
+ // Shorten the line to only go from the edge of the center dot to the
+ // edge of the selection circle.
+ final double sin = Math.sin(selAngleRad);
+ final double cos = Math.cos(selAngleRad);
+ final float lineLength = selLength - selRadius;
+ final int centerX = mXCenter + (int) (mCenterDotRadius * sin);
+ final int centerY = mYCenter - (int) (mCenterDotRadius * cos);
+ final float linePointX = centerX + (int) (lineLength * sin);
+ final float linePointY = centerY - (int) (lineLength * cos);
+
+ // Draw the line.
+ final Paint linePaint = mPaintSelector[SELECTOR_LINE];
+ linePaint.setColor(mSelectorColor);
+ linePaint.setStrokeWidth(mSelectorStroke);
+ canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint);
+ }
+
+ private void calculatePositionsHours() {
+ // Calculate the text positions
+ final float numbersRadius = mCircleRadius - mTextInset[HOURS];
+
+ // Calculate the positions for the 12 numbers in the main circle.
+ calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
+ mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]);
+
+ // If we have an inner circle, calculate those positions too.
+ if (mIs24HourMode) {
+ final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER];
+ calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
+ mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY);
+ }
+ }
+
+ private void calculatePositionsMinutes() {
+ // Calculate the text positions
+ final float numbersRadius = mCircleRadius - mTextInset[MINUTES];
+
+ // Calculate the positions for the 12 numbers in the main circle.
+ calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
+ mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]);
+ }
+
+ /**
+ * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
+ * drawn at based on the specified circle radius. Place the values in the textGridHeights and
+ * textGridWidths parameters.
+ */
+ private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter,
+ float textSize, float[] x, float[] y) {
+ // Adjust yCenter to account for the text's baseline.
+ paint.setTextSize(textSize);
+ yCenter -= (paint.descent() + paint.ascent()) / 2;
+
+ for (int i = 0; i < NUM_POSITIONS; i++) {
+ x[i] = xCenter - radius * COS_30[i];
+ y[i] = yCenter - radius * SIN_30[i];
+ }
+ }
+
+ /**
+ * Draw the 12 text values at the positions specified by the textGrid parameters.
+ */
+ private void drawTextElements(Canvas canvas, float textSize, Typeface typeface,
+ ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint,
+ int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) {
+ paint.setTextSize(textSize);
+ paint.setTypeface(typeface);
+
+ // The activated index can touch a range of elements.
+ final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS);
+ final int activatedFloor = (int) activatedIndex;
+ final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS;
+
+ for (int i = 0; i < 12; i++) {
+ final boolean activated = (activatedFloor == i || activatedCeil == i);
+ if (activatedOnly && !activated) {
+ continue;
+ }
+
+ final int stateMask = StateSet.VIEW_STATE_ENABLED
+ | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0);
+ final int color = textColor.getColorForState(StateSet.get(stateMask), 0);
+ paint.setColor(color);
+ paint.setAlpha(getMultipliedAlpha(color, alpha));
+
+ canvas.drawText(texts[i], textX[i], textY[i], paint);
+ }
+ }
+
+ private int getDegreesFromXY(float x, float y, boolean constrainOutside) {
+ // Ensure the point is inside the touchable area.
+ final int innerBound;
+ final int outerBound;
+ if (mIs24HourMode && mShowHours) {
+ innerBound = mMinDistForInnerNumber;
+ outerBound = mMaxDistForOuterNumber;
+ } else {
+ final int index = mShowHours ? HOURS : MINUTES;
+ final int center = mCircleRadius - mTextInset[index];
+ innerBound = center - mSelectorRadius;
+ outerBound = center + mSelectorRadius;
+ }
+
+ final double dX = x - mXCenter;
+ final double dY = y - mYCenter;
+ final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
+ if (distFromCenter < innerBound || constrainOutside && distFromCenter > outerBound) {
+ return -1;
+ }
+
+ // Convert to degrees.
+ final int degrees = (int) (Math.toDegrees(Math.atan2(dY, dX) + Math.PI / 2) + 0.5);
+ if (degrees < 0) {
+ return degrees + 360;
+ } else {
+ return degrees;
+ }
+ }
+
+ private boolean getInnerCircleFromXY(float x, float y) {
+ if (mIs24HourMode && mShowHours) {
+ final double dX = x - mXCenter;
+ final double dY = y - mYCenter;
+ final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
+ return distFromCenter <= mHalfwayDist;
+ }
+ return false;
+ }
+
+ boolean mChangedDuringTouch = false;
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mInputEnabled) {
+ return true;
+ }
+
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_MOVE
+ || action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_DOWN) {
+ boolean forceSelection = false;
+ boolean autoAdvance = false;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ // This is a new event stream, reset whether the value changed.
+ mChangedDuringTouch = false;
+ } else if (action == MotionEvent.ACTION_UP) {
+ autoAdvance = true;
+
+ // If we saw a down/up pair without the value changing, assume
+ // this is a single-tap selection and force a change.
+ if (!mChangedDuringTouch) {
+ forceSelection = true;
+ }
+ }
+
+ mChangedDuringTouch |= handleTouchInput(
+ event.getX(), event.getY(), forceSelection, autoAdvance);
+ }
+
+ return true;
+ }
+
+ private boolean handleTouchInput(
+ float x, float y, boolean forceSelection, boolean autoAdvance) {
+ final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
+ final int degrees = getDegreesFromXY(x, y, false);
+ if (degrees == -1) {
+ return false;
+ }
+
+ // Ensure we're showing the correct picker.
+ animatePicker(mShowHours, ANIM_DURATION_TOUCH);
+
+ final @PickerType int type;
+ final int newValue;
+ final boolean valueChanged;
+
+ if (mShowHours) {
+ final int snapDegrees = snapOnly30s(degrees, 0) % 360;
+ valueChanged = mIsOnInnerCircle != isOnInnerCircle
+ || mSelectionDegrees[HOURS] != snapDegrees;
+ mIsOnInnerCircle = isOnInnerCircle;
+ mSelectionDegrees[HOURS] = snapDegrees;
+ type = HOURS;
+ newValue = getCurrentHour();
+ } else {
+ final int snapDegrees = snapPrefer30s(degrees) % 360;
+ valueChanged = mSelectionDegrees[MINUTES] != snapDegrees;
+ mSelectionDegrees[MINUTES] = snapDegrees;
+ type = MINUTES;
+ newValue = getCurrentMinute();
+ }
+
+ if (valueChanged || forceSelection || autoAdvance) {
+ // Fire the listener even if we just need to auto-advance.
+ if (mListener != null) {
+ mListener.onValueSelected(type, newValue, autoAdvance);
+ }
+
+ // Only provide feedback if the value actually changed.
+ if (valueChanged || forceSelection) {
+ performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ invalidate();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ // First right-of-refusal goes the touch exploration helper.
+ if (mTouchHelper.dispatchHoverEvent(event)) {
+ return true;
+ }
+ return super.dispatchHoverEvent(event);
+ }
+
+ public void setInputEnabled(boolean inputEnabled) {
+ mInputEnabled = inputEnabled;
+ invalidate();
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (!isEnabled()) {
+ return null;
+ }
+ final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
+ if (degrees != -1) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ private class RadialPickerTouchHelper extends ExploreByTouchHelper {
+ private final Rect mTempRect = new Rect();
+
+ private final int TYPE_HOUR = 1;
+ private final int TYPE_MINUTE = 2;
+
+ private final int SHIFT_TYPE = 0;
+ private final int MASK_TYPE = 0xF;
+
+ private final int SHIFT_VALUE = 8;
+ private final int MASK_VALUE = 0xFF;
+
+ /** Increment in which virtual views are exposed for minutes. */
+ private final int MINUTE_INCREMENT = 5;
+
+ public RadialPickerTouchHelper() {
+ super(RadialTimePickerView.this);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+ if (super.performAccessibilityAction(host, action, arguments)) {
+ return true;
+ }
+
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ adjustPicker(1);
+ return true;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ adjustPicker(-1);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void adjustPicker(int step) {
+ final int stepSize;
+ final int initialStep;
+ final int maxValue;
+ final int minValue;
+ if (mShowHours) {
+ stepSize = 1;
+
+ final int currentHour24 = getCurrentHour();
+ if (mIs24HourMode) {
+ initialStep = currentHour24;
+ minValue = 0;
+ maxValue = 23;
+ } else {
+ initialStep = hour24To12(currentHour24);
+ minValue = 1;
+ maxValue = 12;
+ }
+ } else {
+ stepSize = 5;
+ initialStep = getCurrentMinute() / stepSize;
+ minValue = 0;
+ maxValue = 55;
+ }
+
+ final int nextValue = (initialStep + step) * stepSize;
+ final int clampedValue = MathUtils.constrain(nextValue, minValue, maxValue);
+ if (mShowHours) {
+ setCurrentHour(clampedValue);
+ } else {
+ setCurrentMinute(clampedValue);
+ }
+ }
+
+ @Override
+ protected int getVirtualViewAt(float x, float y) {
+ final int id;
+ final int degrees = getDegreesFromXY(x, y, true);
+ if (degrees != -1) {
+ final int snapDegrees = snapOnly30s(degrees, 0) % 360;
+ if (mShowHours) {
+ final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
+ final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle);
+ final int hour = mIs24HourMode ? hour24 : hour24To12(hour24);
+ id = makeId(TYPE_HOUR, hour);
+ } else {
+ final int current = getCurrentMinute();
+ final int touched = getMinuteForDegrees(degrees);
+ final int snapped = getMinuteForDegrees(snapDegrees);
+
+ // If the touched minute is closer to the current minute
+ // than it is to the snapped minute, return current.
+ final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE);
+ final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE);
+ final int minute;
+ if (currentOffset < snappedOffset) {
+ minute = current;
+ } else {
+ minute = snapped;
+ }
+ id = makeId(TYPE_MINUTE, minute);
+ }
+ } else {
+ id = INVALID_ID;
+ }
+
+ return id;
+ }
+
+ /**
+ * Returns the difference in degrees between two values along a circle.
+ *
+ * @param first value in the range [0,max]
+ * @param second value in the range [0,max]
+ * @param max the maximum value along the circle
+ * @return the difference in between the two values
+ */
+ private int getCircularDiff(int first, int second, int max) {
+ final int diff = Math.abs(first - second);
+ final int midpoint = max / 2;
+ return (diff > midpoint) ? (max - diff) : diff;
+ }
+
+ @Override
+ protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+ if (mShowHours) {
+ final int min = mIs24HourMode ? 0 : 1;
+ final int max = mIs24HourMode ? 23 : 12;
+ for (int i = min; i <= max ; i++) {
+ virtualViewIds.add(makeId(TYPE_HOUR, i));
+ }
+ } else {
+ final int current = getCurrentMinute();
+ for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) {
+ virtualViewIds.add(makeId(TYPE_MINUTE, i));
+
+ // If the current minute falls between two increments,
+ // insert an extra node for it.
+ if (current > i && current < i + MINUTE_INCREMENT) {
+ virtualViewIds.add(makeId(TYPE_MINUTE, current));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+ event.setClassName(getClass().getName());
+
+ final int type = getTypeFromId(virtualViewId);
+ final int value = getValueFromId(virtualViewId);
+ final CharSequence description = getVirtualViewDescription(type, value);
+ event.setContentDescription(description);
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+ node.setClassName(getClass().getName());
+ node.addAction(AccessibilityAction.ACTION_CLICK);
+
+ final int type = getTypeFromId(virtualViewId);
+ final int value = getValueFromId(virtualViewId);
+ final CharSequence description = getVirtualViewDescription(type, value);
+ node.setContentDescription(description);
+
+ getBoundsForVirtualView(virtualViewId, mTempRect);
+ node.setBoundsInParent(mTempRect);
+
+ final boolean selected = isVirtualViewSelected(type, value);
+ node.setSelected(selected);
+
+ final int nextId = getVirtualViewIdAfter(type, value);
+ if (nextId != INVALID_ID) {
+ node.setTraversalBefore(RadialTimePickerView.this, nextId);
+ }
+ }
+
+ private int getVirtualViewIdAfter(int type, int value) {
+ if (type == TYPE_HOUR) {
+ final int nextValue = value + 1;
+ final int max = mIs24HourMode ? 23 : 12;
+ if (nextValue <= max) {
+ return makeId(type, nextValue);
+ }
+ } else if (type == TYPE_MINUTE) {
+ final int current = getCurrentMinute();
+ final int snapValue = value - (value % MINUTE_INCREMENT);
+ final int nextValue = snapValue + MINUTE_INCREMENT;
+ if (value < current && nextValue > current) {
+ // The current value is between two snap values.
+ return makeId(type, current);
+ } else if (nextValue < MINUTES_IN_CIRCLE) {
+ return makeId(type, nextValue);
+ }
+ }
+ return INVALID_ID;
+ }
+
+ @Override
+ protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+ Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ final int type = getTypeFromId(virtualViewId);
+ final int value = getValueFromId(virtualViewId);
+ if (type == TYPE_HOUR) {
+ final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm);
+ setCurrentHour(hour);
+ return true;
+ } else if (type == TYPE_MINUTE) {
+ setCurrentMinute(value);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int hour12To24(int hour12, int amOrPm) {
+ int hour24 = hour12;
+ if (hour12 == 12) {
+ if (amOrPm == AM) {
+ hour24 = 0;
+ }
+ } else if (amOrPm == PM) {
+ hour24 += 12;
+ }
+ return hour24;
+ }
+
+ private int hour24To12(int hour24) {
+ if (hour24 == 0) {
+ return 12;
+ } else if (hour24 > 12) {
+ return hour24 - 12;
+ } else {
+ return hour24;
+ }
+ }
+
+ private void getBoundsForVirtualView(int virtualViewId, Rect bounds) {
+ final float radius;
+ final int type = getTypeFromId(virtualViewId);
+ final int value = getValueFromId(virtualViewId);
+ final float centerRadius;
+ final float degrees;
+ if (type == TYPE_HOUR) {
+ final boolean innerCircle = getInnerCircleForHour(value);
+ if (innerCircle) {
+ centerRadius = mCircleRadius - mTextInset[HOURS_INNER];
+ radius = mSelectorRadius;
+ } else {
+ centerRadius = mCircleRadius - mTextInset[HOURS];
+ radius = mSelectorRadius;
+ }
+
+ degrees = getDegreesForHour(value);
+ } else if (type == TYPE_MINUTE) {
+ centerRadius = mCircleRadius - mTextInset[MINUTES];
+ degrees = getDegreesForMinute(value);
+ radius = mSelectorRadius;
+ } else {
+ // This should never happen.
+ centerRadius = 0;
+ degrees = 0;
+ radius = 0;
+ }
+
+ final double radians = Math.toRadians(degrees);
+ final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians);
+ final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians);
+
+ bounds.set((int) (xCenter - radius), (int) (yCenter - radius),
+ (int) (xCenter + radius), (int) (yCenter + radius));
+ }
+
+ private CharSequence getVirtualViewDescription(int type, int value) {
+ final CharSequence description;
+ if (type == TYPE_HOUR || type == TYPE_MINUTE) {
+ description = Integer.toString(value);
+ } else {
+ description = null;
+ }
+ return description;
+ }
+
+ private boolean isVirtualViewSelected(int type, int value) {
+ final boolean selected;
+ if (type == TYPE_HOUR) {
+ selected = getCurrentHour() == value;
+ } else if (type == TYPE_MINUTE) {
+ selected = getCurrentMinute() == value;
+ } else {
+ selected = false;
+ }
+ return selected;
+ }
+
+ private int makeId(int type, int value) {
+ return type << SHIFT_TYPE | value << SHIFT_VALUE;
+ }
+
+ private int getTypeFromId(int id) {
+ return id >>> SHIFT_TYPE & MASK_TYPE;
+ }
+
+ private int getValueFromId(int id) {
+ return id >>> SHIFT_VALUE & MASK_VALUE;
+ }
+ }
+}
diff --git a/android/widget/RadioButton.java b/android/widget/RadioButton.java
new file mode 100644
index 0000000..d44fbd7
--- /dev/null
+++ b/android/widget/RadioButton.java
@@ -0,0 +1,84 @@
+/*
+ * 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>See the <a href="{@docRoot}guide/topics/ui/controls/radiobutton.html">Radio Buttons</a>
+ * guide.</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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * {@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();
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return RadioButton.class.getName();
+ }
+}
diff --git a/android/widget/RadioGroup.java b/android/widget/RadioGroup.java
new file mode 100644
index 0000000..5c4d4d2
--- /dev/null
+++ b/android/widget/RadioGroup.java
@@ -0,0 +1,463 @@
+/*
+ * 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.annotation.IdRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+
+/**
+ * <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 {
+ private static final String LOG_TAG = RadioGroup.class.getSimpleName();
+
+ // 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;
+
+ // Indicates whether the child was set from resources or dynamically, so it can be used
+ // to sanitize autofill requests.
+ private int mInitialCheckedId = View.NO_ID;
+
+ /**
+ * {@inheritDoc}
+ */
+ public RadioGroup(Context context) {
+ super(context);
+ setOrientation(VERTICAL);
+ init();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public RadioGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // RadioGroup is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ // 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;
+ mInitialCheckedId = 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);
+ }
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (child instanceof RadioButton) {
+ final RadioButton button = (RadioButton) child;
+ if (button.isChecked()) {
+ mProtectFromCheckedChange = true;
+ if (mCheckedId != -1) {
+ setCheckedStateForView(mCheckedId, false);
+ }
+ mProtectFromCheckedChange = false;
+ setCheckedId(button.getId());
+ }
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * <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(@IdRes 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(@IdRes int id) {
+ mCheckedId = id;
+ if (mOnCheckedChangeListener != null) {
+ mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
+ }
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.notifyValueChanged(this);
+ }
+ }
+
+ 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()
+ *
+ * @attr ref android.R.styleable#RadioGroup_checkedButton
+ */
+ @IdRes
+ 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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return RadioGroup.class.getName();
+ }
+
+ /**
+ * <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, @IdRes int checkedId);
+ }
+
+ private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
+ @Override
+ 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}
+ */
+ @Override
+ 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 = View.generateViewId();
+ child.setId(id);
+ }
+ ((RadioButton) child).setOnCheckedChangeWidgetListener(
+ mChildOnCheckedChangeListener);
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+ super.onProvideAutofillStructure(structure, flags);
+ structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!isEnabled()) return;
+
+ if (!value.isList()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ final int index = value.getListValue();
+ final View child = getChildAt(index);
+ if (child == null) {
+ Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
+ return;
+ }
+
+ check(child.getId());
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ if (!isEnabled()) return null;
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getId() == mCheckedId) {
+ return AutofillValue.forList(i);
+ }
+ }
+ return null;
+ }
+}
diff --git a/android/widget/RatingBar.java b/android/widget/RatingBar.java
new file mode 100644
index 0000000..70b70bc
--- /dev/null
+++ b/android/widget/RatingBar.java
@@ -0,0 +1,354 @@
+/*
+ * 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 android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in
+ * stars. The user can touch/drag or use arrow keys 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>
+ * When using a RatingBar that supports user interaction, placing widgets to the
+ * left or right of the RatingBar is discouraged.
+ * <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
+ * or arrow key/trackball as well as changes that were initiated
+ * programmatically.
+ */
+ public interface OnRatingBarChangeListener {
+
+ /**
+ * Notification that the rating has changed. Clients can use the
+ * fromUser 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 fromUser True if the rating change was initiated by a user's
+ * touch gesture or arrow key/horizontal trackbell movement.
+ */
+ void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser);
+
+ }
+
+ private int mNumStars = 5;
+
+ private int mProgressOnStartTracking;
+
+ private OnRatingBarChangeListener mOnRatingBarChangeListener;
+
+ public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.RatingBar, defStyleAttr, defStyleRes);
+ 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 0.5 so boundaries round up).
+ mTouchProgressOffset = 0.6f;
+ }
+
+ 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.
+ *
+ * @attr ref android.R.styleable#RatingBar_isIndicator
+ */
+ public void setIsIndicator(boolean isIndicator) {
+ mIsUserSeekable = !isIndicator;
+ if (isIndicator) {
+ setFocusable(FOCUSABLE_AUTO);
+ } else {
+ setFocusable(FOCUSABLE);
+ }
+ }
+
+ /**
+ * @return Whether this rating bar is only an indicator.
+ *
+ * @attr ref android.R.styleable#RatingBar_isIndicator
+ */
+ 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(Math.round(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 fromUser, int progress) {
+ super.onProgressRefresh(scale, fromUser, progress);
+
+ // Keep secondary progress in sync with primary
+ updateSecondaryProgress(progress);
+
+ if (!fromUser) {
+ // Callback for non-user 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 (mSampleWidth > 0) {
+ final int width = mSampleWidth * mNumStars;
+ setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, 0),
+ getMeasuredHeight());
+ }
+ }
+
+ @Override
+ void onStartTrackingTouch() {
+ mProgressOnStartTracking = getProgress();
+
+ super.onStartTrackingTouch();
+ }
+
+ @Override
+ void onStopTrackingTouch() {
+ super.onStopTrackingTouch();
+
+ if (getProgress() != mProgressOnStartTracking) {
+ dispatchRatingChange(true);
+ }
+ }
+
+ @Override
+ void onKeyChange() {
+ super.onKeyChange();
+ dispatchRatingChange(true);
+ }
+
+ void dispatchRatingChange(boolean fromUser) {
+ if (mOnRatingBarChangeListener != null) {
+ mOnRatingBarChangeListener.onRatingChanged(this, getRating(),
+ fromUser);
+ }
+ }
+
+ @Override
+ public synchronized void setMax(int max) {
+ // Disallow max progress = 0
+ if (max <= 0) {
+ return;
+ }
+
+ super.setMax(max);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return RatingBar.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (canUserSetProgress()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
+ }
+ }
+
+ @Override
+ boolean canUserSetProgress() {
+ return super.canUserSetProgress() && !isIndicator();
+ }
+}
diff --git a/android/widget/RelativeLayout.java b/android/widget/RelativeLayout.java
new file mode 100644
index 0000000..75fc538
--- /dev/null
+++ b/android/widget/RelativeLayout.java
@@ -0,0 +1,1909 @@
+/*
+ * 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 static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Pools.SynchronizedPool;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A Layout where the positions of the children can be described in relation to each other or to the
+ * parent.
+ *
+ * <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><strong>Note:</strong> In platform version 17 and lower, RelativeLayout was affected by
+ * a measurement bug that could cause child views to be measured with incorrect
+ * {@link android.view.View.MeasureSpec MeasureSpec} values. (See
+ * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int) MeasureSpec.makeMeasureSpec}
+ * for more details.) This was triggered when a RelativeLayout container was placed in
+ * a scrolling container, such as a ScrollView or HorizontalScrollView. If a custom view
+ * not equipped to properly measure with the MeasureSpec mode
+ * {@link android.view.View.MeasureSpec#UNSPECIFIED UNSPECIFIED} was placed in a RelativeLayout,
+ * this would silently work anyway as RelativeLayout would pass a very large
+ * {@link android.view.View.MeasureSpec#AT_MOST AT_MOST} MeasureSpec instead.</p>
+ *
+ * <p>This behavior has been preserved for apps that set <code>android:targetSdkVersion="17"</code>
+ * or older in their manifest's <code>uses-sdk</code> tag for compatibility. Apps targeting SDK
+ * version 18 or newer will receive the correct behavior</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/layout/relative.html">Relative
+ * Layout</a> guide.</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;
+ /**
+ * Rule that aligns a child's end edge with another child's start edge.
+ */
+ public static final int START_OF = 16;
+ /**
+ * Rule that aligns a child's start edge with another child's end edge.
+ */
+ public static final int END_OF = 17;
+ /**
+ * Rule that aligns a child's start edge with another child's start edge.
+ */
+ public static final int ALIGN_START = 18;
+ /**
+ * Rule that aligns a child's end edge with another child's end edge.
+ */
+ public static final int ALIGN_END = 19;
+ /**
+ * Rule that aligns the child's start edge with its RelativeLayout
+ * parent's start edge.
+ */
+ public static final int ALIGN_PARENT_START = 20;
+ /**
+ * Rule that aligns the child's end edge with its RelativeLayout
+ * parent's end edge.
+ */
+ public static final int ALIGN_PARENT_END = 21;
+
+ private static final int VERB_COUNT = 22;
+
+
+ private static final int[] RULES_VERTICAL = {
+ ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
+ };
+
+ private static final int[] RULES_HORIZONTAL = {
+ LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END
+ };
+
+ /**
+ * Used to indicate left/right/top/bottom should be inferred from constraints
+ */
+ private static final int VALUE_NOT_SET = Integer.MIN_VALUE;
+
+ private View mBaselineView = null;
+
+ private int mGravity = Gravity.START | Gravity.TOP;
+ private final Rect mContentBounds = new Rect();
+ private final Rect mSelfBounds = new Rect();
+ private int mIgnoreGravity;
+
+ private SortedSet<View> mTopToBottomLeftToRightSet = null;
+
+ private boolean mDirtyHierarchy;
+ private View[] mSortedHorizontalChildren;
+ private View[] mSortedVerticalChildren;
+ private final DependencyGraph mGraph = new DependencyGraph();
+
+ // Compatibility hack. Old versions of the platform had problems
+ // with MeasureSpec value overflow and RelativeLayout was one source of them.
+ // Some apps came to rely on them. :(
+ private boolean mAllowBrokenMeasureSpecs = false;
+ // Compatibility hack. Old versions of the platform would not take
+ // margins and padding into account when generating the height measure spec
+ // for children during the horizontal measure pass.
+ private boolean mMeasureVerticalWithPaddingMargin = false;
+
+ // A default width used for RTL measure pass
+ /**
+ * Value reduced so as not to interfere with View's measurement spec. flags. See:
+ * {@link View#MEASURED_SIZE_MASK}.
+ * {@link View#MEASURED_STATE_TOO_SMALL}.
+ **/
+ private static final int DEFAULT_WIDTH = 0x00010000;
+
+ public RelativeLayout(Context context) {
+ this(context, null);
+ }
+
+ public RelativeLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
+ queryCompatibilityModes(context);
+ }
+
+ private void initFromAttributes(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.RelativeLayout, defStyleAttr, defStyleRes);
+ mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, View.NO_ID);
+ mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity);
+ a.recycle();
+ }
+
+ private void queryCompatibilityModes(Context context) {
+ int version = context.getApplicationInfo().targetSdkVersion;
+ mAllowBrokenMeasureSpecs = version <= Build.VERSION_CODES.JELLY_BEAN_MR1;
+ mMeasureVerticalWithPaddingMargin = version >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ /**
+ * Defines which View is ignored when the gravity is applied. This setting has no
+ * effect if the gravity is <code>Gravity.START | 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
+ */
+ @android.view.RemotableViewMethod
+ public void setIgnoreGravity(int viewId) {
+ mIgnoreGravity = viewId;
+ }
+
+ /**
+ * Describes how the child views are positioned.
+ *
+ * @return the gravity.
+ *
+ * @see #setGravity(int)
+ * @see android.view.Gravity
+ *
+ * @attr ref android.R.styleable#RelativeLayout_gravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Describes how the child views are positioned. Defaults to
+ * <code>Gravity.START | Gravity.TOP</code>.
+ *
+ * <p>Note that since RelativeLayout considers the positioning of each child
+ * relative to one another to be significant, setting gravity will affect
+ * the positioning of all children as a single unit within the parent.
+ * This happens after children have been relatively positioned.</p>
+ *
+ * @param gravity See {@link android.view.Gravity}
+ *
+ * @see #setHorizontalGravity(int)
+ * @see #setVerticalGravity(int)
+ *
+ * @attr ref android.R.styleable#RelativeLayout_gravity
+ */
+ @android.view.RemotableViewMethod
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.START;
+ }
+
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ public void setHorizontalGravity(int horizontalGravity) {
+ final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ 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
+ public void requestLayout() {
+ super.requestLayout();
+ mDirtyHierarchy = true;
+ }
+
+ private void sortChildren() {
+ final int count = getChildCount();
+ if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
+ mSortedVerticalChildren = new View[count];
+ }
+
+ if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
+ mSortedHorizontalChildren = new View[count];
+ }
+
+ final DependencyGraph graph = mGraph;
+ graph.clear();
+
+ for (int i = 0; i < count; i++) {
+ graph.add(getChildAt(i));
+ }
+
+ graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
+ graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mDirtyHierarchy) {
+ mDirtyHierarchy = false;
+ sortChildren();
+ }
+
+ int myWidth = -1;
+ int myHeight = -1;
+
+ int width = 0;
+ int height = 0;
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final 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;
+ }
+
+ View ignore = null;
+ int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+ final boolean horizontalGravity = gravity != Gravity.START && 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;
+
+ boolean offsetHorizontalAxis = false;
+ boolean offsetVerticalAxis = false;
+
+ if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) {
+ ignore = findViewById(mIgnoreGravity);
+ }
+
+ final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY;
+ final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;
+
+ // We need to know our size for doing the correct computation of children positioning in RTL
+ // mode but there is no practical way to get it instead of running the code below.
+ // So, instead of running the code twice, we just set the width to a "default display width"
+ // before the computation and then, as a last pass, we will update their real position with
+ // an offset equals to "DEFAULT_WIDTH - width".
+ final int layoutDirection = getLayoutDirection();
+ if (isLayoutRtl() && myWidth == -1) {
+ myWidth = DEFAULT_WIDTH;
+ }
+
+ View[] views = mSortedHorizontalChildren;
+ int count = views.length;
+
+ for (int i = 0; i < count; i++) {
+ View child = views[i];
+ if (child.getVisibility() != GONE) {
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ int[] rules = params.getRules(layoutDirection);
+
+ applyHorizontalSizeRules(params, myWidth, rules);
+ measureChildHorizontal(child, params, myWidth, myHeight);
+
+ if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
+ offsetHorizontalAxis = true;
+ }
+ }
+ }
+
+ views = mSortedVerticalChildren;
+ count = views.length;
+ final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE) {
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+
+ applyVerticalSizeRules(params, myHeight, child.getBaseline());
+ measureChild(child, params, myWidth, myHeight);
+ if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
+ offsetVerticalAxis = true;
+ }
+
+ if (isWrapContentWidth) {
+ if (isLayoutRtl()) {
+ if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+ width = Math.max(width, myWidth - params.mLeft);
+ } else {
+ width = Math.max(width, myWidth - params.mLeft + params.leftMargin);
+ }
+ } else {
+ if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+ width = Math.max(width, params.mRight);
+ } else {
+ width = Math.max(width, params.mRight + params.rightMargin);
+ }
+ }
+ }
+
+ if (isWrapContentHeight) {
+ if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+ height = Math.max(height, params.mBottom);
+ } else {
+ height = Math.max(height, params.mBottom + params.bottomMargin);
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ // Use the top-start-most laid out view as the baseline. RTL offsets are
+ // applied later, so we can use the left-most edge as the starting edge.
+ View baselineView = null;
+ LayoutParams baselineParams = null;
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE) {
+ final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
+ if (baselineView == null || baselineParams == null
+ || compareLayoutPosition(childParams, baselineParams) < 0) {
+ baselineView = child;
+ baselineParams = childParams;
+ }
+ }
+ }
+ mBaselineView = baselineView;
+
+ if (isWrapContentWidth) {
+ // Width already has left padding in it since it was calculated by looking at
+ // the right of each child view
+ width += mPaddingRight;
+
+ if (mLayoutParams != null && mLayoutParams.width >= 0) {
+ width = Math.max(width, mLayoutParams.width);
+ }
+
+ width = Math.max(width, getSuggestedMinimumWidth());
+ width = resolveSize(width, widthMeasureSpec);
+
+ if (offsetHorizontalAxis) {
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE) {
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+ final int[] rules = params.getRules(layoutDirection);
+ if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
+ centerHorizontal(child, params, width);
+ } else if (rules[ALIGN_PARENT_RIGHT] != 0) {
+ final int childWidth = child.getMeasuredWidth();
+ params.mLeft = width - mPaddingRight - childWidth;
+ params.mRight = params.mLeft + childWidth;
+ }
+ }
+ }
+ }
+ }
+
+ if (isWrapContentHeight) {
+ // Height already has top padding in it since it was calculated by looking at
+ // the bottom of each child view
+ height += mPaddingBottom;
+
+ if (mLayoutParams != null && mLayoutParams.height >= 0) {
+ height = Math.max(height, mLayoutParams.height);
+ }
+
+ height = Math.max(height, getSuggestedMinimumHeight());
+ height = resolveSize(height, heightMeasureSpec);
+
+ if (offsetVerticalAxis) {
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE) {
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+ final int[] rules = params.getRules(layoutDirection);
+ if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
+ centerVertical(child, params, height);
+ } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
+ final int childHeight = child.getMeasuredHeight();
+ params.mTop = height - mPaddingBottom - childHeight;
+ params.mBottom = params.mTop + childHeight;
+ }
+ }
+ }
+ }
+ }
+
+ 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,
+ layoutDirection);
+
+ final int horizontalOffset = contentBounds.left - left;
+ final int verticalOffset = contentBounds.top - top;
+ if (horizontalOffset != 0 || verticalOffset != 0) {
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE && child != ignore) {
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+ if (horizontalGravity) {
+ params.mLeft += horizontalOffset;
+ params.mRight += horizontalOffset;
+ }
+ if (verticalGravity) {
+ params.mTop += verticalOffset;
+ params.mBottom += verticalOffset;
+ }
+ }
+ }
+ }
+ }
+
+ if (isLayoutRtl()) {
+ final int offsetWidth = myWidth - width;
+ for (int i = 0; i < count; i++) {
+ final View child = views[i];
+ if (child.getVisibility() != GONE) {
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+ params.mLeft -= offsetWidth;
+ params.mRight -= offsetWidth;
+ }
+ }
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * @return a negative number if the top of {@code p1} is above the top of
+ * {@code p2} or if they have identical top values and the left of
+ * {@code p1} is to the left of {@code p2}, or a positive number
+ * otherwise
+ */
+ private int compareLayoutPosition(LayoutParams p1, LayoutParams p2) {
+ final int topDiff = p1.mTop - p2.mTop;
+ if (topDiff != 0) {
+ return topDiff;
+ }
+ return p1.mLeft - p2.mLeft;
+ }
+
+ /**
+ * Measure a child. The child should have left, top, right and bottom information
+ * stored in its LayoutParams. If any of these values is VALUE_NOT_SET 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);
+ }
+
+ private void measureChildHorizontal(
+ View child, LayoutParams params, int myWidth, int myHeight) {
+ final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
+ params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
+ myWidth);
+
+ final int childHeightMeasureSpec;
+ if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
+ if (params.height >= 0) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ params.height, MeasureSpec.EXACTLY);
+ } else {
+ // Negative values in a mySize/myWidth/myWidth value in
+ // RelativeLayout measurement is code for, "we got an
+ // unspecified mode in the RelativeLayout's measure spec."
+ // Carry it forward.
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ } else {
+ final int maxHeight;
+ if (mMeasureVerticalWithPaddingMargin) {
+ maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
+ - params.topMargin - params.bottomMargin);
+ } else {
+ maxHeight = Math.max(0, myHeight);
+ }
+
+ final int heightMode;
+ if (params.height == LayoutParams.MATCH_PARENT) {
+ heightMode = MeasureSpec.EXACTLY;
+ } else {
+ heightMode = MeasureSpec.AT_MOST;
+ }
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Get a measure spec that accounts for all of the constraints on this view.
+ * This includes size constraints 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;
+
+ // Negative values in a mySize value in RelativeLayout
+ // measurement is code for, "we got an unspecified mode in the
+ // RelativeLayout's measure spec."
+ final boolean isUnspecified = mySize < 0;
+ if (isUnspecified && !mAllowBrokenMeasureSpecs) {
+ if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
+ // Constraints fixed both edges, so child has an exact size.
+ childSpecSize = Math.max(0, childEnd - childStart);
+ childSpecMode = MeasureSpec.EXACTLY;
+ } else if (childSize >= 0) {
+ // The child specified an exact size.
+ childSpecSize = childSize;
+ childSpecMode = MeasureSpec.EXACTLY;
+ } else {
+ // Allow the child to be whatever size it wants.
+ childSpecSize = 0;
+ childSpecMode = MeasureSpec.UNSPECIFIED;
+ }
+
+ return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
+ }
+
+ // 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 == VALUE_NOT_SET) {
+ tempStart = startPadding + startMargin;
+ }
+ if (tempEnd == VALUE_NOT_SET) {
+ tempEnd = mySize - endPadding - endMargin;
+ }
+
+ // Figure out maximum size available to this view
+ final int maxAvailable = tempEnd - tempStart;
+
+ if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
+ // Constraints fixed both edges, so child must be an exact size.
+ childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
+ childSpecSize = Math.max(0, maxAvailable);
+ } else {
+ if (childSize >= 0) {
+ // Child wanted an exact size. Give as much as possible.
+ childSpecMode = MeasureSpec.EXACTLY;
+
+ if (maxAvailable >= 0) {
+ // We have a maximum size in this dimension.
+ childSpecSize = Math.min(maxAvailable, childSize);
+ } else {
+ // We can grow in this dimension.
+ childSpecSize = childSize;
+ }
+ } else if (childSize == LayoutParams.MATCH_PARENT) {
+ // Child wanted to be as big as possible. Give all available
+ // space.
+ childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
+ childSpecSize = Math.max(0, 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 maximum 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);
+ }
+
+ private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
+ boolean wrapContent) {
+
+ final int layoutDirection = getLayoutDirection();
+ int[] rules = params.getRules(layoutDirection);
+
+ if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
+ // Right is fixed, but left varies
+ params.mLeft = params.mRight - child.getMeasuredWidth();
+ } else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
+ // Left is fixed, but right varies
+ params.mRight = params.mLeft + child.getMeasuredWidth();
+ } else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
+ // Both left and right vary
+ if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
+ if (!wrapContent) {
+ centerHorizontal(child, params, myWidth);
+ } else {
+ positionAtEdge(child, params, myWidth);
+ }
+ return true;
+ } else {
+ // This is the default case. For RTL we start from the right and for LTR we start
+ // from the left. This will give LEFT/TOP for LTR and RIGHT/TOP for RTL.
+ positionAtEdge(child, params, myWidth);
+ }
+ }
+ return rules[ALIGN_PARENT_END] != 0;
+ }
+
+ private void positionAtEdge(View child, LayoutParams params, int myWidth) {
+ if (isLayoutRtl()) {
+ params.mRight = myWidth - mPaddingRight - params.rightMargin;
+ params.mLeft = params.mRight - child.getMeasuredWidth();
+ } else {
+ params.mLeft = mPaddingLeft + params.leftMargin;
+ params.mRight = params.mLeft + child.getMeasuredWidth();
+ }
+ }
+
+ private boolean positionChildVertical(View child, LayoutParams params, int myHeight,
+ boolean wrapContent) {
+
+ int[] rules = params.getRules();
+
+ if (params.mTop == VALUE_NOT_SET && params.mBottom != VALUE_NOT_SET) {
+ // Bottom is fixed, but top varies
+ params.mTop = params.mBottom - child.getMeasuredHeight();
+ } else if (params.mTop != VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
+ // Top is fixed, but bottom varies
+ params.mBottom = params.mTop + child.getMeasuredHeight();
+ } else if (params.mTop == VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
+ // Both top and bottom vary
+ if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
+ if (!wrapContent) {
+ centerVertical(child, params, myHeight);
+ } else {
+ params.mTop = mPaddingTop + params.topMargin;
+ params.mBottom = params.mTop + child.getMeasuredHeight();
+ }
+ return true;
+ } else {
+ params.mTop = mPaddingTop + params.topMargin;
+ params.mBottom = params.mTop + child.getMeasuredHeight();
+ }
+ }
+ return rules[ALIGN_PARENT_BOTTOM] != 0;
+ }
+
+ private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
+ RelativeLayout.LayoutParams anchorParams;
+
+ // VALUE_NOT_SET indicates a "soft requirement" in that direction. For example:
+ // left=10, right=VALUE_NOT_SET means the view must start at 10, but can go as far as it
+ // wants to the right
+ // left=VALUE_NOT_SET, 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 = VALUE_NOT_SET;
+ childParams.mRight = VALUE_NOT_SET;
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ private void applyVerticalSizeRules(LayoutParams childParams, int myHeight, int myBaseline) {
+ final int[] rules = childParams.getRules();
+
+ // Baseline alignment overrides any explicitly specified top or bottom.
+ int baselineOffset = getRelatedViewBaselineOffset(rules);
+ if (baselineOffset != -1) {
+ if (myBaseline != -1) {
+ baselineOffset -= myBaseline;
+ }
+ childParams.mTop = baselineOffset;
+ childParams.mBottom = VALUE_NOT_SET;
+ return;
+ }
+
+ RelativeLayout.LayoutParams anchorParams;
+
+ childParams.mTop = VALUE_NOT_SET;
+ childParams.mBottom = VALUE_NOT_SET;
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ private View getRelatedView(int[] rules, int relation) {
+ int id = rules[relation];
+ if (id != 0) {
+ DependencyGraph.Node node = mGraph.mKeyNodes.get(id);
+ if (node == null) return null;
+ View v = node.view;
+
+ // Find the first non-GONE view up the chain
+ while (v.getVisibility() == View.GONE) {
+ rules = ((LayoutParams) v.getLayoutParams()).getRules(v.getLayoutDirection());
+ node = mGraph.mKeyNodes.get((rules[relation]));
+ // ignore self dependency. for more info look in git commit: da3003
+ if (node == null || v == node.view) return null;
+ v = node.view;
+ }
+
+ 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 getRelatedViewBaselineOffset(int[] rules) {
+ final View v = getRelatedView(rules, ALIGN_BASELINE);
+ if (v != null) {
+ final int baseline = v.getBaseline();
+ if (baseline != -1) {
+ final ViewGroup.LayoutParams params = v.getLayoutParams();
+ if (params instanceof LayoutParams) {
+ final LayoutParams anchorParams = (LayoutParams) v.getLayoutParams();
+ return anchorParams.mTop + baseline;
+ }
+ }
+ }
+ return -1;
+ }
+
+ private static 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 static 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.
+ final 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 lp) {
+ if (sPreserveMarginParamsInLayoutParamConversion) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ }
+ }
+ return new LayoutParams(lp);
+ }
+
+ /** @hide */
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ if (mTopToBottomLeftToRightSet == null) {
+ mTopToBottomLeftToRightSet = new TreeSet<View>(new TopToBottomLeftToRightComparator());
+ }
+
+ // sort children top-to-bottom and left-to-right
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ mTopToBottomLeftToRightSet.add(getChildAt(i));
+ }
+
+ for (View view : mTopToBottomLeftToRightSet) {
+ if (view.getVisibility() == View.VISIBLE
+ && view.dispatchPopulateAccessibilityEvent(event)) {
+ mTopToBottomLeftToRightSet.clear();
+ return true;
+ }
+ }
+
+ mTopToBottomLeftToRightSet.clear();
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return RelativeLayout.class.getName();
+ }
+
+ /**
+ * Compares two views in left-to-right and top-to-bottom fashion.
+ */
+ private class TopToBottomLeftToRightComparator implements Comparator<View> {
+ public int compare(View first, View second) {
+ // top - bottom
+ int topDifference = first.getTop() - second.getTop();
+ if (topDifference != 0) {
+ return topDifference;
+ }
+ // left - right
+ int leftDifference = first.getLeft() - second.getLeft();
+ if (leftDifference != 0) {
+ return leftDifference;
+ }
+ // break tie by height
+ int heightDiference = first.getHeight() - second.getHeight();
+ if (heightDiference != 0) {
+ return heightDiference;
+ }
+ // break tie by width
+ int widthDiference = first.getWidth() - second.getWidth();
+ if (widthDiference != 0) {
+ return widthDiference;
+ }
+ return 0;
+ }
+ }
+
+ /**
+ * Specifies how a view is positioned within a {@link RelativeLayout}.
+ * The relative layout containing the view uses the value of these layout parameters to
+ * determine where to position the view on the screen. If the view is not contained
+ * within a relative layout, these attributes are ignored.
+ *
+ * See the <a href=“https://developer.android.com/guide/topics/ui/layout/relative.html”>
+ * Relative Layout</a> guide for example code demonstrating how to use relative layout’s
+ * layout parameters in a layout XML.
+ *
+ * To learn more about layout parameters and how they differ from typical view attributes,
+ * see the <a href=“https://developer.android.com/guide/topics/ui/declaring-layout.html#attributes”>
+ * Layouts guide</a>.
+ *
+ *
+ * @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
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toStartOf
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toEndOf
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignStart
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignEnd
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentStart
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentEnd
+ */
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ @ViewDebug.ExportedProperty(category = "layout", resolveId = true, indexMapping = {
+ @ViewDebug.IntToString(from = ABOVE, to = "above"),
+ @ViewDebug.IntToString(from = ALIGN_BASELINE, to = "alignBaseline"),
+ @ViewDebug.IntToString(from = ALIGN_BOTTOM, to = "alignBottom"),
+ @ViewDebug.IntToString(from = ALIGN_LEFT, to = "alignLeft"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_BOTTOM, to = "alignParentBottom"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_LEFT, to = "alignParentLeft"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_RIGHT, to = "alignParentRight"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_TOP, to = "alignParentTop"),
+ @ViewDebug.IntToString(from = ALIGN_RIGHT, to = "alignRight"),
+ @ViewDebug.IntToString(from = ALIGN_TOP, to = "alignTop"),
+ @ViewDebug.IntToString(from = BELOW, to = "below"),
+ @ViewDebug.IntToString(from = CENTER_HORIZONTAL, to = "centerHorizontal"),
+ @ViewDebug.IntToString(from = CENTER_IN_PARENT, to = "center"),
+ @ViewDebug.IntToString(from = CENTER_VERTICAL, to = "centerVertical"),
+ @ViewDebug.IntToString(from = LEFT_OF, to = "leftOf"),
+ @ViewDebug.IntToString(from = RIGHT_OF, to = "rightOf"),
+ @ViewDebug.IntToString(from = ALIGN_START, to = "alignStart"),
+ @ViewDebug.IntToString(from = ALIGN_END, to = "alignEnd"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_START, to = "alignParentStart"),
+ @ViewDebug.IntToString(from = ALIGN_PARENT_END, to = "alignParentEnd"),
+ @ViewDebug.IntToString(from = START_OF, to = "startOf"),
+ @ViewDebug.IntToString(from = END_OF, to = "endOf")
+ }, mapping = {
+ @ViewDebug.IntToString(from = TRUE, to = "true"),
+ @ViewDebug.IntToString(from = 0, to = "false/NO_ID")
+ })
+
+ private int[] mRules = new int[VERB_COUNT];
+ private int[] mInitialRules = new int[VERB_COUNT];
+
+ private int mLeft, mTop, mRight, mBottom;
+
+ /**
+ * Whether this view had any relative rules modified following the most
+ * recent resolution of layout direction.
+ */
+ private boolean mNeedsLayoutResolution;
+
+ private boolean mRulesChanged = false;
+ private boolean mIsRtlCompatibilityMode = false;
+
+ /**
+ * When true, uses the parent as the anchor if the anchor doesn't exist or if
+ * the anchor's visibility is GONE.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ 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 targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
+ mIsRtlCompatibilityMode = (targetSdkVersion < JELLY_BEAN_MR1 ||
+ !c.getApplicationInfo().hasRtlSupport());
+
+ final int[] rules = mRules;
+ //noinspection MismatchedReadAndWriteOfArray
+ final int[] initialRules = mInitialRules;
+
+ 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;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toStartOf:
+ rules[START_OF] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toEndOf:
+ rules[END_OF] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignStart:
+ rules[ALIGN_START] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignEnd:
+ rules[ALIGN_END] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentStart:
+ rules[ALIGN_PARENT_START] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentEnd:
+ rules[ALIGN_PARENT_END] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ }
+ }
+ mRulesChanged = true;
+ System.arraycopy(rules, LEFT_OF, initialRules, LEFT_OF, VERB_COUNT);
+
+ 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);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height, margin values, and rules
+ * of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ super(source);
+
+ this.mIsRtlCompatibilityMode = source.mIsRtlCompatibilityMode;
+ this.mRulesChanged = source.mRulesChanged;
+ this.alignWithParent = source.alignWithParent;
+
+ System.arraycopy(source.mRules, LEFT_OF, this.mRules, LEFT_OF, VERB_COUNT);
+ System.arraycopy(
+ source.mInitialRules, LEFT_OF, this.mInitialRules, LEFT_OF, VERB_COUNT);
+ }
+
+ @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.
+ * <p>
+ * This method should only be used for verbs that don't refer to a
+ * sibling (ex. {@link #ALIGN_RIGHT}) or take a boolean
+ * value ({@link #TRUE} for true or 0 for false). To
+ * specify a verb that takes a subject, use {@link #addRule(int, int)}.
+ * <p>
+ * If the rule is relative to the layout direction (ex.
+ * {@link #ALIGN_PARENT_START}), then the layout direction must be
+ * resolved using {@link #resolveLayoutDirection(int)} before calling
+ * {@link #getRule(int)} an absolute rule (ex.
+ * {@link #ALIGN_PARENT_LEFT}.
+ *
+ * @param verb a layout verb, such as {@link #ALIGN_PARENT_LEFT}
+ * @see #addRule(int, int)
+ * @see #removeRule(int)
+ * @see #getRule(int)
+ */
+ public void addRule(int verb) {
+ addRule(verb, TRUE);
+ }
+
+ /**
+ * Adds a layout rule to be interpreted by the RelativeLayout.
+ * <p>
+ * Use this for verbs that refer to a sibling (ex.
+ * {@link #ALIGN_RIGHT}) or take a boolean value (ex.
+ * {@link #CENTER_IN_PARENT}).
+ * <p>
+ * If the rule is relative to the layout direction (ex.
+ * {@link #START_OF}), then the layout direction must be resolved using
+ * {@link #resolveLayoutDirection(int)} before calling
+ * {@link #getRule(int)} with an absolute rule (ex. {@link #LEFT_OF}.
+ *
+ * @param verb a layout verb, such as {@link #ALIGN_RIGHT}
+ * @param subject the ID of another view to use as an anchor, or a
+ * boolean value (represented as {@link #TRUE} for true
+ * or 0 for false)
+ * @see #addRule(int)
+ * @see #removeRule(int)
+ * @see #getRule(int)
+ */
+ public void addRule(int verb, int subject) {
+ // If we're removing a relative rule, we'll need to force layout
+ // resolution the next time it's requested.
+ if (!mNeedsLayoutResolution && isRelativeRule(verb)
+ && mInitialRules[verb] != 0 && subject == 0) {
+ mNeedsLayoutResolution = true;
+ }
+
+ mRules[verb] = subject;
+ mInitialRules[verb] = subject;
+ mRulesChanged = true;
+ }
+
+ /**
+ * Removes a layout rule to be interpreted by the RelativeLayout.
+ * <p>
+ * If the rule is relative to the layout direction (ex.
+ * {@link #START_OF}, {@link #ALIGN_PARENT_START}, etc.) then the
+ * layout direction must be resolved using
+ * {@link #resolveLayoutDirection(int)} before before calling
+ * {@link #getRule(int)} with an absolute rule (ex. {@link #LEFT_OF}.
+ *
+ * @param verb One of the verbs defined by
+ * {@link android.widget.RelativeLayout RelativeLayout}, such as
+ * ALIGN_WITH_PARENT_LEFT.
+ * @see #addRule(int)
+ * @see #addRule(int, int)
+ * @see #getRule(int)
+ */
+ public void removeRule(int verb) {
+ addRule(verb, 0);
+ }
+
+ /**
+ * Returns the layout rule associated with a specific verb.
+ *
+ * @param verb one of the verbs defined by {@link RelativeLayout}, such
+ * as ALIGN_WITH_PARENT_LEFT
+ * @return the id of another view to use as an anchor, a boolean value
+ * (represented as {@link RelativeLayout#TRUE} for true
+ * or 0 for false), or -1 for verbs that don't refer to another
+ * sibling (for example, ALIGN_WITH_PARENT_BOTTOM)
+ * @see #addRule(int)
+ * @see #addRule(int, int)
+ */
+ public int getRule(int verb) {
+ return mRules[verb];
+ }
+
+ private boolean hasRelativeRules() {
+ return (mInitialRules[START_OF] != 0 || mInitialRules[END_OF] != 0 ||
+ mInitialRules[ALIGN_START] != 0 || mInitialRules[ALIGN_END] != 0 ||
+ mInitialRules[ALIGN_PARENT_START] != 0 || mInitialRules[ALIGN_PARENT_END] != 0);
+ }
+
+ private boolean isRelativeRule(int rule) {
+ return rule == START_OF || rule == END_OF
+ || rule == ALIGN_START || rule == ALIGN_END
+ || rule == ALIGN_PARENT_START || rule == ALIGN_PARENT_END;
+ }
+
+ // The way we are resolving rules depends on the layout direction and if we are pre JB MR1
+ // or not.
+ //
+ // If we are pre JB MR1 (said as "RTL compatibility mode"), "left"/"right" rules are having
+ // predominance over any "start/end" rules that could have been defined. A special case:
+ // if no "left"/"right" rule has been defined and "start"/"end" rules are defined then we
+ // resolve those "start"/"end" rules to "left"/"right" respectively.
+ //
+ // If we are JB MR1+, then "start"/"end" rules are having predominance over "left"/"right"
+ // rules. If no "start"/"end" rule is defined then we use "left"/"right" rules.
+ //
+ // In all cases, the result of the resolution should clear the "start"/"end" rules to leave
+ // only the "left"/"right" rules at the end.
+ private void resolveRules(int layoutDirection) {
+ final boolean isLayoutRtl = (layoutDirection == View.LAYOUT_DIRECTION_RTL);
+
+ // Reset to initial state
+ System.arraycopy(mInitialRules, LEFT_OF, mRules, LEFT_OF, VERB_COUNT);
+
+ // Apply rules depending on direction and if we are in RTL compatibility mode
+ if (mIsRtlCompatibilityMode) {
+ if (mRules[ALIGN_START] != 0) {
+ if (mRules[ALIGN_LEFT] == 0) {
+ // "left" rule is not defined but "start" rule is: use the "start" rule as
+ // the "left" rule
+ mRules[ALIGN_LEFT] = mRules[ALIGN_START];
+ }
+ mRules[ALIGN_START] = 0;
+ }
+
+ if (mRules[ALIGN_END] != 0) {
+ if (mRules[ALIGN_RIGHT] == 0) {
+ // "right" rule is not defined but "end" rule is: use the "end" rule as the
+ // "right" rule
+ mRules[ALIGN_RIGHT] = mRules[ALIGN_END];
+ }
+ mRules[ALIGN_END] = 0;
+ }
+
+ if (mRules[START_OF] != 0) {
+ if (mRules[LEFT_OF] == 0) {
+ // "left" rule is not defined but "start" rule is: use the "start" rule as
+ // the "left" rule
+ mRules[LEFT_OF] = mRules[START_OF];
+ }
+ mRules[START_OF] = 0;
+ }
+
+ if (mRules[END_OF] != 0) {
+ if (mRules[RIGHT_OF] == 0) {
+ // "right" rule is not defined but "end" rule is: use the "end" rule as the
+ // "right" rule
+ mRules[RIGHT_OF] = mRules[END_OF];
+ }
+ mRules[END_OF] = 0;
+ }
+
+ if (mRules[ALIGN_PARENT_START] != 0) {
+ if (mRules[ALIGN_PARENT_LEFT] == 0) {
+ // "left" rule is not defined but "start" rule is: use the "start" rule as
+ // the "left" rule
+ mRules[ALIGN_PARENT_LEFT] = mRules[ALIGN_PARENT_START];
+ }
+ mRules[ALIGN_PARENT_START] = 0;
+ }
+
+ if (mRules[ALIGN_PARENT_END] != 0) {
+ if (mRules[ALIGN_PARENT_RIGHT] == 0) {
+ // "right" rule is not defined but "end" rule is: use the "end" rule as the
+ // "right" rule
+ mRules[ALIGN_PARENT_RIGHT] = mRules[ALIGN_PARENT_END];
+ }
+ mRules[ALIGN_PARENT_END] = 0;
+ }
+ } else {
+ // JB MR1+ case
+ if ((mRules[ALIGN_START] != 0 || mRules[ALIGN_END] != 0) &&
+ (mRules[ALIGN_LEFT] != 0 || mRules[ALIGN_RIGHT] != 0)) {
+ // "start"/"end" rules take precedence over "left"/"right" rules
+ mRules[ALIGN_LEFT] = 0;
+ mRules[ALIGN_RIGHT] = 0;
+ }
+ if (mRules[ALIGN_START] != 0) {
+ // "start" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? ALIGN_RIGHT : ALIGN_LEFT] = mRules[ALIGN_START];
+ mRules[ALIGN_START] = 0;
+ }
+ if (mRules[ALIGN_END] != 0) {
+ // "end" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? ALIGN_LEFT : ALIGN_RIGHT] = mRules[ALIGN_END];
+ mRules[ALIGN_END] = 0;
+ }
+
+ if ((mRules[START_OF] != 0 || mRules[END_OF] != 0) &&
+ (mRules[LEFT_OF] != 0 || mRules[RIGHT_OF] != 0)) {
+ // "start"/"end" rules take precedence over "left"/"right" rules
+ mRules[LEFT_OF] = 0;
+ mRules[RIGHT_OF] = 0;
+ }
+ if (mRules[START_OF] != 0) {
+ // "start" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? RIGHT_OF : LEFT_OF] = mRules[START_OF];
+ mRules[START_OF] = 0;
+ }
+ if (mRules[END_OF] != 0) {
+ // "end" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? LEFT_OF : RIGHT_OF] = mRules[END_OF];
+ mRules[END_OF] = 0;
+ }
+
+ if ((mRules[ALIGN_PARENT_START] != 0 || mRules[ALIGN_PARENT_END] != 0) &&
+ (mRules[ALIGN_PARENT_LEFT] != 0 || mRules[ALIGN_PARENT_RIGHT] != 0)) {
+ // "start"/"end" rules take precedence over "left"/"right" rules
+ mRules[ALIGN_PARENT_LEFT] = 0;
+ mRules[ALIGN_PARENT_RIGHT] = 0;
+ }
+ if (mRules[ALIGN_PARENT_START] != 0) {
+ // "start" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? ALIGN_PARENT_RIGHT : ALIGN_PARENT_LEFT] = mRules[ALIGN_PARENT_START];
+ mRules[ALIGN_PARENT_START] = 0;
+ }
+ if (mRules[ALIGN_PARENT_END] != 0) {
+ // "end" rule resolved to "left" or "right" depending on the direction
+ mRules[isLayoutRtl ? ALIGN_PARENT_LEFT : ALIGN_PARENT_RIGHT] = mRules[ALIGN_PARENT_END];
+ mRules[ALIGN_PARENT_END] = 0;
+ }
+ }
+
+ mRulesChanged = false;
+ mNeedsLayoutResolution = false;
+ }
+
+ /**
+ * 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. If there are relative rules defined (*_START / *_END), they will be resolved
+ * depending on the layout direction.
+ *
+ * @param layoutDirection the direction of the layout.
+ * Should be either {@link View#LAYOUT_DIRECTION_LTR}
+ * or {@link View#LAYOUT_DIRECTION_RTL}
+ * @return the supported rules
+ * @see #addRule(int, int)
+ *
+ * @hide
+ */
+ public int[] getRules(int layoutDirection) {
+ resolveLayoutDirection(layoutDirection);
+ return mRules;
+ }
+
+ /**
+ * 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. There will be no resolution of relative rules done.
+ *
+ * @return the supported rules
+ * @see #addRule(int, int)
+ */
+ public int[] getRules() {
+ return mRules;
+ }
+
+ /**
+ * This will be called by {@link android.view.View#requestLayout()} to
+ * resolve layout parameters that are relative to the layout direction.
+ * <p>
+ * After this method is called, any rules using layout-relative verbs
+ * (ex. {@link #START_OF}) previously added via {@link #addRule(int)}
+ * may only be accessed via their resolved absolute verbs (ex.
+ * {@link #LEFT_OF}).
+ */
+ @Override
+ public void resolveLayoutDirection(int layoutDirection) {
+ if (shouldResolveLayoutDirection(layoutDirection)) {
+ resolveRules(layoutDirection);
+ }
+
+ // This will set the layout direction.
+ super.resolveLayoutDirection(layoutDirection);
+ }
+
+ private boolean shouldResolveLayoutDirection(int layoutDirection) {
+ return (mNeedsLayoutResolution || hasRelativeRules())
+ && (mRulesChanged || layoutDirection != getLayoutDirection());
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("layout:alignWithParent", alignWithParent);
+ }
+ }
+
+ private static class DependencyGraph {
+ /**
+ * List of all views in the graph.
+ */
+ private ArrayList<Node> mNodes = new ArrayList<Node>();
+
+ /**
+ * List of nodes in the graph. Each node is identified by its
+ * view id (see View#getId()).
+ */
+ private SparseArray<Node> mKeyNodes = new SparseArray<Node>();
+
+ /**
+ * Temporary data structure used to build the list of roots
+ * for this graph.
+ */
+ private ArrayDeque<Node> mRoots = new ArrayDeque<Node>();
+
+ /**
+ * Clears the graph.
+ */
+ void clear() {
+ final ArrayList<Node> nodes = mNodes;
+ final int count = nodes.size();
+
+ for (int i = 0; i < count; i++) {
+ nodes.get(i).release();
+ }
+ nodes.clear();
+
+ mKeyNodes.clear();
+ mRoots.clear();
+ }
+
+ /**
+ * Adds a view to the graph.
+ *
+ * @param view The view to be added as a node to the graph.
+ */
+ void add(View view) {
+ final int id = view.getId();
+ final Node node = Node.acquire(view);
+
+ if (id != View.NO_ID) {
+ mKeyNodes.put(id, node);
+ }
+
+ mNodes.add(node);
+ }
+
+ /**
+ * Builds a sorted list of views. The sorting order depends on the dependencies
+ * between the view. For instance, if view C needs view A to be processed first
+ * and view A needs view B to be processed first, the dependency graph
+ * is: B -> A -> C. The sorted array will contain views B, A and C in this order.
+ *
+ * @param sorted The sorted list of views. The length of this array must
+ * be equal to getChildCount().
+ * @param rules The list of rules to take into account.
+ */
+ void getSortedViews(View[] sorted, int... rules) {
+ final ArrayDeque<Node> roots = findRoots(rules);
+ int index = 0;
+
+ Node node;
+ while ((node = roots.pollLast()) != null) {
+ final View view = node.view;
+ final int key = view.getId();
+
+ sorted[index++] = view;
+
+ final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
+ final int count = dependents.size();
+ for (int i = 0; i < count; i++) {
+ final Node dependent = dependents.keyAt(i);
+ final SparseArray<Node> dependencies = dependent.dependencies;
+
+ dependencies.remove(key);
+ if (dependencies.size() == 0) {
+ roots.add(dependent);
+ }
+ }
+ }
+
+ if (index < sorted.length) {
+ throw new IllegalStateException("Circular dependencies cannot exist"
+ + " in RelativeLayout");
+ }
+ }
+
+ /**
+ * Finds the roots of the graph. A root is a node with no dependency and
+ * with [0..n] dependents.
+ *
+ * @param rulesFilter The list of rules to consider when building the
+ * dependencies
+ *
+ * @return A list of node, each being a root of the graph
+ */
+ private ArrayDeque<Node> findRoots(int[] rulesFilter) {
+ final SparseArray<Node> keyNodes = mKeyNodes;
+ final ArrayList<Node> nodes = mNodes;
+ final int count = nodes.size();
+
+ // Find roots can be invoked several times, so make sure to clear
+ // all dependents and dependencies before running the algorithm
+ for (int i = 0; i < count; i++) {
+ final Node node = nodes.get(i);
+ node.dependents.clear();
+ node.dependencies.clear();
+ }
+
+ // Builds up the dependents and dependencies for each node of the graph
+ for (int i = 0; i < count; i++) {
+ final Node node = nodes.get(i);
+
+ final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
+ final int[] rules = layoutParams.mRules;
+ final int rulesCount = rulesFilter.length;
+
+ // Look only the the rules passed in parameter, this way we build only the
+ // dependencies for a specific set of rules
+ for (int j = 0; j < rulesCount; j++) {
+ final int rule = rules[rulesFilter[j]];
+ if (rule > 0) {
+ // The node this node depends on
+ final Node dependency = keyNodes.get(rule);
+ // Skip unknowns and self dependencies
+ if (dependency == null || dependency == node) {
+ continue;
+ }
+ // Add the current node as a dependent
+ dependency.dependents.put(node, this);
+ // Add a dependency to the current node
+ node.dependencies.put(rule, dependency);
+ }
+ }
+ }
+
+ final ArrayDeque<Node> roots = mRoots;
+ roots.clear();
+
+ // Finds all the roots in the graph: all nodes with no dependencies
+ for (int i = 0; i < count; i++) {
+ final Node node = nodes.get(i);
+ if (node.dependencies.size() == 0) roots.addLast(node);
+ }
+
+ return roots;
+ }
+
+ /**
+ * A node in the dependency graph. A node is a view, its list of dependencies
+ * and its list of dependents.
+ *
+ * A node with no dependent is considered a root of the graph.
+ */
+ static class Node {
+ /**
+ * The view representing this node in the layout.
+ */
+ View view;
+
+ /**
+ * The list of dependents for this node; a dependent is a node
+ * that needs this node to be processed first.
+ */
+ final ArrayMap<Node, DependencyGraph> dependents =
+ new ArrayMap<Node, DependencyGraph>();
+
+ /**
+ * The list of dependencies for this node.
+ */
+ final SparseArray<Node> dependencies = new SparseArray<Node>();
+
+ /*
+ * START POOL IMPLEMENTATION
+ */
+ // The pool is static, so all nodes instances are shared across
+ // activities, that's why we give it a rather high limit
+ private static final int POOL_LIMIT = 100;
+ private static final SynchronizedPool<Node> sPool =
+ new SynchronizedPool<Node>(POOL_LIMIT);
+
+ static Node acquire(View view) {
+ Node node = sPool.acquire();
+ if (node == null) {
+ node = new Node();
+ }
+ node.view = view;
+ return node;
+ }
+
+ void release() {
+ view = null;
+ dependents.clear();
+ dependencies.clear();
+
+ sPool.release(this);
+ }
+ /*
+ * END POOL IMPLEMENTATION
+ */
+ }
+ }
+}
diff --git a/android/widget/RemoteViews.java b/android/widget/RemoteViews.java
new file mode 100644
index 0000000..bc85fad
--- /dev/null
+++ b/android/widget/RemoteViews.java
@@ -0,0 +1,3875 @@
+/*
+ * 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.ColorInt;
+import android.annotation.DimenRes;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.LayoutInflater.Filter;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.android.internal.R;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.internal.util.Preconditions;
+
+import libcore.util.Objects;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Stack;
+import java.util.concurrent.Executor;
+
+/**
+ * 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 intent extra that contains the appWidgetId.
+ * @hide
+ */
+ static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId";
+
+ /**
+ * Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and
+ * {@link #RemoteViews(RemoteViews, RemoteViews)}.
+ */
+ private static final int MAX_NESTED_VIEWS = 10;
+
+ // The unique identifiers for each custom {@link Action}.
+ private static final int SET_ON_CLICK_PENDING_INTENT_TAG = 1;
+ private static final int REFLECTION_ACTION_TAG = 2;
+ private static final int SET_DRAWABLE_PARAMETERS_TAG = 3;
+ private static final int VIEW_GROUP_ACTION_ADD_TAG = 4;
+ private static final int SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG = 5;
+ private static final int SET_EMPTY_VIEW_ACTION_TAG = 6;
+ private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7;
+ private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8;
+ private static final int SET_ON_CLICK_FILL_IN_INTENT_TAG = 9;
+ private static final int SET_REMOTE_VIEW_ADAPTER_INTENT_TAG = 10;
+ private static final int TEXT_VIEW_DRAWABLE_ACTION_TAG = 11;
+ private static final int BITMAP_REFLECTION_ACTION_TAG = 12;
+ private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13;
+ private static final int VIEW_PADDING_ACTION_TAG = 14;
+ private static final int SET_REMOTE_VIEW_ADAPTER_LIST_TAG = 15;
+ private static final int TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG = 17;
+ private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18;
+ private static final int LAYOUT_PARAM_ACTION_TAG = 19;
+ private static final int OVERRIDE_TEXT_COLORS_TAG = 20;
+
+ /**
+ * Application that hosts the remote views.
+ *
+ * @hide
+ */
+ private ApplicationInfo mApplication;
+
+ /**
+ * The resource ID of the layout file. (Added to the parcel)
+ */
+ private final int mLayoutId;
+
+ /**
+ * An array of actions to perform on the view tree once it has been
+ * inflated
+ */
+ private ArrayList<Action> mActions;
+
+ /**
+ * Maps bitmaps to unique indicies to avoid Bitmap duplication.
+ */
+ private BitmapCache mBitmapCache;
+
+ /**
+ * Indicates whether or not this RemoteViews object is contained as a child of any other
+ * RemoteViews.
+ */
+ private boolean mIsRoot = true;
+
+ /**
+ * Whether reapply is disallowed on this remoteview. This maybe be true if some actions modify
+ * the layout in a way that isn't recoverable, since views are being removed.
+ */
+ private boolean mReapplyDisallowed;
+
+ /**
+ * Constants to whether or not this RemoteViews is composed of a landscape and portrait
+ * RemoteViews.
+ */
+ private static final int MODE_NORMAL = 0;
+ private static final int MODE_HAS_LANDSCAPE_AND_PORTRAIT = 1;
+
+ /**
+ * Used in conjunction with the special constructor
+ * {@link #RemoteViews(RemoteViews, RemoteViews)} to keep track of the landscape and portrait
+ * RemoteViews.
+ */
+ private RemoteViews mLandscape = null;
+ private RemoteViews mPortrait = null;
+
+ /**
+ * This flag indicates whether this RemoteViews object is being created from a
+ * RemoteViewsService for use as a child of a widget collection. This flag is used
+ * to determine whether or not certain features are available, in particular,
+ * setting on click extras and setting on click pending intents. The former is enabled,
+ * and the latter disabled when this flag is true.
+ */
+ private boolean mIsWidgetCollectionChild = false;
+
+ private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = new OnClickHandler();
+
+ private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>();
+
+ /**
+ * This key is used to perform lookups in sMethods without causing allocations.
+ */
+ private static final MethodKey sLookupKey = new MethodKey();
+
+ /**
+ * @hide
+ */
+ public void setRemoteInputs(int viewId, RemoteInput[] remoteInputs) {
+ mActions.add(new SetRemoteInputsAction(viewId, remoteInputs));
+ }
+
+ /**
+ * Reduces all images and ensures that they are all below the given sizes.
+ *
+ * @param maxWidth the maximum width allowed
+ * @param maxHeight the maximum height allowed
+ *
+ * @hide
+ */
+ public void reduceImageSizes(int maxWidth, int maxHeight) {
+ ArrayList<Bitmap> cache = mBitmapCache.mBitmaps;
+ for (int i = 0; i < cache.size(); i++) {
+ Bitmap bitmap = cache.get(i);
+ cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
+ }
+ }
+
+ /**
+ * Override all text colors in this layout and replace them by the given text color.
+ *
+ * @param textColor The color to use.
+ *
+ * @hide
+ */
+ public void overrideTextColors(int textColor) {
+ addAction(new OverrideTextColorsAction(textColor));
+ }
+
+ /**
+ * Set that it is disallowed to reapply another remoteview with the same layout as this view.
+ * This should be done if an action is destroying the view tree of the base layout.
+ *
+ * @hide
+ */
+ public void setReapplyDisallowed() {
+ mReapplyDisallowed = true;
+ }
+
+ /**
+ * @return Whether it is disallowed to reapply another remoteview with the same layout as this
+ * view. True if this remoteview has actions that destroyed view tree of the base layout.
+ *
+ * @hide
+ */
+ public boolean isReapplyDisallowed() {
+ return mReapplyDisallowed;
+ }
+
+ /**
+ * Stores information related to reflection method lookup.
+ */
+ static class MethodKey {
+ public Class targetClass;
+ public Class paramClass;
+ public String methodName;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof MethodKey)) {
+ return false;
+ }
+ MethodKey p = (MethodKey) o;
+ return Objects.equal(p.targetClass, targetClass)
+ && Objects.equal(p.paramClass, paramClass)
+ && Objects.equal(p.methodName, methodName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(targetClass) ^ Objects.hashCode(paramClass)
+ ^ Objects.hashCode(methodName);
+ }
+
+ public void set(Class targetClass, Class paramClass, String methodName) {
+ this.targetClass = targetClass;
+ this.paramClass = paramClass;
+ this.methodName = methodName;
+ }
+ }
+
+
+ /**
+ * Stores information related to reflection method lookup result.
+ */
+ static class MethodArgs {
+ public MethodHandle syncMethod;
+ public MethodHandle asyncMethod;
+ public String asyncMethodName;
+ }
+
+ /**
+ * This annotation indicates that a subclass of View is allowed to be used
+ * with the {@link 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(Exception ex) {
+ super(ex);
+ }
+ public ActionException(String message) {
+ super(message);
+ }
+ /**
+ * @hide
+ */
+ public ActionException(Throwable t) {
+ super(t);
+ }
+ }
+
+ /** @hide */
+ public static class OnClickHandler {
+
+ private int mEnterAnimationId;
+
+ public boolean onClickHandler(View view, PendingIntent pendingIntent,
+ Intent fillInIntent) {
+ return onClickHandler(view, pendingIntent, fillInIntent, StackId.INVALID_STACK_ID);
+ }
+
+ public boolean onClickHandler(View view, PendingIntent pendingIntent,
+ Intent fillInIntent, int launchStackId) {
+ try {
+ // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
+ Context context = view.getContext();
+ ActivityOptions opts;
+ if (mEnterAnimationId != 0) {
+ opts = ActivityOptions.makeCustomAnimation(context, mEnterAnimationId, 0);
+ } else {
+ opts = ActivityOptions.makeBasic();
+ }
+
+ if (launchStackId != StackId.INVALID_STACK_ID) {
+ opts.setLaunchStackId(launchStackId);
+ }
+ context.startIntentSender(
+ pendingIntent.getIntentSender(), fillInIntent,
+ Intent.FLAG_ACTIVITY_NEW_TASK,
+ Intent.FLAG_ACTIVITY_NEW_TASK, 0, opts.toBundle());
+ } catch (IntentSender.SendIntentException e) {
+ android.util.Log.e(LOG_TAG, "Cannot send pending intent: ", e);
+ return false;
+ } catch (Exception e) {
+ android.util.Log.e(LOG_TAG, "Cannot send pending intent due to " +
+ "unknown exception: ", e);
+ return false;
+ }
+ return true;
+ }
+
+ public void setEnterAnimationId(int enterAnimationId) {
+ mEnterAnimationId = enterAnimationId;
+ }
+ }
+
+ /**
+ * Base class for all actions that can be performed on an
+ * inflated view.
+ *
+ * SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
+ */
+ private abstract static class Action implements Parcelable {
+ public abstract void apply(View root, ViewGroup rootParent,
+ OnClickHandler handler) throws ActionException;
+
+ public static final int MERGE_REPLACE = 0;
+ public static final int MERGE_APPEND = 1;
+ public static final int MERGE_IGNORE = 2;
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void setBitmapCache(BitmapCache bitmapCache) {
+ // Do nothing
+ }
+
+ public int mergeBehavior() {
+ return MERGE_REPLACE;
+ }
+
+ public abstract String getActionName();
+
+ public String getUniqueKey() {
+ return (getActionName() + viewId);
+ }
+
+ /**
+ * This is called on the background thread. It should perform any non-ui computations
+ * and return the final action which will run on the UI thread.
+ * Override this if some of the tasks can be performed async.
+ */
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ return this;
+ }
+
+ public boolean prefersAsyncApply() {
+ return false;
+ }
+
+ /**
+ * Overridden by subclasses which have (or inherit) an ApplicationInfo instance
+ * as member variable
+ */
+ public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
+ return true;
+ }
+
+ int viewId;
+ }
+
+ /**
+ * Action class used during async inflation of RemoteViews. Subclasses are not parcelable.
+ */
+ private static abstract class RuntimeAction extends Action {
+ @Override
+ public final String getActionName() {
+ return "RuntimeAction";
+ }
+
+ @Override
+ public final void writeToParcel(Parcel dest, int flags) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ // Constant used during async execution. It is not parcelable.
+ private static final Action ACTION_NOOP = new RuntimeAction() {
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { }
+ };
+
+ /**
+ * Merges the passed RemoteViews actions with this RemoteViews actions according to
+ * action-specific merge rules.
+ *
+ * @param newRv
+ *
+ * @hide
+ */
+ public void mergeRemoteViews(RemoteViews newRv) {
+ if (newRv == null) return;
+ // We first copy the new RemoteViews, as the process of merging modifies the way the actions
+ // reference the bitmap cache. We don't want to modify the object as it may need to
+ // be merged and applied multiple times.
+ RemoteViews copy = new RemoteViews(newRv);
+
+ HashMap<String, Action> map = new HashMap<String, Action>();
+ if (mActions == null) {
+ mActions = new ArrayList<Action>();
+ }
+
+ int count = mActions.size();
+ for (int i = 0; i < count; i++) {
+ Action a = mActions.get(i);
+ map.put(a.getUniqueKey(), a);
+ }
+
+ ArrayList<Action> newActions = copy.mActions;
+ if (newActions == null) return;
+ count = newActions.size();
+ for (int i = 0; i < count; i++) {
+ Action a = newActions.get(i);
+ String key = newActions.get(i).getUniqueKey();
+ int mergeBehavior = newActions.get(i).mergeBehavior();
+ if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) {
+ mActions.remove(map.get(key));
+ map.remove(key);
+ }
+
+ // If the merge behavior is ignore, we don't bother keeping the extra action
+ if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) {
+ mActions.add(a);
+ }
+ }
+
+ // Because pruning can remove the need for bitmaps, we reconstruct the bitmap cache
+ mBitmapCache = new BitmapCache();
+ setBitmapCache(mBitmapCache);
+ }
+
+ private static class RemoteViewsContextWrapper extends ContextWrapper {
+ private final Context mContextForResources;
+
+ RemoteViewsContextWrapper(Context context, Context contextForResources) {
+ super(context);
+ mContextForResources = contextForResources;
+ }
+
+ @Override
+ public Resources getResources() {
+ return mContextForResources.getResources();
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ return mContextForResources.getTheme();
+ }
+
+ @Override
+ public String getPackageName() {
+ return mContextForResources.getPackageName();
+ }
+ }
+
+ private class SetEmptyView extends Action {
+ int viewId;
+ int emptyViewId;
+
+ SetEmptyView(int viewId, int emptyViewId) {
+ this.viewId = viewId;
+ this.emptyViewId = emptyViewId;
+ }
+
+ SetEmptyView(Parcel in) {
+ this.viewId = in.readInt();
+ this.emptyViewId = in.readInt();
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(SET_EMPTY_VIEW_ACTION_TAG);
+ out.writeInt(this.viewId);
+ out.writeInt(this.emptyViewId);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View view = root.findViewById(viewId);
+ if (!(view instanceof AdapterView<?>)) return;
+
+ AdapterView<?> adapterView = (AdapterView<?>) view;
+
+ final View emptyView = root.findViewById(emptyViewId);
+ if (emptyView == null) return;
+
+ adapterView.setEmptyView(emptyView);
+ }
+
+ public String getActionName() {
+ return "SetEmptyView";
+ }
+ }
+
+ private class SetOnClickFillInIntent extends Action {
+ public SetOnClickFillInIntent(int id, Intent fillInIntent) {
+ this.viewId = id;
+ this.fillInIntent = fillInIntent;
+ }
+
+ public SetOnClickFillInIntent(Parcel parcel) {
+ viewId = parcel.readInt();
+ fillInIntent = Intent.CREATOR.createFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_ON_CLICK_FILL_IN_INTENT_TAG);
+ dest.writeInt(viewId);
+ fillInIntent.writeToParcel(dest, 0 /* no flags */);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ if (!mIsWidgetCollectionChild) {
+ Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " +
+ "only from RemoteViewsFactory (ie. on collection items).");
+ return;
+ }
+ if (target == root) {
+ target.setTagInternal(com.android.internal.R.id.fillInIntent, fillInIntent);
+ } else if (fillInIntent != null) {
+ OnClickListener listener = new OnClickListener() {
+ public void onClick(View v) {
+ // Insure that this view is a child of an AdapterView
+ View parent = (View) v.getParent();
+ // Break the for loop on the first encounter of:
+ // 1) an AdapterView,
+ // 2) an AppWidgetHostView that is not a RemoteViewsFrameLayout, or
+ // 3) a null parent.
+ // 2) and 3) are unexpected and catch the case where a child is not
+ // correctly parented in an AdapterView.
+ while (parent != null && !(parent instanceof AdapterView<?>)
+ && !((parent instanceof AppWidgetHostView) &&
+ !(parent instanceof RemoteViewsAdapter.RemoteViewsFrameLayout))) {
+ parent = (View) parent.getParent();
+ }
+
+ if (!(parent instanceof AdapterView<?>)) {
+ // Somehow they've managed to get this far without having
+ // and AdapterView as a parent.
+ Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent");
+ return;
+ }
+
+ // Insure that a template pending intent has been set on an ancestor
+ if (!(parent.getTag() instanceof PendingIntent)) {
+ Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without" +
+ " calling setPendingIntentTemplate on parent.");
+ return;
+ }
+
+ PendingIntent pendingIntent = (PendingIntent) parent.getTag();
+
+ final Rect rect = getSourceBounds(v);
+
+ fillInIntent.setSourceBounds(rect);
+ handler.onClickHandler(v, pendingIntent, fillInIntent);
+ }
+
+ };
+ target.setOnClickListener(listener);
+ }
+ }
+
+ public String getActionName() {
+ return "SetOnClickFillInIntent";
+ }
+
+ Intent fillInIntent;
+ }
+
+ private class SetPendingIntentTemplate extends Action {
+ public SetPendingIntentTemplate(int id, PendingIntent pendingIntentTemplate) {
+ this.viewId = id;
+ this.pendingIntentTemplate = pendingIntentTemplate;
+ }
+
+ public SetPendingIntentTemplate(Parcel parcel) {
+ viewId = parcel.readInt();
+ pendingIntentTemplate = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_PENDING_INTENT_TEMPLATE_TAG);
+ dest.writeInt(viewId);
+ pendingIntentTemplate.writeToParcel(dest, 0 /* no flags */);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ // If the view isn't an AdapterView, setting a PendingIntent template doesn't make sense
+ if (target instanceof AdapterView<?>) {
+ AdapterView<?> av = (AdapterView<?>) target;
+ // The PendingIntent template is stored in the view's tag.
+ OnItemClickListener listener = new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ // The view should be a frame layout
+ if (view instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) view;
+
+ // AdapterViews contain their children in a frame
+ // so we need to go one layer deeper here.
+ if (parent instanceof AdapterViewAnimator) {
+ vg = (ViewGroup) vg.getChildAt(0);
+ }
+ if (vg == null) return;
+
+ Intent fillInIntent = null;
+ int childCount = vg.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ Object tag = vg.getChildAt(i).getTag(com.android.internal.R.id.fillInIntent);
+ if (tag instanceof Intent) {
+ fillInIntent = (Intent) tag;
+ break;
+ }
+ }
+ if (fillInIntent == null) return;
+
+ final Rect rect = getSourceBounds(view);
+
+ final Intent intent = new Intent();
+ intent.setSourceBounds(rect);
+ handler.onClickHandler(view, pendingIntentTemplate, fillInIntent);
+ }
+ }
+ };
+ av.setOnItemClickListener(listener);
+ av.setTag(pendingIntentTemplate);
+ } else {
+ Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" +
+ "an AdapterView (id: " + viewId + ")");
+ return;
+ }
+ }
+
+ public String getActionName() {
+ return "SetPendingIntentTemplate";
+ }
+
+ PendingIntent pendingIntentTemplate;
+ }
+
+ private class SetRemoteViewsAdapterList extends Action {
+ public SetRemoteViewsAdapterList(int id, ArrayList<RemoteViews> list, int viewTypeCount) {
+ this.viewId = id;
+ this.list = list;
+ this.viewTypeCount = viewTypeCount;
+ }
+
+ public SetRemoteViewsAdapterList(Parcel parcel) {
+ viewId = parcel.readInt();
+ viewTypeCount = parcel.readInt();
+ int count = parcel.readInt();
+ list = new ArrayList<RemoteViews>();
+
+ for (int i = 0; i < count; i++) {
+ RemoteViews rv = RemoteViews.CREATOR.createFromParcel(parcel);
+ list.add(rv);
+ }
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_REMOTE_VIEW_ADAPTER_LIST_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(viewTypeCount);
+
+ if (list == null || list.size() == 0) {
+ dest.writeInt(0);
+ } else {
+ int count = list.size();
+ dest.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ RemoteViews rv = list.get(i);
+ rv.writeToParcel(dest, flags);
+ }
+ }
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ // Ensure that we are applying to an AppWidget root
+ if (!(rootParent instanceof AppWidgetHostView)) {
+ Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " +
+ "AppWidgets (root id: " + viewId + ")");
+ return;
+ }
+ // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it
+ if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) {
+ Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " +
+ "an AbsListView or AdapterViewAnimator (id: " + viewId + ")");
+ return;
+ }
+
+ if (target instanceof AbsListView) {
+ AbsListView v = (AbsListView) target;
+ Adapter a = v.getAdapter();
+ if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
+ ((RemoteViewsListAdapter) a).setViewsList(list);
+ } else {
+ v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount));
+ }
+ } else if (target instanceof AdapterViewAnimator) {
+ AdapterViewAnimator v = (AdapterViewAnimator) target;
+ Adapter a = v.getAdapter();
+ if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
+ ((RemoteViewsListAdapter) a).setViewsList(list);
+ } else {
+ v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount));
+ }
+ }
+ }
+
+ public String getActionName() {
+ return "SetRemoteViewsAdapterList";
+ }
+
+ int viewTypeCount;
+ ArrayList<RemoteViews> list;
+ }
+
+ private class SetRemoteViewsAdapterIntent extends Action {
+ public SetRemoteViewsAdapterIntent(int id, Intent intent) {
+ this.viewId = id;
+ this.intent = intent;
+ }
+
+ public SetRemoteViewsAdapterIntent(Parcel parcel) {
+ viewId = parcel.readInt();
+ intent = Intent.CREATOR.createFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_REMOTE_VIEW_ADAPTER_INTENT_TAG);
+ dest.writeInt(viewId);
+ intent.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ // Ensure that we are applying to an AppWidget root
+ if (!(rootParent instanceof AppWidgetHostView)) {
+ Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " +
+ "AppWidgets (root id: " + viewId + ")");
+ return;
+ }
+ // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it
+ if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) {
+ Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " +
+ "an AbsListView or AdapterViewAnimator (id: " + viewId + ")");
+ return;
+ }
+
+ // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent
+ // RemoteViewsService
+ AppWidgetHostView host = (AppWidgetHostView) rootParent;
+ intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId());
+ if (target instanceof AbsListView) {
+ AbsListView v = (AbsListView) target;
+ v.setRemoteViewsAdapter(intent, isAsync);
+ v.setRemoteViewsOnClickHandler(handler);
+ } else if (target instanceof AdapterViewAnimator) {
+ AdapterViewAnimator v = (AdapterViewAnimator) target;
+ v.setRemoteViewsAdapter(intent, isAsync);
+ v.setRemoteViewsOnClickHandler(handler);
+ }
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent,
+ OnClickHandler handler) {
+ SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(viewId, intent);
+ copy.isAsync = true;
+ return copy;
+ }
+
+ public String getActionName() {
+ return "SetRemoteViewsAdapterIntent";
+ }
+
+ Intent intent;
+ boolean isAsync = false;
+ }
+
+ /**
+ * Equivalent to calling
+ * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
+ * to launch the provided {@link PendingIntent}.
+ */
+ private class SetOnClickPendingIntent extends Action {
+ public SetOnClickPendingIntent(int id, PendingIntent pendingIntent) {
+ this.viewId = id;
+ this.pendingIntent = pendingIntent;
+ }
+
+ public SetOnClickPendingIntent(Parcel parcel) {
+ viewId = parcel.readInt();
+
+ // We check a flag to determine if the parcel contains a PendingIntent.
+ if (parcel.readInt() != 0) {
+ pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
+ }
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_ON_CLICK_PENDING_INTENT_TAG);
+ dest.writeInt(viewId);
+
+ // We use a flag to indicate whether the parcel contains a valid object.
+ dest.writeInt(pendingIntent != null ? 1 : 0);
+ if (pendingIntent != null) {
+ pendingIntent.writeToParcel(dest, 0 /* no flags */);
+ }
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ // If the view is an AdapterView, setting a PendingIntent on click doesn't make much
+ // sense, do they mean to set a PendingIntent template for the AdapterView's children?
+ if (mIsWidgetCollectionChild) {
+ Log.w(LOG_TAG, "Cannot setOnClickPendingIntent for collection item " +
+ "(id: " + viewId + ")");
+ ApplicationInfo appInfo = root.getContext().getApplicationInfo();
+
+ // We let this slide for HC and ICS so as to not break compatibility. It should have
+ // been disabled from the outset, but was left open by accident.
+ if (appInfo != null &&
+ appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
+ return;
+ }
+ }
+
+ // If the pendingIntent is null, we clear the onClickListener
+ OnClickListener listener = null;
+ if (pendingIntent != null) {
+ listener = new OnClickListener() {
+ public void onClick(View v) {
+ // Find target view location in screen coordinates and
+ // fill into PendingIntent before sending.
+ final Rect rect = getSourceBounds(v);
+
+ final Intent intent = new Intent();
+ intent.setSourceBounds(rect);
+ handler.onClickHandler(v, pendingIntent, intent);
+ }
+ };
+ }
+ target.setOnClickListener(listener);
+ }
+
+ public String getActionName() {
+ return "SetOnClickPendingIntent";
+ }
+
+ PendingIntent pendingIntent;
+ }
+
+ private static Rect getSourceBounds(View v) {
+ final float appScale = v.getContext().getResources()
+ .getCompatibilityInfo().applicationScale;
+ final int[] pos = new int[2];
+ v.getLocationOnScreen(pos);
+
+ final Rect rect = new Rect();
+ rect.left = (int) (pos[0] * appScale + 0.5f);
+ rect.top = (int) (pos[1] * appScale + 0.5f);
+ rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
+ rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
+ return rect;
+ }
+
+ private MethodHandle getMethod(View view, String methodName, Class<?> paramType,
+ boolean async) {
+ MethodArgs result;
+ Class<? extends View> klass = view.getClass();
+
+ synchronized (sMethods) {
+ // The key is defined by the view class, param class and method name.
+ sLookupKey.set(klass, paramType, methodName);
+ result = sMethods.get(sLookupKey);
+
+ if (result == null) {
+ Method method;
+ try {
+ if (paramType == null) {
+ method = klass.getMethod(methodName);
+ } else {
+ method = klass.getMethod(methodName, paramType);
+ }
+ if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
+ throw new ActionException("view: " + klass.getName()
+ + " can't use method with RemoteViews: "
+ + methodName + getParameters(paramType));
+ }
+
+ result = new MethodArgs();
+ result.syncMethod = MethodHandles.publicLookup().unreflect(method);
+ result.asyncMethodName =
+ method.getAnnotation(RemotableViewMethod.class).asyncImpl();
+ } catch (NoSuchMethodException | IllegalAccessException ex) {
+ throw new ActionException("view: " + klass.getName() + " doesn't have method: "
+ + methodName + getParameters(paramType));
+ }
+
+ MethodKey key = new MethodKey();
+ key.set(klass, paramType, methodName);
+ sMethods.put(key, result);
+ }
+
+ if (!async) {
+ return result.syncMethod;
+ }
+ // Check this so see if async method is implemented or not.
+ if (result.asyncMethodName.isEmpty()) {
+ return null;
+ }
+ // Async method is lazily loaded. If it is not yet loaded, load now.
+ if (result.asyncMethod == null) {
+ MethodType asyncType = result.syncMethod.type()
+ .dropParameterTypes(0, 1).changeReturnType(Runnable.class);
+ try {
+ result.asyncMethod = MethodHandles.publicLookup().findVirtual(
+ klass, result.asyncMethodName, asyncType);
+ } catch (NoSuchMethodException | IllegalAccessException ex) {
+ throw new ActionException("Async implementation declared as "
+ + result.asyncMethodName + " but not defined for " + methodName
+ + ": public Runnable " + result.asyncMethodName + " ("
+ + TextUtils.join(",", asyncType.parameterArray()) + ")");
+ }
+ }
+ return result.asyncMethod;
+ }
+ }
+
+ private static String getParameters(Class<?> paramType) {
+ if (paramType == null) return "()";
+ return "(" + paramType + ")";
+ }
+
+ /**
+ * Equivalent to calling a combination of {@link Drawable#setAlpha(int)},
+ * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
+ * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given view.
+ * <p>
+ * These operations will be performed on the {@link Drawable} returned by the
+ * target {@link View#getBackground()} by default. If targetBackground is false,
+ * we assume the target is an {@link ImageView} and try applying the operations
+ * to {@link ImageView#getDrawable()}.
+ * <p>
+ * You can omit specific calls by marking their values with null or -1.
+ */
+ private class SetDrawableParameters extends Action {
+ public SetDrawableParameters(int id, boolean targetBackground, int alpha,
+ int colorFilter, PorterDuff.Mode mode, int level) {
+ this.viewId = id;
+ this.targetBackground = targetBackground;
+ this.alpha = alpha;
+ this.colorFilter = colorFilter;
+ this.filterMode = mode;
+ this.level = level;
+ }
+
+ public SetDrawableParameters(Parcel parcel) {
+ viewId = parcel.readInt();
+ targetBackground = parcel.readInt() != 0;
+ alpha = parcel.readInt();
+ colorFilter = parcel.readInt();
+ boolean hasMode = parcel.readInt() != 0;
+ if (hasMode) {
+ filterMode = PorterDuff.Mode.valueOf(parcel.readString());
+ } else {
+ filterMode = null;
+ }
+ level = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_DRAWABLE_PARAMETERS_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(targetBackground ? 1 : 0);
+ dest.writeInt(alpha);
+ dest.writeInt(colorFilter);
+ if (filterMode != null) {
+ dest.writeInt(1);
+ dest.writeString(filterMode.toString());
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(level);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ // Pick the correct drawable to modify for this view
+ Drawable targetDrawable = null;
+ if (targetBackground) {
+ targetDrawable = target.getBackground();
+ } else if (target instanceof ImageView) {
+ ImageView imageView = (ImageView) target;
+ targetDrawable = imageView.getDrawable();
+ }
+
+ if (targetDrawable != null) {
+ // Perform modifications only if values are set correctly
+ if (alpha != -1) {
+ targetDrawable.mutate().setAlpha(alpha);
+ }
+ if (filterMode != null) {
+ targetDrawable.mutate().setColorFilter(colorFilter, filterMode);
+ }
+ if (level != -1) {
+ targetDrawable.mutate().setLevel(level);
+ }
+ }
+ }
+
+ public String getActionName() {
+ return "SetDrawableParameters";
+ }
+
+ boolean targetBackground;
+ int alpha;
+ int colorFilter;
+ PorterDuff.Mode filterMode;
+ int level;
+ }
+
+ private final class ReflectionActionWithoutParams extends Action {
+ final String methodName;
+
+ ReflectionActionWithoutParams(int viewId, String methodName) {
+ this.viewId = viewId;
+ this.methodName = methodName;
+ }
+
+ ReflectionActionWithoutParams(Parcel in) {
+ this.viewId = in.readInt();
+ this.methodName = in.readString();
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG);
+ out.writeInt(this.viewId);
+ out.writeString(this.methodName);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View view = root.findViewById(viewId);
+ if (view == null) return;
+
+ try {
+ getMethod(view, this.methodName, null, false /* async */).invoke(view);
+ } catch (Throwable ex) {
+ throw new ActionException(ex);
+ }
+ }
+
+ public int mergeBehavior() {
+ // we don't need to build up showNext or showPrevious calls
+ if (methodName.equals("showNext") || methodName.equals("showPrevious")) {
+ return MERGE_IGNORE;
+ } else {
+ return MERGE_REPLACE;
+ }
+ }
+
+ public String getActionName() {
+ return "ReflectionActionWithoutParams";
+ }
+ }
+
+ private static class BitmapCache {
+
+ ArrayList<Bitmap> mBitmaps;
+ int mBitmapMemory = -1;
+
+ public BitmapCache() {
+ mBitmaps = new ArrayList<>();
+ }
+
+ public BitmapCache(Parcel source) {
+ int count = source.readInt();
+ mBitmaps = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ Bitmap b = Bitmap.CREATOR.createFromParcel(source);
+ mBitmaps.add(b);
+ }
+ }
+
+ public int getBitmapId(Bitmap b) {
+ if (b == null) {
+ return -1;
+ } else {
+ if (mBitmaps.contains(b)) {
+ return mBitmaps.indexOf(b);
+ } else {
+ mBitmaps.add(b);
+ mBitmapMemory = -1;
+ return (mBitmaps.size() - 1);
+ }
+ }
+ }
+
+ public Bitmap getBitmapForId(int id) {
+ if (id == -1 || id >= mBitmaps.size()) {
+ return null;
+ } else {
+ return mBitmaps.get(id);
+ }
+ }
+
+ public void writeBitmapsToParcel(Parcel dest, int flags) {
+ int count = mBitmaps.size();
+ dest.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ mBitmaps.get(i).writeToParcel(dest, flags);
+ }
+ }
+
+ public int getBitmapMemory() {
+ if (mBitmapMemory < 0) {
+ mBitmapMemory = 0;
+ int count = mBitmaps.size();
+ for (int i = 0; i < count; i++) {
+ mBitmapMemory += mBitmaps.get(i).getAllocationByteCount();
+ }
+ }
+ return mBitmapMemory;
+ }
+ }
+
+ private class BitmapReflectionAction extends Action {
+ int bitmapId;
+ Bitmap bitmap;
+ String methodName;
+
+ BitmapReflectionAction(int viewId, String methodName, Bitmap bitmap) {
+ this.bitmap = bitmap;
+ this.viewId = viewId;
+ this.methodName = methodName;
+ bitmapId = mBitmapCache.getBitmapId(bitmap);
+ }
+
+ BitmapReflectionAction(Parcel in) {
+ viewId = in.readInt();
+ methodName = in.readString();
+ bitmapId = in.readInt();
+ bitmap = mBitmapCache.getBitmapForId(bitmapId);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(BITMAP_REFLECTION_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeString(methodName);
+ dest.writeInt(bitmapId);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent,
+ OnClickHandler handler) throws ActionException {
+ ReflectionAction ra = new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP,
+ bitmap);
+ ra.apply(root, rootParent, handler);
+ }
+
+ @Override
+ public void setBitmapCache(BitmapCache bitmapCache) {
+ bitmapId = bitmapCache.getBitmapId(bitmap);
+ }
+
+ public String getActionName() {
+ return "BitmapReflectionAction";
+ }
+ }
+
+ /**
+ * Base class for the reflection actions.
+ */
+ private final class ReflectionAction extends Action {
+ static final int BOOLEAN = 1;
+ static final int BYTE = 2;
+ static final int SHORT = 3;
+ static final int INT = 4;
+ static final int LONG = 5;
+ static final int FLOAT = 6;
+ static final int DOUBLE = 7;
+ static final int CHAR = 8;
+ static final int STRING = 9;
+ static final int CHAR_SEQUENCE = 10;
+ static final int URI = 11;
+ // BITMAP actions are never stored in the list of actions. They are only used locally
+ // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
+ static final int BITMAP = 12;
+ static final int BUNDLE = 13;
+ static final int INTENT = 14;
+ static final int COLOR_STATE_LIST = 15;
+ static final int ICON = 16;
+
+ String methodName;
+ int type;
+ Object value;
+
+ ReflectionAction(int viewId, String methodName, int type, Object value) {
+ this.viewId = viewId;
+ this.methodName = methodName;
+ this.type = type;
+ this.value = value;
+ }
+
+ ReflectionAction(Parcel in) {
+ this.viewId = in.readInt();
+ this.methodName = in.readString();
+ this.type = in.readInt();
+ //noinspection ConstantIfStatement
+ if (false) {
+ Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
+ + " methodName=" + this.methodName + " type=" + this.type);
+ }
+
+ // For some values that may have been null, we first check a flag to see if they were
+ // written to the parcel.
+ switch (this.type) {
+ case BOOLEAN:
+ this.value = in.readInt() != 0;
+ break;
+ case BYTE:
+ this.value = in.readByte();
+ break;
+ case SHORT:
+ this.value = (short)in.readInt();
+ break;
+ case INT:
+ this.value = in.readInt();
+ break;
+ case LONG:
+ this.value = in.readLong();
+ break;
+ case FLOAT:
+ this.value = in.readFloat();
+ break;
+ case DOUBLE:
+ this.value = in.readDouble();
+ break;
+ case CHAR:
+ this.value = (char)in.readInt();
+ break;
+ case STRING:
+ this.value = in.readString();
+ break;
+ case CHAR_SEQUENCE:
+ this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ break;
+ case URI:
+ if (in.readInt() != 0) {
+ this.value = Uri.CREATOR.createFromParcel(in);
+ }
+ break;
+ case BITMAP:
+ if (in.readInt() != 0) {
+ this.value = Bitmap.CREATOR.createFromParcel(in);
+ }
+ break;
+ case BUNDLE:
+ this.value = in.readBundle();
+ break;
+ case INTENT:
+ if (in.readInt() != 0) {
+ this.value = Intent.CREATOR.createFromParcel(in);
+ }
+ break;
+ case COLOR_STATE_LIST:
+ if (in.readInt() != 0) {
+ this.value = ColorStateList.CREATOR.createFromParcel(in);
+ }
+ break;
+ case ICON:
+ if (in.readInt() != 0) {
+ this.value = Icon.CREATOR.createFromParcel(in);
+ }
+ default:
+ break;
+ }
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(REFLECTION_ACTION_TAG);
+ out.writeInt(this.viewId);
+ out.writeString(this.methodName);
+ out.writeInt(this.type);
+ //noinspection ConstantIfStatement
+ if (false) {
+ Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
+ + " methodName=" + this.methodName + " type=" + this.type);
+ }
+
+ // For some values which are null, we record an integer flag to indicate whether
+ // we have written a valid value to the parcel.
+ switch (this.type) {
+ case BOOLEAN:
+ out.writeInt((Boolean) this.value ? 1 : 0);
+ break;
+ case BYTE:
+ out.writeByte((Byte) this.value);
+ break;
+ case SHORT:
+ out.writeInt((Short) this.value);
+ break;
+ case INT:
+ out.writeInt((Integer) this.value);
+ break;
+ case LONG:
+ out.writeLong((Long) this.value);
+ break;
+ case FLOAT:
+ out.writeFloat((Float) this.value);
+ break;
+ case DOUBLE:
+ out.writeDouble((Double) this.value);
+ break;
+ case CHAR:
+ out.writeInt((int)((Character)this.value).charValue());
+ break;
+ case STRING:
+ out.writeString((String)this.value);
+ break;
+ case CHAR_SEQUENCE:
+ TextUtils.writeToParcel((CharSequence)this.value, out, flags);
+ break;
+ case BUNDLE:
+ out.writeBundle((Bundle) this.value);
+ break;
+ case URI:
+ case BITMAP:
+ case INTENT:
+ case COLOR_STATE_LIST:
+ case ICON:
+ out.writeInt(this.value != null ? 1 : 0);
+ if (this.value != null) {
+ ((Parcelable) this.value).writeToParcel(out, flags);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private Class<?> getParameterType() {
+ switch (this.type) {
+ case BOOLEAN:
+ return boolean.class;
+ case BYTE:
+ return byte.class;
+ case SHORT:
+ return short.class;
+ case INT:
+ return int.class;
+ case LONG:
+ return long.class;
+ case FLOAT:
+ return float.class;
+ case DOUBLE:
+ return double.class;
+ case CHAR:
+ return char.class;
+ case STRING:
+ return String.class;
+ case CHAR_SEQUENCE:
+ return CharSequence.class;
+ case URI:
+ return Uri.class;
+ case BITMAP:
+ return Bitmap.class;
+ case BUNDLE:
+ return Bundle.class;
+ case INTENT:
+ return Intent.class;
+ case COLOR_STATE_LIST:
+ return ColorStateList.class;
+ case ICON:
+ return Icon.class;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View view = root.findViewById(viewId);
+ if (view == null) return;
+
+ Class<?> param = getParameterType();
+ if (param == null) {
+ throw new ActionException("bad type: " + this.type);
+ }
+ try {
+ getMethod(view, this.methodName, param, false /* async */).invoke(view, this.value);
+ } catch (Throwable ex) {
+ throw new ActionException(ex);
+ }
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ final View view = root.findViewById(viewId);
+ if (view == null) return ACTION_NOOP;
+
+ Class<?> param = getParameterType();
+ if (param == null) {
+ throw new ActionException("bad type: " + this.type);
+ }
+
+ try {
+ MethodHandle method = getMethod(view, this.methodName, param, true /* async */);
+
+ if (method != null) {
+ Runnable endAction = (Runnable) method.invoke(view, this.value);
+ if (endAction == null) {
+ return ACTION_NOOP;
+ } else {
+ // Special case view stub
+ if (endAction instanceof ViewStub.ViewReplaceRunnable) {
+ root.createTree();
+ // Replace child tree
+ root.findViewTreeById(viewId).replaceView(
+ ((ViewStub.ViewReplaceRunnable) endAction).view);
+ }
+ return new RunnableAction(endAction);
+ }
+ }
+ } catch (Throwable ex) {
+ throw new ActionException(ex);
+ }
+
+ return this;
+ }
+
+ public int mergeBehavior() {
+ // smoothScrollBy is cumulative, everything else overwites.
+ if (methodName.equals("smoothScrollBy")) {
+ return MERGE_APPEND;
+ } else {
+ return MERGE_REPLACE;
+ }
+ }
+
+ public String getActionName() {
+ // Each type of reflection action corresponds to a setter, so each should be seen as
+ // unique from the standpoint of merging.
+ return "ReflectionAction" + this.methodName + this.type;
+ }
+
+ @Override
+ public boolean prefersAsyncApply() {
+ return this.type == URI || this.type == ICON;
+ }
+ }
+
+ /**
+ * This is only used for async execution of actions and it not parcelable.
+ */
+ private static final class RunnableAction extends RuntimeAction {
+ private final Runnable mRunnable;
+
+ RunnableAction(Runnable r) {
+ mRunnable = r;
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ mRunnable.run();
+ }
+ }
+
+ private void configureRemoteViewsAsChild(RemoteViews rv) {
+ rv.setBitmapCache(mBitmapCache);
+ rv.setNotRoot();
+ }
+
+ void setNotRoot() {
+ mIsRoot = false;
+ }
+
+ /**
+ * ViewGroup methods that are related to adding Views.
+ */
+ private class ViewGroupActionAdd extends Action {
+ private RemoteViews mNestedViews;
+ private int mIndex;
+
+ ViewGroupActionAdd(int viewId, RemoteViews nestedViews) {
+ this(viewId, nestedViews, -1 /* index */);
+ }
+
+ ViewGroupActionAdd(int viewId, RemoteViews nestedViews, int index) {
+ this.viewId = viewId;
+ mNestedViews = nestedViews;
+ mIndex = index;
+ if (nestedViews != null) {
+ configureRemoteViewsAsChild(nestedViews);
+ }
+ }
+
+ ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info,
+ int depth) {
+ viewId = parcel.readInt();
+ mIndex = parcel.readInt();
+ mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(VIEW_GROUP_ACTION_ADD_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(mIndex);
+ mNestedViews.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
+ return mNestedViews.mApplication.packageName.equals(parentInfo.packageName)
+ && mNestedViews.mApplication.uid == parentInfo.uid;
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final Context context = root.getContext();
+ final ViewGroup target = root.findViewById(viewId);
+
+ if (target == null) {
+ return;
+ }
+
+ // Inflate nested views and add as children
+ target.addView(mNestedViews.apply(context, target, handler), mIndex);
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ // In the async implementation, update the view tree so that subsequent calls to
+ // findViewById return the current view.
+ root.createTree();
+ ViewTree target = root.findViewTreeById(viewId);
+ if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
+ return ACTION_NOOP;
+ }
+ final ViewGroup targetVg = (ViewGroup) target.mRoot;
+
+ // Inflate nested views and perform all the async tasks for the child remoteView.
+ final Context context = root.mRoot.getContext();
+ final AsyncApplyTask task = mNestedViews.getAsyncApplyTask(
+ context, targetVg, null, handler);
+ final ViewTree tree = task.doInBackground();
+
+ if (tree == null) {
+ throw new ActionException(task.mError);
+ }
+
+ // Update the global view tree, so that next call to findViewTreeById
+ // goes through the subtree as well.
+ target.addChild(tree, mIndex);
+
+ return new RuntimeAction() {
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
+ throws ActionException {
+ task.onPostExecute(tree);
+ targetVg.addView(task.mResult, mIndex);
+ }
+ };
+ }
+
+ @Override
+ public void setBitmapCache(BitmapCache bitmapCache) {
+ mNestedViews.setBitmapCache(bitmapCache);
+ }
+
+ @Override
+ public int mergeBehavior() {
+ return MERGE_APPEND;
+ }
+
+ @Override
+ public boolean prefersAsyncApply() {
+ return mNestedViews.prefersAsyncApply();
+ }
+
+
+ @Override
+ public String getActionName() {
+ return "ViewGroupActionAdd";
+ }
+ }
+
+ /**
+ * ViewGroup methods related to removing child views.
+ */
+ private class ViewGroupActionRemove extends Action {
+ /**
+ * Id that indicates that all child views of the affected ViewGroup should be removed.
+ *
+ * <p>Using -2 because the default id is -1. This avoids accidentally matching that.
+ */
+ private static final int REMOVE_ALL_VIEWS_ID = -2;
+
+ private int mViewIdToKeep;
+
+ ViewGroupActionRemove(int viewId) {
+ this(viewId, REMOVE_ALL_VIEWS_ID);
+ }
+
+ ViewGroupActionRemove(int viewId, int viewIdToKeep) {
+ this.viewId = viewId;
+ mViewIdToKeep = viewIdToKeep;
+ }
+
+ ViewGroupActionRemove(Parcel parcel) {
+ viewId = parcel.readInt();
+ mViewIdToKeep = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(VIEW_GROUP_ACTION_REMOVE_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(mViewIdToKeep);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final ViewGroup target = root.findViewById(viewId);
+
+ if (target == null) {
+ return;
+ }
+
+ if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
+ target.removeAllViews();
+ return;
+ }
+
+ removeAllViewsExceptIdToKeep(target);
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ // In the async implementation, update the view tree so that subsequent calls to
+ // findViewById return the current view.
+ root.createTree();
+ ViewTree target = root.findViewTreeById(viewId);
+
+ if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
+ return ACTION_NOOP;
+ }
+
+ final ViewGroup targetVg = (ViewGroup) target.mRoot;
+
+ // Clear all children when nested views omitted
+ target.mChildren = null;
+ return new RuntimeAction() {
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
+ throws ActionException {
+ if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
+ targetVg.removeAllViews();
+ return;
+ }
+
+ removeAllViewsExceptIdToKeep(targetVg);
+ }
+ };
+ }
+
+ /**
+ * Iterates through the children in the given ViewGroup and removes all the views that
+ * do not have an id of {@link #mViewIdToKeep}.
+ */
+ private void removeAllViewsExceptIdToKeep(ViewGroup viewGroup) {
+ // Otherwise, remove all the views that do not match the id to keep.
+ int index = viewGroup.getChildCount() - 1;
+ while (index >= 0) {
+ if (viewGroup.getChildAt(index).getId() != mViewIdToKeep) {
+ viewGroup.removeViewAt(index);
+ }
+ index--;
+ }
+ }
+
+ @Override
+ public String getActionName() {
+ return "ViewGroupActionRemove";
+ }
+
+ @Override
+ public int mergeBehavior() {
+ return MERGE_APPEND;
+ }
+ }
+
+ /**
+ * Helper action to set compound drawables on a TextView. Supports relative
+ * (s/t/e/b) or cardinal (l/t/r/b) arrangement.
+ */
+ private class TextViewDrawableAction extends Action {
+ public TextViewDrawableAction(int viewId, boolean isRelative, int d1, int d2, int d3, int d4) {
+ this.viewId = viewId;
+ this.isRelative = isRelative;
+ this.useIcons = false;
+ this.d1 = d1;
+ this.d2 = d2;
+ this.d3 = d3;
+ this.d4 = d4;
+ }
+
+ public TextViewDrawableAction(int viewId, boolean isRelative,
+ Icon i1, Icon i2, Icon i3, Icon i4) {
+ this.viewId = viewId;
+ this.isRelative = isRelative;
+ this.useIcons = true;
+ this.i1 = i1;
+ this.i2 = i2;
+ this.i3 = i3;
+ this.i4 = i4;
+ }
+
+ public TextViewDrawableAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ isRelative = (parcel.readInt() != 0);
+ useIcons = (parcel.readInt() != 0);
+ if (useIcons) {
+ if (parcel.readInt() != 0) {
+ i1 = Icon.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ i2 = Icon.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ i3 = Icon.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ i4 = Icon.CREATOR.createFromParcel(parcel);
+ }
+ } else {
+ d1 = parcel.readInt();
+ d2 = parcel.readInt();
+ d3 = parcel.readInt();
+ d4 = parcel.readInt();
+ }
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TEXT_VIEW_DRAWABLE_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(isRelative ? 1 : 0);
+ dest.writeInt(useIcons ? 1 : 0);
+ if (useIcons) {
+ if (i1 != null) {
+ dest.writeInt(1);
+ i1.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ if (i2 != null) {
+ dest.writeInt(1);
+ i2.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ if (i3 != null) {
+ dest.writeInt(1);
+ i3.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ if (i4 != null) {
+ dest.writeInt(1);
+ i4.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ } else {
+ dest.writeInt(d1);
+ dest.writeInt(d2);
+ dest.writeInt(d3);
+ dest.writeInt(d4);
+ }
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final TextView target = root.findViewById(viewId);
+ if (target == null) return;
+ if (drawablesLoaded) {
+ if (isRelative) {
+ target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
+ } else {
+ target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
+ }
+ } else if (useIcons) {
+ final Context ctx = target.getContext();
+ final Drawable id1 = i1 == null ? null : i1.loadDrawable(ctx);
+ final Drawable id2 = i2 == null ? null : i2.loadDrawable(ctx);
+ final Drawable id3 = i3 == null ? null : i3.loadDrawable(ctx);
+ final Drawable id4 = i4 == null ? null : i4.loadDrawable(ctx);
+ if (isRelative) {
+ target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
+ } else {
+ target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
+ }
+ } else {
+ if (isRelative) {
+ target.setCompoundDrawablesRelativeWithIntrinsicBounds(d1, d2, d3, d4);
+ } else {
+ target.setCompoundDrawablesWithIntrinsicBounds(d1, d2, d3, d4);
+ }
+ }
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ final TextView target = root.findViewById(viewId);
+ if (target == null) return ACTION_NOOP;
+
+ TextViewDrawableAction copy = useIcons ?
+ new TextViewDrawableAction(viewId, isRelative, i1, i2, i3, i4) :
+ new TextViewDrawableAction(viewId, isRelative, d1, d2, d3, d4);
+
+ // Load the drawables on the background thread.
+ copy.drawablesLoaded = true;
+ final Context ctx = target.getContext();
+
+ if (useIcons) {
+ copy.id1 = i1 == null ? null : i1.loadDrawable(ctx);
+ copy.id2 = i2 == null ? null : i2.loadDrawable(ctx);
+ copy.id3 = i3 == null ? null : i3.loadDrawable(ctx);
+ copy.id4 = i4 == null ? null : i4.loadDrawable(ctx);
+ } else {
+ copy.id1 = d1 == 0 ? null : ctx.getDrawable(d1);
+ copy.id2 = d2 == 0 ? null : ctx.getDrawable(d2);
+ copy.id3 = d3 == 0 ? null : ctx.getDrawable(d3);
+ copy.id4 = d4 == 0 ? null : ctx.getDrawable(d4);
+ }
+ return copy;
+ }
+
+ @Override
+ public boolean prefersAsyncApply() {
+ return useIcons;
+ }
+
+ public String getActionName() {
+ return "TextViewDrawableAction";
+ }
+
+ boolean isRelative = false;
+ boolean useIcons = false;
+ int d1, d2, d3, d4;
+ Icon i1, i2, i3, i4;
+
+ boolean drawablesLoaded = false;
+ Drawable id1, id2, id3, id4;
+ }
+
+ /**
+ * Helper action to set text size on a TextView in any supported units.
+ */
+ private class TextViewSizeAction extends Action {
+ public TextViewSizeAction(int viewId, int units, float size) {
+ this.viewId = viewId;
+ this.units = units;
+ this.size = size;
+ }
+
+ public TextViewSizeAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ units = parcel.readInt();
+ size = parcel.readFloat();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TEXT_VIEW_SIZE_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(units);
+ dest.writeFloat(size);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final TextView target = root.findViewById(viewId);
+ if (target == null) return;
+ target.setTextSize(units, size);
+ }
+
+ public String getActionName() {
+ return "TextViewSizeAction";
+ }
+
+ int units;
+ float size;
+ }
+
+ /**
+ * Helper action to set padding on a View.
+ */
+ private class ViewPaddingAction extends Action {
+ public ViewPaddingAction(int viewId, int left, int top, int right, int bottom) {
+ this.viewId = viewId;
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ public ViewPaddingAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ left = parcel.readInt();
+ top = parcel.readInt();
+ right = parcel.readInt();
+ bottom = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(VIEW_PADDING_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(left);
+ dest.writeInt(top);
+ dest.writeInt(right);
+ dest.writeInt(bottom);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+ target.setPadding(left, top, right, bottom);
+ }
+
+ public String getActionName() {
+ return "ViewPaddingAction";
+ }
+
+ int left, top, right, bottom;
+ }
+
+ /**
+ * Helper action to set layout params on a View.
+ */
+ private static class LayoutParamAction extends Action {
+
+ /** Set marginEnd */
+ public static final int LAYOUT_MARGIN_END_DIMEN = 1;
+ /** Set width */
+ public static final int LAYOUT_WIDTH = 2;
+ public static final int LAYOUT_MARGIN_BOTTOM_DIMEN = 3;
+
+ /**
+ * @param viewId ID of the view alter
+ * @param property which layout parameter to alter
+ * @param value new value of the layout parameter
+ */
+ public LayoutParamAction(int viewId, int property, int value) {
+ this.viewId = viewId;
+ this.property = property;
+ this.value = value;
+ }
+
+ public LayoutParamAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ property = parcel.readInt();
+ value = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(LAYOUT_PARAM_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(property);
+ dest.writeInt(value);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) {
+ return;
+ }
+ ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
+ if (layoutParams == null) {
+ return;
+ }
+ switch (property) {
+ case LAYOUT_MARGIN_END_DIMEN:
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
+ int resolved = resolveDimenPixelOffset(target, value);
+ ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(resolved);
+ target.setLayoutParams(layoutParams);
+ }
+ break;
+ case LAYOUT_MARGIN_BOTTOM_DIMEN:
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
+ int resolved = resolveDimenPixelOffset(target, value);
+ ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = resolved;
+ target.setLayoutParams(layoutParams);
+ }
+ break;
+ case LAYOUT_WIDTH:
+ layoutParams.width = value;
+ target.setLayoutParams(layoutParams);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown property " + property);
+ }
+ }
+
+ private static int resolveDimenPixelOffset(View target, int value) {
+ if (value == 0) {
+ return 0;
+ }
+ return target.getContext().getResources().getDimensionPixelOffset(value);
+ }
+
+ public String getActionName() {
+ return "LayoutParamAction" + property + ".";
+ }
+
+ int property;
+ int value;
+ }
+
+ /**
+ * Helper action to set a color filter on a compound drawable on a TextView. Supports relative
+ * (s/t/e/b) or cardinal (l/t/r/b) arrangement.
+ */
+ private class TextViewDrawableColorFilterAction extends Action {
+ public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index,
+ int color, PorterDuff.Mode mode) {
+ this.viewId = viewId;
+ this.isRelative = isRelative;
+ this.index = index;
+ this.color = color;
+ this.mode = mode;
+ }
+
+ public TextViewDrawableColorFilterAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ isRelative = (parcel.readInt() != 0);
+ index = parcel.readInt();
+ color = parcel.readInt();
+ mode = readPorterDuffMode(parcel);
+ }
+
+ private PorterDuff.Mode readPorterDuffMode(Parcel parcel) {
+ int mode = parcel.readInt();
+ if (mode >= 0 && mode < PorterDuff.Mode.values().length) {
+ return PorterDuff.Mode.values()[mode];
+ } else {
+ return PorterDuff.Mode.CLEAR;
+ }
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(isRelative ? 1 : 0);
+ dest.writeInt(index);
+ dest.writeInt(color);
+ dest.writeInt(mode.ordinal());
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final TextView target = root.findViewById(viewId);
+ if (target == null) return;
+ Drawable[] drawables = isRelative
+ ? target.getCompoundDrawablesRelative()
+ : target.getCompoundDrawables();
+ if (index < 0 || index >= 4) {
+ throw new IllegalStateException("index must be in range [0, 3].");
+ }
+ Drawable d = drawables[index];
+ if (d != null) {
+ d.mutate();
+ d.setColorFilter(color, mode);
+ }
+ }
+
+ public String getActionName() {
+ return "TextViewDrawableColorFilterAction";
+ }
+
+ final boolean isRelative;
+ final int index;
+ final int color;
+ final PorterDuff.Mode mode;
+ }
+
+ /**
+ * Helper action to add a view tag with RemoteInputs.
+ */
+ private class SetRemoteInputsAction extends Action {
+
+ public SetRemoteInputsAction(int viewId, RemoteInput[] remoteInputs) {
+ this.viewId = viewId;
+ this.remoteInputs = remoteInputs;
+ }
+
+ public SetRemoteInputsAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ remoteInputs = parcel.createTypedArray(RemoteInput.CREATOR);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(SET_REMOTE_INPUTS_ACTION_TAG);
+ dest.writeInt(viewId);
+ dest.writeTypedArray(remoteInputs, flags);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+ if (target == null) return;
+
+ target.setTagInternal(R.id.remote_input_tag, remoteInputs);
+ }
+
+ public String getActionName() {
+ return "SetRemoteInputsAction";
+ }
+
+ final Parcelable[] remoteInputs;
+ }
+
+ /**
+ * Helper action to override all textViewColors
+ */
+ private class OverrideTextColorsAction extends Action {
+
+ private final int textColor;
+
+ public OverrideTextColorsAction(int textColor) {
+ this.textColor = textColor;
+ }
+
+ public OverrideTextColorsAction(Parcel parcel) {
+ textColor = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(OVERRIDE_TEXT_COLORS_TAG);
+ dest.writeInt(textColor);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ // Let's traverse the viewtree and override all textColors!
+ Stack<View> viewsToProcess = new Stack<>();
+ viewsToProcess.add(root);
+ while (!viewsToProcess.isEmpty()) {
+ View v = viewsToProcess.pop();
+ if (v instanceof TextView) {
+ TextView textView = (TextView) v;
+ textView.setText(NotificationColorUtil.clearColorSpans(textView.getText()));
+ textView.setTextColor(textColor);
+ }
+ if (v instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) v;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ viewsToProcess.push(viewGroup.getChildAt(i));
+ }
+ }
+ }
+ }
+
+ public String getActionName() {
+ return "OverrideTextColorsAction";
+ }
+ }
+
+ /**
+ * 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) {
+ this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
+ }
+
+ /**
+ * 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 userId The user under which the package is running.
+ * @param layoutId The id of the layout resource.
+ *
+ * @hide
+ */
+ public RemoteViews(String packageName, int userId, int layoutId) {
+ this(getApplicationInfo(packageName, userId), layoutId);
+ }
+
+ /**
+ * Create a new RemoteViews object that will display the views contained
+ * in the specified layout file.
+ *
+ * @param application The application whose content is shown by the views.
+ * @param layoutId The id of the layout resource.
+ *
+ * @hide
+ */
+ protected RemoteViews(ApplicationInfo application, int layoutId) {
+ mApplication = application;
+ mLayoutId = layoutId;
+ mBitmapCache = new BitmapCache();
+ }
+
+ private boolean hasLandscapeAndPortraitLayouts() {
+ return (mLandscape != null) && (mPortrait != null);
+ }
+
+ /**
+ * Create a new RemoteViews object that will inflate as the specified
+ * landspace or portrait RemoteViews, depending on the current configuration.
+ *
+ * @param landscape The RemoteViews to inflate in landscape configuration
+ * @param portrait The RemoteViews to inflate in portrait configuration
+ */
+ public RemoteViews(RemoteViews landscape, RemoteViews portrait) {
+ if (landscape == null || portrait == null) {
+ throw new RuntimeException("Both RemoteViews must be non-null");
+ }
+ if (landscape.mApplication.uid != portrait.mApplication.uid
+ || !landscape.mApplication.packageName.equals(portrait.mApplication.packageName)) {
+ throw new RuntimeException("Both RemoteViews must share the same package and user");
+ }
+ mApplication = portrait.mApplication;
+ mLayoutId = portrait.getLayoutId();
+
+ mLandscape = landscape;
+ mPortrait = portrait;
+
+ mBitmapCache = new BitmapCache();
+ configureRemoteViewsAsChild(landscape);
+ configureRemoteViewsAsChild(portrait);
+ }
+
+ /**
+ * Creates a copy of another RemoteViews.
+ */
+ public RemoteViews(RemoteViews src) {
+ mBitmapCache = src.mBitmapCache;
+ mApplication = src.mApplication;
+ mIsRoot = src.mIsRoot;
+ mLayoutId = src.mLayoutId;
+ mIsWidgetCollectionChild = src.mIsWidgetCollectionChild;
+ mReapplyDisallowed = src.mReapplyDisallowed;
+
+ if (src.hasLandscapeAndPortraitLayouts()) {
+ mLandscape = new RemoteViews(src.mLandscape);
+ mPortrait = new RemoteViews(src.mPortrait);
+
+ }
+
+ if (src.mActions != null) {
+ mActions = new ArrayList<>();
+
+ Parcel p = Parcel.obtain();
+ int count = src.mActions.size();
+ for (int i = 0; i < count; i++) {
+ p.setDataPosition(0);
+ Action a = src.mActions.get(i);
+ a.writeToParcel(
+ p, a.hasSameAppInfo(mApplication) ? PARCELABLE_ELIDE_DUPLICATES : 0);
+ p.setDataPosition(0);
+ // Since src is already in memory, we do not care about stack overflow as it has
+ // already been read once.
+ mActions.add(getActionFromParcel(p, 0));
+ }
+ p.recycle();
+ }
+
+ // Now that everything is initialized and duplicated, setting a new BitmapCache will
+ // re-initialize the cache.
+ setBitmapCache(new BitmapCache());
+ }
+
+ /**
+ * Reads a RemoteViews object from a parcel.
+ *
+ * @param parcel
+ */
+ public RemoteViews(Parcel parcel) {
+ this(parcel, null, null, 0);
+ }
+
+ private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth) {
+ if (depth > MAX_NESTED_VIEWS
+ && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) {
+ throw new IllegalArgumentException("Too many nested views.");
+ }
+ depth++;
+
+ int mode = parcel.readInt();
+
+ // We only store a bitmap cache in the root of the RemoteViews.
+ if (bitmapCache == null) {
+ mBitmapCache = new BitmapCache(parcel);
+ } else {
+ setBitmapCache(bitmapCache);
+ setNotRoot();
+ }
+
+ if (mode == MODE_NORMAL) {
+ mApplication = parcel.readInt() == 0 ? info :
+ ApplicationInfo.CREATOR.createFromParcel(parcel);
+ mLayoutId = parcel.readInt();
+ mIsWidgetCollectionChild = parcel.readInt() == 1;
+
+ int count = parcel.readInt();
+ if (count > 0) {
+ mActions = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ mActions.add(getActionFromParcel(parcel, depth));
+ }
+ }
+ } else {
+ // MODE_HAS_LANDSCAPE_AND_PORTRAIT
+ mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth);
+ mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth);
+ mApplication = mPortrait.mApplication;
+ mLayoutId = mPortrait.getLayoutId();
+ }
+ mReapplyDisallowed = parcel.readInt() == 0;
+ }
+
+ private Action getActionFromParcel(Parcel parcel, int depth) {
+ int tag = parcel.readInt();
+ switch (tag) {
+ case SET_ON_CLICK_PENDING_INTENT_TAG:
+ return new SetOnClickPendingIntent(parcel);
+ case SET_DRAWABLE_PARAMETERS_TAG:
+ return new SetDrawableParameters(parcel);
+ case REFLECTION_ACTION_TAG:
+ return new ReflectionAction(parcel);
+ case VIEW_GROUP_ACTION_ADD_TAG:
+ return new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, depth);
+ case VIEW_GROUP_ACTION_REMOVE_TAG:
+ return new ViewGroupActionRemove(parcel);
+ case SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG:
+ return new ReflectionActionWithoutParams(parcel);
+ case SET_EMPTY_VIEW_ACTION_TAG:
+ return new SetEmptyView(parcel);
+ case SET_PENDING_INTENT_TEMPLATE_TAG:
+ return new SetPendingIntentTemplate(parcel);
+ case SET_ON_CLICK_FILL_IN_INTENT_TAG:
+ return new SetOnClickFillInIntent(parcel);
+ case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG:
+ return new SetRemoteViewsAdapterIntent(parcel);
+ case TEXT_VIEW_DRAWABLE_ACTION_TAG:
+ return new TextViewDrawableAction(parcel);
+ case TEXT_VIEW_SIZE_ACTION_TAG:
+ return new TextViewSizeAction(parcel);
+ case VIEW_PADDING_ACTION_TAG:
+ return new ViewPaddingAction(parcel);
+ case BITMAP_REFLECTION_ACTION_TAG:
+ return new BitmapReflectionAction(parcel);
+ case SET_REMOTE_VIEW_ADAPTER_LIST_TAG:
+ return new SetRemoteViewsAdapterList(parcel);
+ case TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG:
+ return new TextViewDrawableColorFilterAction(parcel);
+ case SET_REMOTE_INPUTS_ACTION_TAG:
+ return new SetRemoteInputsAction(parcel);
+ case LAYOUT_PARAM_ACTION_TAG:
+ return new LayoutParamAction(parcel);
+ case OVERRIDE_TEXT_COLORS_TAG:
+ return new OverrideTextColorsAction(parcel);
+ default:
+ throw new ActionException("Tag " + tag + " not found");
+ }
+ };
+
+ /**
+ * Returns a deep copy of the RemoteViews object. The RemoteView may not be
+ * attached to another RemoteView -- it must be the root of a hierarchy.
+ *
+ * @deprecated use {@link #RemoteViews(RemoteViews)} instead.
+ * @throws IllegalStateException if this is not the root of a RemoteView
+ * hierarchy
+ */
+ @Override
+ @Deprecated
+ public RemoteViews clone() {
+ Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. "
+ + "May only clone the root of a RemoteView hierarchy.");
+
+ return new RemoteViews(this);
+ }
+
+ public String getPackage() {
+ return (mApplication != null) ? mApplication.packageName : null;
+ }
+
+ /**
+ * Returns the layout id of the root layout associated with this RemoteViews. In the case
+ * that the RemoteViews has both a landscape and portrait root, this will return the layout
+ * id associated with the portrait layout.
+ *
+ * @return the layout id.
+ */
+ public int getLayoutId() {
+ return mLayoutId;
+ }
+
+ /*
+ * This flag indicates whether this RemoteViews object is being created from a
+ * RemoteViewsService for use as a child of a widget collection. This flag is used
+ * to determine whether or not certain features are available, in particular,
+ * setting on click extras and setting on click pending intents. The former is enabled,
+ * and the latter disabled when this flag is true.
+ */
+ void setIsWidgetCollectionChild(boolean isWidgetCollectionChild) {
+ mIsWidgetCollectionChild = isWidgetCollectionChild;
+ }
+
+ /**
+ * Recursively sets BitmapCache in the hierarchy and update the bitmap ids.
+ */
+ private void setBitmapCache(BitmapCache bitmapCache) {
+ mBitmapCache = bitmapCache;
+ if (!hasLandscapeAndPortraitLayouts()) {
+ if (mActions != null) {
+ final int count = mActions.size();
+ for (int i= 0; i < count; ++i) {
+ mActions.get(i).setBitmapCache(bitmapCache);
+ }
+ }
+ } else {
+ mLandscape.setBitmapCache(bitmapCache);
+ mPortrait.setBitmapCache(bitmapCache);
+ }
+ }
+
+ /**
+ * Returns an estimate of the bitmap heap memory usage for this RemoteViews.
+ */
+ /** @hide */
+ public int estimateMemoryUsage() {
+ return mBitmapCache.getBitmapMemory();
+ }
+
+ /**
+ * 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 (hasLandscapeAndPortraitLayouts()) {
+ throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
+ " layouts cannot be modified. Instead, fully configure the landscape and" +
+ " portrait layouts individually before constructing the combined layout.");
+ }
+ if (mActions == null) {
+ mActions = new ArrayList<>();
+ }
+ mActions.add(a);
+ }
+
+ /**
+ * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the
+ * given {@link RemoteViews}. This allows users to build "nested"
+ * {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may
+ * recycle layouts, use {@link #removeAllViews(int)} to clear any existing
+ * children.
+ *
+ * @param viewId The id of the parent {@link ViewGroup} to add child into.
+ * @param nestedView {@link RemoteViews} that describes the child.
+ */
+ public void addView(int viewId, RemoteViews nestedView) {
+ addAction(nestedView == null
+ ? new ViewGroupActionRemove(viewId)
+ : new ViewGroupActionAdd(viewId, nestedView));
+ }
+
+ /**
+ * Equivalent to calling {@link ViewGroup#addView(View, int)} after inflating the
+ * given {@link RemoteViews}.
+ *
+ * @param viewId The id of the parent {@link ViewGroup} to add the child into.
+ * @param nestedView {@link RemoteViews} of the child to add.
+ * @param index The position at which to add the child.
+ *
+ * @hide
+ */
+ public void addView(int viewId, RemoteViews nestedView, int index) {
+ addAction(new ViewGroupActionAdd(viewId, nestedView, index));
+ }
+
+ /**
+ * Equivalent to calling {@link ViewGroup#removeAllViews()}.
+ *
+ * @param viewId The id of the parent {@link ViewGroup} to remove all
+ * children from.
+ */
+ public void removeAllViews(int viewId) {
+ addAction(new ViewGroupActionRemove(viewId));
+ }
+
+ /**
+ * Removes all views in the {@link ViewGroup} specified by the {@code viewId} except for any
+ * child that has the {@code viewIdToKeep} as its id.
+ *
+ * @param viewId The id of the parent {@link ViewGroup} to remove children from.
+ * @param viewIdToKeep The id of a child that should not be removed.
+ *
+ * @hide
+ */
+ public void removeAllViewsExceptId(int viewId, int viewIdToKeep) {
+ addAction(new ViewGroupActionRemove(viewId, viewIdToKeep));
+ }
+
+ /**
+ * Equivalent to calling {@link AdapterViewAnimator#showNext()}
+ *
+ * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()}
+ */
+ public void showNext(int viewId) {
+ addAction(new ReflectionActionWithoutParams(viewId, "showNext"));
+ }
+
+ /**
+ * Equivalent to calling {@link AdapterViewAnimator#showPrevious()}
+ *
+ * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()}
+ */
+ public void showPrevious(int viewId) {
+ addAction(new ReflectionActionWithoutParams(viewId, "showPrevious"));
+ }
+
+ /**
+ * Equivalent to calling {@link AdapterViewAnimator#setDisplayedChild(int)}
+ *
+ * @param viewId The id of the view on which to call
+ * {@link AdapterViewAnimator#setDisplayedChild(int)}
+ */
+ public void setDisplayedChild(int viewId, int childIndex) {
+ setInt(viewId, "setDisplayedChild", childIndex);
+ }
+
+ /**
+ * Equivalent to calling {@link View#setVisibility(int)}
+ *
+ * @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) {
+ setInt(viewId, "setVisibility", visibility);
+ }
+
+ /**
+ * Equivalent to calling {@link TextView#setText(CharSequence)}
+ *
+ * @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) {
+ setCharSequence(viewId, "setText", text);
+ }
+
+ /**
+ * Equivalent to calling {@link TextView#setTextSize(int, float)}
+ *
+ * @param viewId The id of the view whose text size should change
+ * @param units The units of size (e.g. COMPLEX_UNIT_SP)
+ * @param size The size of the text
+ */
+ public void setTextViewTextSize(int viewId, int units, float size) {
+ addAction(new TextViewSizeAction(viewId, units, size));
+ }
+
+ /**
+ * Equivalent to calling
+ * {@link TextView#setCompoundDrawablesWithIntrinsicBounds(int, int, int, int)}.
+ *
+ * @param viewId The id of the view whose text should change
+ * @param left The id of a drawable to place to the left of the text, or 0
+ * @param top The id of a drawable to place above the text, or 0
+ * @param right The id of a drawable to place to the right of the text, or 0
+ * @param bottom The id of a drawable to place below the text, or 0
+ */
+ public void setTextViewCompoundDrawables(int viewId, int left, int top, int right, int bottom) {
+ addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
+ }
+
+ /**
+ * Equivalent to calling {@link
+ * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, int)}.
+ *
+ * @param viewId The id of the view whose text should change
+ * @param start The id of a drawable to place before the text (relative to the
+ * layout direction), or 0
+ * @param top The id of a drawable to place above the text, or 0
+ * @param end The id of a drawable to place after the text, or 0
+ * @param bottom The id of a drawable to place below the text, or 0
+ */
+ public void setTextViewCompoundDrawablesRelative(int viewId, int start, int top, int end, int bottom) {
+ addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
+ }
+
+ /**
+ * Equivalent to applying a color filter on one of the drawables in
+ * {@link android.widget.TextView#getCompoundDrawablesRelative()}.
+ *
+ * @param viewId The id of the view whose text should change.
+ * @param index The index of the drawable in the array of
+ * {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color
+ * filter on. Must be in [0, 3].
+ * @param color The color of the color filter. See
+ * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}.
+ * @param mode The mode of the color filter. See
+ * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}.
+ * @hide
+ */
+ public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId,
+ int index, int color, PorterDuff.Mode mode) {
+ if (index < 0 || index >= 4) {
+ throw new IllegalArgumentException("index must be in range [0, 3].");
+ }
+ addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode));
+ }
+
+ /**
+ * Equivalent to calling {@link
+ * TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
+ * using the drawables yielded by {@link Icon#loadDrawable(Context)}.
+ *
+ * @param viewId The id of the view whose text should change
+ * @param left an Icon to place to the left of the text, or 0
+ * @param top an Icon to place above the text, or 0
+ * @param right an Icon to place to the right of the text, or 0
+ * @param bottom an Icon to place below the text, or 0
+ *
+ * @hide
+ */
+ public void setTextViewCompoundDrawables(int viewId, Icon left, Icon top, Icon right, Icon bottom) {
+ addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
+ }
+
+ /**
+ * Equivalent to calling {@link
+ * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
+ * using the drawables yielded by {@link Icon#loadDrawable(Context)}.
+ *
+ * @param viewId The id of the view whose text should change
+ * @param start an Icon to place before the text (relative to the
+ * layout direction), or 0
+ * @param top an Icon to place above the text, or 0
+ * @param end an Icon to place after the text, or 0
+ * @param bottom an Icon to place below the text, or 0
+ *
+ * @hide
+ */
+ public void setTextViewCompoundDrawablesRelative(int viewId, Icon start, Icon top, Icon end, Icon bottom) {
+ addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
+ }
+
+ /**
+ * Equivalent to calling {@link ImageView#setImageResource(int)}
+ *
+ * @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) {
+ setInt(viewId, "setImageResource", srcId);
+ }
+
+ /**
+ * Equivalent to calling {@link ImageView#setImageURI(Uri)}
+ *
+ * @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) {
+ setUri(viewId, "setImageURI", uri);
+ }
+
+ /**
+ * Equivalent to calling {@link ImageView#setImageBitmap(Bitmap)}
+ *
+ * @param viewId The id of the view whose bitmap should change
+ * @param bitmap The new Bitmap for the drawable
+ */
+ public void setImageViewBitmap(int viewId, Bitmap bitmap) {
+ setBitmap(viewId, "setImageBitmap", bitmap);
+ }
+
+ /**
+ * Equivalent to calling {@link ImageView#setImageIcon(Icon)}
+ *
+ * @param viewId The id of the view whose bitmap should change
+ * @param icon The new Icon for the ImageView
+ */
+ public void setImageViewIcon(int viewId, Icon icon) {
+ setIcon(viewId, "setImageIcon", icon);
+ }
+
+ /**
+ * Equivalent to calling {@link AdapterView#setEmptyView(View)}
+ *
+ * @param viewId The id of the view on which to set the empty view
+ * @param emptyViewId The view id of the empty view
+ */
+ public void setEmptyView(int viewId, int emptyViewId) {
+ addAction(new SetEmptyView(viewId, emptyViewId));
+ }
+
+ /**
+ * 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 {@link Chronometer} to 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 started True if you want the clock to be started, false if not.
+ *
+ * @see #setChronometerCountDown(int, boolean)
+ */
+ public void setChronometer(int viewId, long base, String format, boolean started) {
+ setLong(viewId, "setBase", base);
+ setString(viewId, "setFormat", format);
+ setBoolean(viewId, "setStarted", started);
+ }
+
+ /**
+ * Equivalent to calling {@link Chronometer#setCountDown(boolean) Chronometer.setCountDown} on
+ * the chronometer with the given viewId.
+ *
+ * @param viewId The id of the {@link Chronometer} to change
+ * @param isCountDown True if you want the chronometer to count down to base instead of
+ * counting up.
+ */
+ public void setChronometerCountDown(int viewId, boolean isCountDown) {
+ setBoolean(viewId, "setCountDown", isCountDown);
+ }
+
+ /**
+ * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax},
+ * {@link ProgressBar#setProgress ProgressBar.setProgress}, and
+ * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate}
+ *
+ * If indeterminate is true, then the values for max and progress are ignored.
+ *
+ * @param viewId The id of the {@link ProgressBar} to 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) {
+ setBoolean(viewId, "setIndeterminate", indeterminate);
+ if (!indeterminate) {
+ setInt(viewId, "setMax", max);
+ setInt(viewId, "setProgress", progress);
+ }
+ }
+
+ /**
+ * Equivalent to calling
+ * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
+ * to launch the provided {@link PendingIntent}.
+ *
+ * When setting the on-click action of items within collections (eg. {@link ListView},
+ * {@link StackView} etc.), this method will not work. Instead, use {@link
+ * RemoteViews#setPendingIntentTemplate(int, PendingIntent)} in conjunction with
+ * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
+ *
+ * @param viewId The id of the view that will trigger the {@link PendingIntent} when clicked
+ * @param pendingIntent The {@link PendingIntent} to send when user clicks
+ */
+ public void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) {
+ addAction(new SetOnClickPendingIntent(viewId, pendingIntent));
+ }
+
+ /**
+ * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
+ * costly to set PendingIntents on the individual items, and is hence not permitted. Instead
+ * this method should be used to set a single PendingIntent template on the collection, and
+ * individual items can differentiate their on-click behavior using
+ * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
+ *
+ * @param viewId The id of the collection who's children will use this PendingIntent template
+ * when clicked
+ * @param pendingIntentTemplate The {@link PendingIntent} to be combined with extras specified
+ * by a child of viewId and executed when that child is clicked
+ */
+ public void setPendingIntentTemplate(int viewId, PendingIntent pendingIntentTemplate) {
+ addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
+ }
+
+ /**
+ * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
+ * costly to set PendingIntents on the individual items, and is hence not permitted. Instead
+ * a single PendingIntent template can be set on the collection, see {@link
+ * RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click
+ * action of a given item can be distinguished by setting a fillInIntent on that item. The
+ * fillInIntent is then combined with the PendingIntent template in order to determine the final
+ * intent which will be executed when the item is clicked. This works as follows: any fields
+ * which are left blank in the PendingIntent template, but are provided by the fillInIntent
+ * will be overwritten, and the resulting PendingIntent will be used. The rest
+ * of the PendingIntent template will then be filled in with the associated fields that are
+ * set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details.
+ *
+ * @param viewId The id of the view on which to set the fillInIntent
+ * @param fillInIntent The intent which will be combined with the parent's PendingIntent
+ * in order to determine the on-click behavior of the view specified by viewId
+ */
+ public void setOnClickFillInIntent(int viewId, Intent fillInIntent) {
+ addAction(new SetOnClickFillInIntent(viewId, fillInIntent));
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling a combination of {@link Drawable#setAlpha(int)},
+ * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
+ * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given
+ * view.
+ * <p>
+ * You can omit specific calls by marking their values with null or -1.
+ *
+ * @param viewId The id of the view that contains the target
+ * {@link Drawable}
+ * @param targetBackground If true, apply these parameters to the
+ * {@link Drawable} returned by
+ * {@link android.view.View#getBackground()}. Otherwise, assume
+ * the target view is an {@link ImageView} and apply them to
+ * {@link ImageView#getDrawable()}.
+ * @param alpha Specify an alpha value for the drawable, or -1 to leave
+ * unchanged.
+ * @param colorFilter Specify a color for a
+ * {@link android.graphics.ColorFilter} for this drawable. This will be ignored if
+ * {@code mode} is {@code null}.
+ * @param mode Specify a PorterDuff mode for this drawable, or null to leave
+ * unchanged.
+ * @param level Specify the level for the drawable, or -1 to leave
+ * unchanged.
+ */
+ public void setDrawableParameters(int viewId, boolean targetBackground, int alpha,
+ int colorFilter, PorterDuff.Mode mode, int level) {
+ addAction(new SetDrawableParameters(viewId, targetBackground, alpha,
+ colorFilter, mode, level));
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}.
+ *
+ * @param viewId The id of the view whose tint should change
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ */
+ public void setProgressTintList(int viewId, ColorStateList tint) {
+ addAction(new ReflectionAction(viewId, "setProgressTintList",
+ ReflectionAction.COLOR_STATE_LIST, tint));
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}.
+ *
+ * @param viewId The id of the view whose tint should change
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ */
+ public void setProgressBackgroundTintList(int viewId, ColorStateList tint) {
+ addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList",
+ ReflectionAction.COLOR_STATE_LIST, tint));
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}.
+ *
+ * @param viewId The id of the view whose tint should change
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ */
+ public void setProgressIndeterminateTintList(int viewId, ColorStateList tint) {
+ addAction(new ReflectionAction(viewId, "setIndeterminateTintList",
+ ReflectionAction.COLOR_STATE_LIST, tint));
+ }
+
+ /**
+ * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}.
+ *
+ * @param viewId The id of the view whose text color should change
+ * @param color Sets the text color for all the states (normal, selected,
+ * focused) to be this color.
+ */
+ public void setTextColor(int viewId, @ColorInt int color) {
+ setInt(viewId, "setTextColor", color);
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling {@link android.widget.TextView#setTextColor(ColorStateList)}.
+ *
+ * @param viewId The id of the view whose text color should change
+ * @param colors the text colors to set
+ */
+ public void setTextColor(int viewId, @ColorInt ColorStateList colors) {
+ addAction(new ReflectionAction(viewId, "setTextColor", ReflectionAction.COLOR_STATE_LIST,
+ colors));
+ }
+
+ /**
+ * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
+ *
+ * @param appWidgetId The id of the app widget which contains the specified view. (This
+ * parameter is ignored in this deprecated method)
+ * @param viewId The id of the {@link AdapterView}
+ * @param intent The intent of the service which will be
+ * providing data to the RemoteViewsAdapter
+ * @deprecated This method has been deprecated. See
+ * {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)}
+ */
+ @Deprecated
+ public void setRemoteAdapter(int appWidgetId, int viewId, Intent intent) {
+ setRemoteAdapter(viewId, intent);
+ }
+
+ /**
+ * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
+ * Can only be used for App Widgets.
+ *
+ * @param viewId The id of the {@link AdapterView}
+ * @param intent The intent of the service which will be
+ * providing data to the RemoteViewsAdapter
+ */
+ public void setRemoteAdapter(int viewId, Intent intent) {
+ addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
+ }
+
+ /**
+ * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
+ * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
+ * This is a simpler but less flexible approach to populating collection widgets. Its use is
+ * encouraged for most scenarios, as long as the total memory within the list of RemoteViews
+ * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
+ * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
+ * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
+ *
+ * This API is supported in the compatibility library for previous API levels, see
+ * RemoteViewsCompat.
+ *
+ * @param viewId The id of the {@link AdapterView}
+ * @param list The list of RemoteViews which will populate the view specified by viewId.
+ * @param viewTypeCount The maximum number of unique layout id's used to construct the list of
+ * RemoteViews. This count cannot change during the life-cycle of a given widget, so this
+ * parameter should account for the maximum possible number of types that may appear in the
+ * See {@link Adapter#getViewTypeCount()}.
+ *
+ * @hide
+ */
+ public void setRemoteAdapter(int viewId, ArrayList<RemoteViews> list, int viewTypeCount) {
+ addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount));
+ }
+
+ /**
+ * Equivalent to calling {@link ListView#smoothScrollToPosition(int)}.
+ *
+ * @param viewId The id of the view to change
+ * @param position Scroll to this adapter position
+ */
+ public void setScrollPosition(int viewId, int position) {
+ setInt(viewId, "smoothScrollToPosition", position);
+ }
+
+ /**
+ * Equivalent to calling {@link ListView#smoothScrollByOffset(int)}.
+ *
+ * @param viewId The id of the view to change
+ * @param offset Scroll by this adapter position offset
+ */
+ public void setRelativeScrollPosition(int viewId, int offset) {
+ setInt(viewId, "smoothScrollByOffset", offset);
+ }
+
+ /**
+ * Equivalent to calling {@link android.view.View#setPadding(int, int, int, int)}.
+ *
+ * @param viewId The id of the view to change
+ * @param left the left padding in pixels
+ * @param top the top padding in pixels
+ * @param right the right padding in pixels
+ * @param bottom the bottom padding in pixels
+ */
+ public void setViewPadding(int viewId, int left, int top, int right, int bottom) {
+ addAction(new ViewPaddingAction(viewId, left, top, right, bottom));
+ }
+
+ /**
+ * @hide
+ * Equivalent to calling {@link android.view.ViewGroup.MarginLayoutParams#setMarginEnd(int)}.
+ * Only works if the {@link View#getLayoutParams()} supports margins.
+ * Hidden for now since we don't want to support this for all different layout margins yet.
+ *
+ * @param viewId The id of the view to change
+ * @param endMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
+ */
+ public void setViewLayoutMarginEndDimen(int viewId, @DimenRes int endMarginDimen) {
+ addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_END_DIMEN,
+ endMarginDimen));
+ }
+
+ /**
+ * Equivalent to setting {@link android.view.ViewGroup.MarginLayoutParams#bottomMargin}.
+ *
+ * @param bottomMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
+ * @hide
+ */
+ public void setViewLayoutMarginBottomDimen(int viewId, @DimenRes int bottomMarginDimen) {
+ addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_BOTTOM_DIMEN,
+ bottomMarginDimen));
+ }
+
+ /**
+ * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width}.
+ *
+ * @param layoutWidth one of 0, MATCH_PARENT or WRAP_CONTENT. Other sizes are not allowed
+ * because they behave poorly when the density changes.
+ * @hide
+ */
+ public void setViewLayoutWidth(int viewId, int layoutWidth) {
+ if (layoutWidth != 0 && layoutWidth != ViewGroup.LayoutParams.MATCH_PARENT
+ && layoutWidth != ViewGroup.LayoutParams.WRAP_CONTENT) {
+ throw new IllegalArgumentException("Only supports 0, WRAP_CONTENT and MATCH_PARENT");
+ }
+ mActions.add(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, layoutWidth));
+ }
+
+ /**
+ * Call a method taking one boolean on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setBoolean(int viewId, String methodName, boolean value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BOOLEAN, value));
+ }
+
+ /**
+ * Call a method taking one byte on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setByte(int viewId, String methodName, byte value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BYTE, value));
+ }
+
+ /**
+ * Call a method taking one short on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setShort(int viewId, String methodName, short value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.SHORT, value));
+ }
+
+ /**
+ * Call a method taking one int on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setInt(int viewId, String methodName, int value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
+ }
+
+ /**
+ * Call a method taking one long on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setLong(int viewId, String methodName, long value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.LONG, value));
+ }
+
+ /**
+ * Call a method taking one float on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setFloat(int viewId, String methodName, float value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value));
+ }
+
+ /**
+ * Call a method taking one double on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setDouble(int viewId, String methodName, double value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.DOUBLE, value));
+ }
+
+ /**
+ * Call a method taking one char on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setChar(int viewId, String methodName, char value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR, value));
+ }
+
+ /**
+ * Call a method taking one String on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setString(int viewId, String methodName, String value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.STRING, value));
+ }
+
+ /**
+ * Call a method taking one CharSequence on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setCharSequence(int viewId, String methodName, CharSequence value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
+ }
+
+ /**
+ * Call a method taking one Uri on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setUri(int viewId, String methodName, Uri value) {
+ if (value != null) {
+ // Resolve any filesystem path before sending remotely
+ value = value.getCanonicalUri();
+ if (StrictMode.vmFileUriExposureEnabled()) {
+ value.checkFileUriExposed("RemoteViews.setUri()");
+ }
+ }
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.URI, value));
+ }
+
+ /**
+ * Call a method taking one Bitmap on a view in the layout for this RemoteViews.
+ * @more
+ * <p class="note">The bitmap will be flattened into the parcel if this object is
+ * sent across processes, so it may end up using a lot of memory, and may be fairly slow.</p>
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setBitmap(int viewId, String methodName, Bitmap value) {
+ addAction(new BitmapReflectionAction(viewId, methodName, value));
+ }
+
+ /**
+ * Call a method taking one Bundle on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The value to pass to the method.
+ */
+ public void setBundle(int viewId, String methodName, Bundle value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BUNDLE, value));
+ }
+
+ /**
+ * Call a method taking one Intent on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The {@link android.content.Intent} to pass the method.
+ */
+ public void setIntent(int viewId, String methodName, Intent value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INTENT, value));
+ }
+
+ /**
+ * Call a method taking one Icon on a view in the layout for this RemoteViews.
+ *
+ * @param viewId The id of the view on which to call the method.
+ * @param methodName The name of the method to call.
+ * @param value The {@link android.graphics.drawable.Icon} to pass the method.
+ */
+ public void setIcon(int viewId, String methodName, Icon value) {
+ addAction(new ReflectionAction(viewId, methodName, ReflectionAction.ICON, value));
+ }
+
+ /**
+ * Equivalent to calling View.setContentDescription(CharSequence).
+ *
+ * @param viewId The id of the view whose content description should change.
+ * @param contentDescription The new content description for the view.
+ */
+ public void setContentDescription(int viewId, CharSequence contentDescription) {
+ setCharSequence(viewId, "setContentDescription", contentDescription);
+ }
+
+ /**
+ * Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}.
+ *
+ * @param viewId The id of the view whose before view in accessibility traversal to set.
+ * @param nextId The id of the next in the accessibility traversal.
+ **/
+ public void setAccessibilityTraversalBefore(int viewId, int nextId) {
+ setInt(viewId, "setAccessibilityTraversalBefore", nextId);
+ }
+
+ /**
+ * Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}.
+ *
+ * @param viewId The id of the view whose after view in accessibility traversal to set.
+ * @param nextId The id of the next in the accessibility traversal.
+ **/
+ public void setAccessibilityTraversalAfter(int viewId, int nextId) {
+ setInt(viewId, "setAccessibilityTraversalAfter", nextId);
+ }
+
+ /**
+ * Equivalent to calling {@link View#setLabelFor(int)}.
+ *
+ * @param viewId The id of the view whose property to set.
+ * @param labeledId The id of a view for which this view serves as a label.
+ */
+ public void setLabelFor(int viewId, int labeledId) {
+ setInt(viewId, "setLabelFor", labeledId);
+ }
+
+ private RemoteViews getRemoteViewsToApply(Context context) {
+ if (hasLandscapeAndPortraitLayouts()) {
+ int orientation = context.getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return mLandscape;
+ } else {
+ return mPortrait;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 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) {
+ return apply(context, parent, null);
+ }
+
+ /** @hide */
+ public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
+ RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+ View result = inflateView(context, rvToApply, parent);
+ loadTransitionOverride(context, handler);
+
+ rvToApply.performApply(result, parent, handler);
+
+ return result;
+ }
+
+ private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
+ // RemoteViews may be built by an application installed in another
+ // user. So build a context that loads resources from that user but
+ // still returns the current users userId so settings like data / time formats
+ // are loaded without requiring cross user persmissions.
+ final Context contextForResources = getContextForResources(context);
+ Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);
+
+ LayoutInflater inflater = (LayoutInflater)
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ // Clone inflater so we load resources from correct context and
+ // we don't add a filter to the static version returned by getSystemService.
+ inflater = inflater.cloneInContext(inflationContext);
+ inflater.setFilter(this);
+ View v = inflater.inflate(rv.getLayoutId(), parent, false);
+ v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
+ return v;
+ }
+
+ private static void loadTransitionOverride(Context context,
+ RemoteViews.OnClickHandler handler) {
+ if (handler != null && context.getResources().getBoolean(
+ com.android.internal.R.bool.config_overrideRemoteViewsActivityTransition)) {
+ TypedArray windowStyle = context.getTheme().obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ int windowAnimations = windowStyle.getResourceId(
+ com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
+ TypedArray windowAnimationStyle = context.obtainStyledAttributes(
+ windowAnimations, com.android.internal.R.styleable.WindowAnimation);
+ handler.setEnterAnimationId(windowAnimationStyle.getResourceId(
+ com.android.internal.R.styleable.
+ WindowAnimation_activityOpenRemoteViewsEnterAnimation, 0));
+ windowStyle.recycle();
+ windowAnimationStyle.recycle();
+ }
+ }
+
+ /**
+ * Implement this interface to receive a callback when
+ * {@link #applyAsync} or {@link #reapplyAsync} is finished.
+ * @hide
+ */
+ public interface OnViewAppliedListener {
+ void onViewApplied(View v);
+
+ void onError(Exception e);
+ }
+
+ /**
+ * Applies the views asynchronously, moving as much of the task on the background
+ * thread as possible.
+ *
+ * @see #apply(Context, ViewGroup)
+ * @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.
+ * @param listener the callback to run when all actions have been applied. May be null.
+ * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used.
+ * @return CancellationSignal
+ * @hide
+ */
+ public CancellationSignal applyAsync(
+ Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) {
+ return applyAsync(context, parent, executor, listener, null);
+ }
+
+ private CancellationSignal startTaskOnExecutor(AsyncApplyTask task, Executor executor) {
+ CancellationSignal cancelSignal = new CancellationSignal();
+ cancelSignal.setOnCancelListener(task);
+
+ task.executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor);
+ return cancelSignal;
+ }
+
+ /** @hide */
+ public CancellationSignal applyAsync(Context context, ViewGroup parent,
+ Executor executor, OnViewAppliedListener listener, OnClickHandler handler) {
+ return startTaskOnExecutor(getAsyncApplyTask(context, parent, listener, handler), executor);
+ }
+
+ private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent,
+ OnViewAppliedListener listener, OnClickHandler handler) {
+ return new AsyncApplyTask(getRemoteViewsToApply(context), parent, context, listener,
+ handler, null);
+ }
+
+ private class AsyncApplyTask extends AsyncTask<Void, Void, ViewTree>
+ implements CancellationSignal.OnCancelListener {
+ final RemoteViews mRV;
+ final ViewGroup mParent;
+ final Context mContext;
+ final OnViewAppliedListener mListener;
+ final OnClickHandler mHandler;
+
+ private View mResult;
+ private ViewTree mTree;
+ private Action[] mActions;
+ private Exception mError;
+
+ private AsyncApplyTask(
+ RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener,
+ OnClickHandler handler, View result) {
+ mRV = rv;
+ mParent = parent;
+ mContext = context;
+ mListener = listener;
+ mHandler = handler;
+
+ mResult = result;
+ loadTransitionOverride(context, handler);
+ }
+
+ @Override
+ protected ViewTree doInBackground(Void... params) {
+ try {
+ if (mResult == null) {
+ mResult = inflateView(mContext, mRV, mParent);
+ }
+
+ mTree = new ViewTree(mResult);
+ if (mRV.mActions != null) {
+ int count = mRV.mActions.size();
+ mActions = new Action[count];
+ for (int i = 0; i < count && !isCancelled(); i++) {
+ // TODO: check if isCancelled in nested views.
+ mActions[i] = mRV.mActions.get(i).initActionAsync(mTree, mParent, mHandler);
+ }
+ } else {
+ mActions = null;
+ }
+ return mTree;
+ } catch (Exception e) {
+ mError = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(ViewTree viewTree) {
+ if (mError == null) {
+ try {
+ if (mActions != null) {
+ OnClickHandler handler = mHandler == null
+ ? DEFAULT_ON_CLICK_HANDLER : mHandler;
+ for (Action a : mActions) {
+ a.apply(viewTree.mRoot, mParent, handler);
+ }
+ }
+ } catch (Exception e) {
+ mError = e;
+ }
+ }
+
+ if (mListener != null) {
+ if (mError != null) {
+ mListener.onError(mError);
+ } else {
+ mListener.onViewApplied(viewTree.mRoot);
+ }
+ } else if (mError != null) {
+ if (mError instanceof ActionException) {
+ throw (ActionException) mError;
+ } else {
+ throw new ActionException(mError);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ cancel(true);
+ }
+ }
+
+ /**
+ * 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) {
+ reapply(context, v, null);
+ }
+
+ /** @hide */
+ public void reapply(Context context, View v, OnClickHandler handler) {
+ RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+ // In the case that a view has this RemoteViews applied in one orientation, is persisted
+ // across orientation change, and has the RemoteViews re-applied in the new orientation,
+ // we throw an exception, since the layouts may be completely unrelated.
+ if (hasLandscapeAndPortraitLayouts()) {
+ if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
+ throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
+ " that does not share the same root layout id.");
+ }
+ }
+
+ rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
+ }
+
+ /**
+ * Applies all the actions to the provided view, moving as much of the task on the background
+ * thread as possible.
+ *
+ * @see #reapply(Context, View)
+ * @param context Default context to use
+ * @param v The view to apply the actions to. This should be the result of
+ * the {@link #apply(Context,ViewGroup)} call.
+ * @param listener the callback to run when all actions have been applied. May be null.
+ * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used
+ * @return CancellationSignal
+ * @hide
+ */
+ public CancellationSignal reapplyAsync(
+ Context context, View v, Executor executor, OnViewAppliedListener listener) {
+ return reapplyAsync(context, v, executor, listener, null);
+ }
+
+ /** @hide */
+ public CancellationSignal reapplyAsync(Context context, View v, Executor executor,
+ OnViewAppliedListener listener, OnClickHandler handler) {
+ RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+ // In the case that a view has this RemoteViews applied in one orientation, is persisted
+ // across orientation change, and has the RemoteViews re-applied in the new orientation,
+ // we throw an exception, since the layouts may be completely unrelated.
+ if (hasLandscapeAndPortraitLayouts()) {
+ if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
+ throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
+ " that does not share the same root layout id.");
+ }
+ }
+
+ return startTaskOnExecutor(new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
+ context, listener, handler, v), executor);
+ }
+
+ private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
+ if (mActions != null) {
+ handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
+ final int count = mActions.size();
+ for (int i = 0; i < count; i++) {
+ Action a = mActions.get(i);
+ a.apply(v, parent, handler);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the RemoteViews contains potentially costly operations and should be
+ * applied asynchronously.
+ *
+ * @hide
+ */
+ public boolean prefersAsyncApply() {
+ if (mActions != null) {
+ final int count = mActions.size();
+ for (int i = 0; i < count; i++) {
+ if (mActions.get(i).prefersAsyncApply()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private Context getContextForResources(Context context) {
+ if (mApplication != null) {
+ if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
+ && context.getPackageName().equals(mApplication.packageName)) {
+ return context;
+ }
+ try {
+ return context.createApplicationContext(mApplication,
+ Context.CONTEXT_RESTRICTED);
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
+ }
+ }
+
+ return context;
+ }
+
+ /**
+ * Returns the number of actions in this RemoteViews. Can be used as a sequence number.
+ *
+ * @hide
+ */
+ public int getSequenceNumber() {
+ return (mActions == null) ? 0 : mActions.size();
+ }
+
+ /* (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) {
+ if (!hasLandscapeAndPortraitLayouts()) {
+ dest.writeInt(MODE_NORMAL);
+ // We only write the bitmap cache if we are the root RemoteViews, as this cache
+ // is shared by all children.
+ if (mIsRoot) {
+ mBitmapCache.writeBitmapsToParcel(dest, flags);
+ }
+ if (!mIsRoot && (flags & PARCELABLE_ELIDE_DUPLICATES) != 0) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mApplication.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mLayoutId);
+ dest.writeInt(mIsWidgetCollectionChild ? 1 : 0);
+ 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, a.hasSameAppInfo(mApplication)
+ ? PARCELABLE_ELIDE_DUPLICATES : 0);
+ }
+ } else {
+ dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT);
+ // We only write the bitmap cache if we are the root RemoteViews, as this cache
+ // is shared by all children.
+ if (mIsRoot) {
+ mBitmapCache.writeBitmapsToParcel(dest, flags);
+ }
+ mLandscape.writeToParcel(dest, flags);
+ // Both RemoteViews already share the same package and user
+ mPortrait.writeToParcel(dest, flags | PARCELABLE_ELIDE_DUPLICATES);
+ }
+ dest.writeInt(mReapplyDisallowed ? 1 : 0);
+ }
+
+ private static ApplicationInfo getApplicationInfo(String packageName, int userId) {
+ if (packageName == null) {
+ return null;
+ }
+
+ // Get the application for the passed in package and user.
+ Application application = ActivityThread.currentApplication();
+ if (application == null) {
+ throw new IllegalStateException("Cannot create remote views out of an aplication.");
+ }
+
+ ApplicationInfo applicationInfo = application.getApplicationInfo();
+ if (UserHandle.getUserId(applicationInfo.uid) != userId
+ || !applicationInfo.packageName.equals(packageName)) {
+ try {
+ Context context = application.getBaseContext().createPackageContextAsUser(
+ packageName, 0, new UserHandle(userId));
+ applicationInfo = context.getApplicationInfo();
+ } catch (NameNotFoundException nnfe) {
+ throw new IllegalArgumentException("No such package " + packageName);
+ }
+ }
+
+ return applicationInfo;
+ }
+
+ /**
+ * 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];
+ }
+ };
+
+ /**
+ * A representation of the view hierarchy. Only views which have a valid ID are added
+ * and can be searched.
+ */
+ private static class ViewTree {
+ private static final int INSERT_AT_END_INDEX = -1;
+ private View mRoot;
+ private ArrayList<ViewTree> mChildren;
+
+ private ViewTree(View root) {
+ mRoot = root;
+ }
+
+ public void createTree() {
+ if (mChildren != null) {
+ return;
+ }
+
+ mChildren = new ArrayList<>();
+ if (mRoot instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) mRoot;
+ int count = vg.getChildCount();
+ for (int i = 0; i < count; i++) {
+ addViewChild(vg.getChildAt(i));
+ }
+ }
+ }
+
+ public ViewTree findViewTreeById(int id) {
+ if (mRoot.getId() == id) {
+ return this;
+ }
+ if (mChildren == null) {
+ return null;
+ }
+ for (ViewTree tree : mChildren) {
+ ViewTree result = tree.findViewTreeById(id);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ public void replaceView(View v) {
+ mRoot = v;
+ mChildren = null;
+ createTree();
+ }
+
+ public <T extends View> T findViewById(int id) {
+ if (mChildren == null) {
+ return mRoot.findViewById(id);
+ }
+ ViewTree tree = findViewTreeById(id);
+ return tree == null ? null : (T) tree.mRoot;
+ }
+
+ public void addChild(ViewTree child) {
+ addChild(child, INSERT_AT_END_INDEX);
+ }
+
+ /**
+ * Adds the given {@link ViewTree} as a child at the given index.
+ *
+ * @param index The position at which to add the child or -1 to add last.
+ */
+ public void addChild(ViewTree child, int index) {
+ if (mChildren == null) {
+ mChildren = new ArrayList<>();
+ }
+ child.createTree();
+
+ if (index == INSERT_AT_END_INDEX) {
+ mChildren.add(child);
+ return;
+ }
+
+ mChildren.add(index, child);
+ }
+
+ private void addViewChild(View v) {
+ // ViewTree only contains Views which can be found using findViewById.
+ // If isRootNamespace is true, this view is skipped.
+ // @see ViewGroup#findViewTraversal(int)
+ if (v.isRootNamespace()) {
+ return;
+ }
+ final ViewTree target;
+
+ // If the view has a valid id, i.e., if can be found using findViewById, add it to the
+ // tree, otherwise skip this view and add its children instead.
+ if (v.getId() != 0) {
+ ViewTree tree = new ViewTree(v);
+ mChildren.add(tree);
+ target = tree;
+ } else {
+ target = this;
+ }
+
+ if (v instanceof ViewGroup) {
+ if (target.mChildren == null) {
+ target.mChildren = new ArrayList<>();
+ ViewGroup vg = (ViewGroup) v;
+ int count = vg.getChildCount();
+ for (int i = 0; i < count; i++) {
+ target.addViewChild(vg.getChildAt(i));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/widget/RemoteViewsAdapter.java b/android/widget/RemoteViewsAdapter.java
new file mode 100644
index 0000000..0968652
--- /dev/null
+++ b/android/widget/RemoteViewsAdapter.java
@@ -0,0 +1,1353 @@
+/*
+ * 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.Manifest;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.OnClickHandler;
+
+import com.android.internal.widget.IRemoteViewsAdapterConnection;
+import com.android.internal.widget.IRemoteViewsFactory;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.concurrent.Executor;
+
+/**
+ * An adapter to a RemoteViewsService which fetches and caches RemoteViews
+ * to be later inflated as child views.
+ */
+/** @hide */
+public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback {
+ private static final String MULTI_USER_PERM = Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+
+ private static final String TAG = "RemoteViewsAdapter";
+
+ // The max number of items in the cache
+ private static final int sDefaultCacheSize = 40;
+ // The delay (in millis) to wait until attempting to unbind from a service after a request.
+ // This ensures that we don't stay continually bound to the service and that it can be destroyed
+ // if we need the memory elsewhere in the system.
+ private static final int sUnbindServiceDelay = 5000;
+
+ // Default height for the default loading view, in case we cannot get inflate the first view
+ private static final int sDefaultLoadingViewHeight = 50;
+
+ // Type defs for controlling different messages across the main and worker message queues
+ private static final int sDefaultMessageType = 0;
+ private static final int sUnbindServiceMessageType = 1;
+
+ private final Context mContext;
+ private final Intent mIntent;
+ private final int mAppWidgetId;
+ private final Executor mAsyncViewLoadExecutor;
+
+ private RemoteViewsAdapterServiceConnection mServiceConnection;
+ private WeakReference<RemoteAdapterConnectionCallback> mCallback;
+ private OnClickHandler mRemoteViewsOnClickHandler;
+ private final FixedSizeRemoteViewsCache mCache;
+ private int mVisibleWindowLowerBound;
+ private int mVisibleWindowUpperBound;
+
+ // A flag to determine whether we should notify data set changed after we connect
+ private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
+
+ // The set of requested views that are to be notified when the associated RemoteViews are
+ // loaded.
+ private RemoteViewsFrameLayoutRefSet mRequestedViews;
+
+ private HandlerThread mWorkerThread;
+ // items may be interrupted within the normally processed queues
+ private Handler mWorkerQueue;
+ private Handler mMainQueue;
+
+ // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
+ // structures;
+ private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache>
+ sCachedRemoteViewsCaches = new HashMap<>();
+ private static final HashMap<RemoteViewsCacheKey, Runnable>
+ sRemoteViewsCacheRemoveRunnables = new HashMap<>();
+
+ private static HandlerThread sCacheRemovalThread;
+ private static Handler sCacheRemovalQueue;
+
+ // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation.
+ // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this
+ // duration, the cache is dropped.
+ private static final int REMOTE_VIEWS_CACHE_DURATION = 5000;
+
+ // Used to indicate to the AdapterView that it can use this Adapter immediately after
+ // construction (happens when we have a cached FixedSizeRemoteViewsCache).
+ private boolean mDataReady = false;
+
+ /**
+ * An interface for the RemoteAdapter to notify other classes when adapters
+ * are actually connected to/disconnected from their actual services.
+ */
+ public interface RemoteAdapterConnectionCallback {
+ /**
+ * @return whether the adapter was set or not.
+ */
+ boolean onRemoteAdapterConnected();
+
+ void onRemoteAdapterDisconnected();
+
+ /**
+ * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+ * connected yet.
+ */
+ void deferNotifyDataSetChanged();
+
+ void setRemoteViewsAdapter(Intent intent, boolean isAsync);
+ }
+
+ public static class AsyncRemoteAdapterAction implements Runnable {
+
+ private final RemoteAdapterConnectionCallback mCallback;
+ private final Intent mIntent;
+
+ public AsyncRemoteAdapterAction(RemoteAdapterConnectionCallback callback, Intent intent) {
+ mCallback = callback;
+ mIntent = intent;
+ }
+
+ @Override
+ public void run() {
+ mCallback.setRemoteViewsAdapter(mIntent, true);
+ }
+ }
+
+ /**
+ * The service connection that gets populated when the RemoteViewsService is
+ * bound. This must be a static inner class to ensure that no references to the outer
+ * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
+ * garbage collected, and would cause us to leak activities due to the caching mechanism for
+ * FrameLayouts in the adapter).
+ */
+ private static class RemoteViewsAdapterServiceConnection extends
+ IRemoteViewsAdapterConnection.Stub {
+ private boolean mIsConnected;
+ private boolean mIsConnecting;
+ private WeakReference<RemoteViewsAdapter> mAdapter;
+ private IRemoteViewsFactory mRemoteViewsFactory;
+
+ public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
+ mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
+ }
+
+ public synchronized void bind(Context context, int appWidgetId, Intent intent) {
+ if (!mIsConnecting) {
+ try {
+ RemoteViewsAdapter adapter;
+ final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
+ if ((adapter = mAdapter.get()) != null) {
+ mgr.bindRemoteViewsService(context.getOpPackageName(), appWidgetId,
+ intent, asBinder());
+ } else {
+ Slog.w(TAG, "bind: adapter was null");
+ }
+ mIsConnecting = true;
+ } catch (Exception e) {
+ Log.e("RVAServiceConnection", "bind(): " + e.getMessage());
+ mIsConnecting = false;
+ mIsConnected = false;
+ }
+ }
+ }
+
+ public synchronized void unbind(Context context, int appWidgetId, Intent intent) {
+ try {
+ RemoteViewsAdapter adapter;
+ final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
+ if ((adapter = mAdapter.get()) != null) {
+ mgr.unbindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent);
+ } else {
+ Slog.w(TAG, "unbind: adapter was null");
+ }
+ mIsConnecting = false;
+ } catch (Exception e) {
+ Log.e("RVAServiceConnection", "unbind(): " + e.getMessage());
+ mIsConnecting = false;
+ mIsConnected = false;
+ }
+ }
+
+ public synchronized void onServiceConnected(IBinder service) {
+ mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
+
+ // Remove any deferred unbind messages
+ final RemoteViewsAdapter adapter = mAdapter.get();
+ if (adapter == null) return;
+
+ // Queue up work that we need to do for the callback to run
+ adapter.mWorkerQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ if (adapter.mNotifyDataSetChangedAfterOnServiceConnected) {
+ // Handle queued notifyDataSetChanged() if necessary
+ adapter.onNotifyDataSetChanged();
+ } else {
+ IRemoteViewsFactory factory =
+ adapter.mServiceConnection.getRemoteViewsFactory();
+ try {
+ if (!factory.isCreated()) {
+ // We only call onDataSetChanged() if this is the factory was just
+ // create in response to this bind
+ factory.onDataSetChanged();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error notifying factory of data set changed in " +
+ "onServiceConnected(): " + e.getMessage());
+
+ // Return early to prevent anything further from being notified
+ // (effectively nothing has changed)
+ return;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error notifying factory of data set changed in " +
+ "onServiceConnected(): " + e.getMessage());
+ }
+
+ // Request meta data so that we have up to date data when calling back to
+ // the remote adapter callback
+ adapter.updateTemporaryMetaData();
+
+ // Notify the host that we've connected
+ adapter.mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (adapter.mCache) {
+ adapter.mCache.commitTemporaryMetaData();
+ }
+
+ final RemoteAdapterConnectionCallback callback =
+ adapter.mCallback.get();
+ if (callback != null) {
+ callback.onRemoteAdapterConnected();
+ }
+ }
+ });
+ }
+
+ // Enqueue unbind message
+ adapter.enqueueDeferredUnbindServiceMessage();
+ mIsConnected = true;
+ mIsConnecting = false;
+ }
+ });
+ }
+
+ public synchronized void onServiceDisconnected() {
+ mIsConnected = false;
+ mIsConnecting = false;
+ mRemoteViewsFactory = null;
+
+ // Clear the main/worker queues
+ final RemoteViewsAdapter adapter = mAdapter.get();
+ if (adapter == null) return;
+
+ adapter.mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ // Dequeue any unbind messages
+ adapter.mMainQueue.removeMessages(sUnbindServiceMessageType);
+
+ final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
+ if (callback != null) {
+ callback.onRemoteAdapterDisconnected();
+ }
+ }
+ });
+ }
+
+ public synchronized IRemoteViewsFactory getRemoteViewsFactory() {
+ return mRemoteViewsFactory;
+ }
+
+ public synchronized boolean isConnected() {
+ return mIsConnected;
+ }
+ }
+
+ /**
+ * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when
+ * they are loaded.
+ */
+ static class RemoteViewsFrameLayout extends AppWidgetHostView {
+ private final FixedSizeRemoteViewsCache mCache;
+
+ public RemoteViewsFrameLayout(Context context, FixedSizeRemoteViewsCache cache) {
+ super(context);
+ mCache = cache;
+ }
+
+ /**
+ * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
+ * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
+ * successfully.
+ * @param forceApplyAsync when true, the host will always try to inflate the view
+ * asynchronously (for eg, when we are already showing the loading
+ * view)
+ */
+ public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler,
+ boolean forceApplyAsync) {
+ setOnClickHandler(handler);
+ applyRemoteViews(view, forceApplyAsync || ((view != null) && view.prefersAsyncApply()));
+ }
+
+ /**
+ * Creates a default loading view. Uses the size of the first row as a guide for the
+ * size of the loading view.
+ */
+ @Override
+ protected View getDefaultView() {
+ int viewHeight = mCache.getMetaData().getLoadingTemplate(getContext()).defaultHeight;
+ // Compose the loading view text
+ TextView loadingTextView = (TextView) LayoutInflater.from(getContext()).inflate(
+ com.android.internal.R.layout.remote_views_adapter_default_loading_view,
+ this, false);
+ loadingTextView.setHeight(viewHeight);
+ return loadingTextView;
+ }
+
+ @Override
+ protected Context getRemoteContext() {
+ return null;
+ }
+
+ @Override
+ protected View getErrorView() {
+ // Use the default loading view as the error view.
+ return getDefaultView();
+ }
+ }
+
+ /**
+ * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the
+ * adapter that have not yet had their RemoteViews loaded.
+ */
+ private class RemoteViewsFrameLayoutRefSet {
+ private final SparseArray<LinkedList<RemoteViewsFrameLayout>> mReferences =
+ new SparseArray<>();
+ private final HashMap<RemoteViewsFrameLayout, LinkedList<RemoteViewsFrameLayout>>
+ mViewToLinkedList = new HashMap<>();
+
+ /**
+ * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
+ */
+ public void add(int position, RemoteViewsFrameLayout layout) {
+ LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position);
+
+ // Create the list if necessary
+ if (refs == null) {
+ refs = new LinkedList<RemoteViewsFrameLayout>();
+ mReferences.put(position, refs);
+ }
+ mViewToLinkedList.put(layout, refs);
+
+ // Add the references to the list
+ refs.add(layout);
+ }
+
+ /**
+ * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that
+ * the associated RemoteViews has loaded.
+ */
+ public void notifyOnRemoteViewsLoaded(int position, RemoteViews view) {
+ if (view == null) return;
+
+ final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position);
+ if (refs != null) {
+ // Notify all the references for that position of the newly loaded RemoteViews
+ for (final RemoteViewsFrameLayout ref : refs) {
+ ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler, true);
+ if (mViewToLinkedList.containsKey(ref)) {
+ mViewToLinkedList.remove(ref);
+ }
+ }
+ refs.clear();
+ // Remove this set from the original mapping
+ mReferences.remove(position);
+ }
+ }
+
+ /**
+ * We need to remove views from this set if they have been recycled by the AdapterView.
+ */
+ public void removeView(RemoteViewsFrameLayout rvfl) {
+ if (mViewToLinkedList.containsKey(rvfl)) {
+ mViewToLinkedList.get(rvfl).remove(rvfl);
+ mViewToLinkedList.remove(rvfl);
+ }
+ }
+
+ /**
+ * Removes all references to all RemoteViewsFrameLayouts returned by the adapter.
+ */
+ public void clear() {
+ // We currently just clear the references, and leave all the previous layouts returned
+ // in their default state of the loading view.
+ mReferences.clear();
+ mViewToLinkedList.clear();
+ }
+ }
+
+ /**
+ * The meta-data associated with the cache in it's current state.
+ */
+ private static class RemoteViewsMetaData {
+ int count;
+ int viewTypeCount;
+ boolean hasStableIds;
+
+ // Used to determine how to construct loading views. If a loading view is not specified
+ // by the user, then we try and load the first view, and use its height as the height for
+ // the default loading view.
+ LoadingViewTemplate loadingTemplate;
+
+ // A mapping from type id to a set of unique type ids
+ private final SparseIntArray mTypeIdIndexMap = new SparseIntArray();
+
+ public RemoteViewsMetaData() {
+ reset();
+ }
+
+ public void set(RemoteViewsMetaData d) {
+ synchronized (d) {
+ count = d.count;
+ viewTypeCount = d.viewTypeCount;
+ hasStableIds = d.hasStableIds;
+ loadingTemplate = d.loadingTemplate;
+ }
+ }
+
+ public void reset() {
+ count = 0;
+
+ // by default there is at least one dummy view type
+ viewTypeCount = 1;
+ hasStableIds = true;
+ loadingTemplate = null;
+ mTypeIdIndexMap.clear();
+ }
+
+ public int getMappedViewType(int typeId) {
+ int mappedTypeId = mTypeIdIndexMap.get(typeId, -1);
+ if (mappedTypeId == -1) {
+ // We +1 because the loading view always has view type id of 0
+ mappedTypeId = mTypeIdIndexMap.size() + 1;
+ mTypeIdIndexMap.put(typeId, mappedTypeId);
+ }
+ return mappedTypeId;
+ }
+
+ public boolean isViewTypeInRange(int typeId) {
+ int mappedType = getMappedViewType(typeId);
+ return (mappedType < viewTypeCount);
+ }
+
+ public synchronized LoadingViewTemplate getLoadingTemplate(Context context) {
+ if (loadingTemplate == null) {
+ loadingTemplate = new LoadingViewTemplate(null, context);
+ }
+ return loadingTemplate;
+ }
+ }
+
+ /**
+ * The meta-data associated with a single item in the cache.
+ */
+ private static class RemoteViewsIndexMetaData {
+ int typeId;
+ long itemId;
+
+ public RemoteViewsIndexMetaData(RemoteViews v, long itemId) {
+ set(v, itemId);
+ }
+
+ public void set(RemoteViews v, long id) {
+ itemId = id;
+ if (v != null) {
+ typeId = v.getLayoutId();
+ } else {
+ typeId = 0;
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private static class FixedSizeRemoteViewsCache {
+ private static final String TAG = "FixedSizeRemoteViewsCache";
+
+ // The meta data related to all the RemoteViews, ie. count, is stable, etc.
+ // The meta data objects are made final so that they can be locked on independently
+ // of the FixedSizeRemoteViewsCache. If we ever lock on both meta data objects, it is in
+ // the order mTemporaryMetaData followed by mMetaData.
+ private final RemoteViewsMetaData mMetaData = new RemoteViewsMetaData();
+ private final RemoteViewsMetaData mTemporaryMetaData = new RemoteViewsMetaData();
+
+ // The cache/mapping of position to RemoteViewsMetaData. This set is guaranteed to be
+ // greater than or equal to the set of RemoteViews.
+ // Note: The reason that we keep this separate from the RemoteViews cache below is that this
+ // we still need to be able to access the mapping of position to meta data, without keeping
+ // the heavy RemoteViews around. The RemoteViews cache is trimmed to fixed constraints wrt.
+ // memory and size, but this metadata cache will retain information until the data at the
+ // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged).
+ private final SparseArray<RemoteViewsIndexMetaData> mIndexMetaData = new SparseArray<>();
+
+ // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses
+ // too much memory.
+ private final SparseArray<RemoteViews> mIndexRemoteViews = new SparseArray<>();
+
+ // An array of indices to load, Indices which are explicitely requested are set to true,
+ // and those determined by the preloading algorithm to prefetch are set to false.
+ private final SparseBooleanArray mIndicesToLoad = new SparseBooleanArray();
+
+ // We keep a reference of the last requested index to determine which item to prune the
+ // farthest items from when we hit the memory limit
+ private int mLastRequestedIndex;
+
+
+ // The lower and upper bounds of the preloaded range
+ private int mPreloadLowerBound;
+ private int mPreloadUpperBound;
+
+ // The bounds of this fixed cache, we will try and fill as many items into the cache up to
+ // the maxCount number of items, or the maxSize memory usage.
+ // The maxCountSlack is used to determine if a new position in the cache to be loaded is
+ // sufficiently ouside the old set, prompting a shifting of the "window" of items to be
+ // preloaded.
+ private final int mMaxCount;
+ private final int mMaxCountSlack;
+ private static final float sMaxCountSlackPercent = 0.75f;
+ private static final int sMaxMemoryLimitInBytes = 2 * 1024 * 1024;
+
+ public FixedSizeRemoteViewsCache(int maxCacheSize) {
+ mMaxCount = maxCacheSize;
+ mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2));
+ mPreloadLowerBound = 0;
+ mPreloadUpperBound = -1;
+ mLastRequestedIndex = -1;
+ }
+
+ public void insert(int position, RemoteViews v, long itemId, int[] visibleWindow) {
+ // Trim the cache if we go beyond the count
+ if (mIndexRemoteViews.size() >= mMaxCount) {
+ mIndexRemoteViews.remove(getFarthestPositionFrom(position, visibleWindow));
+ }
+
+ // Trim the cache if we go beyond the available memory size constraints
+ int pruneFromPosition = (mLastRequestedIndex > -1) ? mLastRequestedIndex : position;
+ while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryLimitInBytes) {
+ // Note: This is currently the most naive mechanism for deciding what to prune when
+ // we hit the memory limit. In the future, we may want to calculate which index to
+ // remove based on both its position as well as it's current memory usage, as well
+ // as whether it was directly requested vs. whether it was preloaded by our caching
+ // mechanism.
+ int trimIndex = getFarthestPositionFrom(pruneFromPosition, visibleWindow);
+
+ // Need to check that this is a valid index, to cover the case where you have only
+ // a single view in the cache, but it's larger than the max memory limit
+ if (trimIndex < 0) {
+ break;
+ }
+
+ mIndexRemoteViews.remove(trimIndex);
+ }
+
+ // Update the metadata cache
+ final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position);
+ if (metaData != null) {
+ metaData.set(v, itemId);
+ } else {
+ mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId));
+ }
+ mIndexRemoteViews.put(position, v);
+ }
+
+ public RemoteViewsMetaData getMetaData() {
+ return mMetaData;
+ }
+ public RemoteViewsMetaData getTemporaryMetaData() {
+ return mTemporaryMetaData;
+ }
+ public RemoteViews getRemoteViewsAt(int position) {
+ return mIndexRemoteViews.get(position);
+ }
+ public RemoteViewsIndexMetaData getMetaDataAt(int position) {
+ return mIndexMetaData.get(position);
+ }
+
+ public void commitTemporaryMetaData() {
+ synchronized (mTemporaryMetaData) {
+ synchronized (mMetaData) {
+ mMetaData.set(mTemporaryMetaData);
+ }
+ }
+ }
+
+ private int getRemoteViewsBitmapMemoryUsage() {
+ // Calculate the memory usage of all the RemoteViews bitmaps being cached
+ int mem = 0;
+ for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) {
+ final RemoteViews v = mIndexRemoteViews.valueAt(i);
+ if (v != null) {
+ mem += v.estimateMemoryUsage();
+ }
+ }
+ return mem;
+ }
+
+ private int getFarthestPositionFrom(int pos, int[] visibleWindow) {
+ // Find the index farthest away and remove that
+ int maxDist = 0;
+ int maxDistIndex = -1;
+ int maxDistNotVisible = 0;
+ int maxDistIndexNotVisible = -1;
+ for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) {
+ int index = mIndexRemoteViews.keyAt(i);
+ int dist = Math.abs(index-pos);
+ if (dist > maxDistNotVisible && Arrays.binarySearch(visibleWindow, index) < 0) {
+ // maxDistNotVisible/maxDistIndexNotVisible will store the index of the
+ // farthest non-visible position
+ maxDistIndexNotVisible = index;
+ maxDistNotVisible = dist;
+ }
+ if (dist >= maxDist) {
+ // maxDist/maxDistIndex will store the index of the farthest position
+ // regardless of whether it is visible or not
+ maxDistIndex = index;
+ maxDist = dist;
+ }
+ }
+ if (maxDistIndexNotVisible > -1) {
+ return maxDistIndexNotVisible;
+ }
+ return maxDistIndex;
+ }
+
+ public void queueRequestedPositionToLoad(int position) {
+ mLastRequestedIndex = position;
+ synchronized (mIndicesToLoad) {
+ mIndicesToLoad.put(position, true);
+ }
+ }
+ public boolean queuePositionsToBePreloadedFromRequestedPosition(int position) {
+ // Check if we need to preload any items
+ if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) {
+ int center = (mPreloadUpperBound + mPreloadLowerBound) / 2;
+ if (Math.abs(position - center) < mMaxCountSlack) {
+ return false;
+ }
+ }
+
+ int count = 0;
+ synchronized (mMetaData) {
+ count = mMetaData.count;
+ }
+ synchronized (mIndicesToLoad) {
+ // Remove all indices which have not been previously requested.
+ for (int i = mIndicesToLoad.size() - 1; i >= 0; i--) {
+ if (!mIndicesToLoad.valueAt(i)) {
+ mIndicesToLoad.removeAt(i);
+ }
+ }
+
+ // Add all the preload indices
+ int halfMaxCount = mMaxCount / 2;
+ mPreloadLowerBound = position - halfMaxCount;
+ mPreloadUpperBound = position + halfMaxCount;
+ int effectiveLowerBound = Math.max(0, mPreloadLowerBound);
+ int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1);
+ for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) {
+ if (mIndexRemoteViews.indexOfKey(i) < 0 && !mIndicesToLoad.get(i)) {
+ // If the index has not been requested, and has not been loaded.
+ mIndicesToLoad.put(i, false);
+ }
+ }
+ }
+ return true;
+ }
+ /** Returns the next index to load */
+ public int getNextIndexToLoad() {
+ // We try and prioritize items that have been requested directly, instead
+ // of items that are loaded as a result of the caching mechanism
+ synchronized (mIndicesToLoad) {
+ // Prioritize requested indices to be loaded first
+ int index = mIndicesToLoad.indexOfValue(true);
+ if (index < 0) {
+ // Otherwise, preload other indices as necessary
+ index = mIndicesToLoad.indexOfValue(false);
+ }
+ if (index < 0) {
+ return -1;
+ } else {
+ int key = mIndicesToLoad.keyAt(index);
+ mIndicesToLoad.removeAt(index);
+ return key;
+ }
+ }
+ }
+
+ public boolean containsRemoteViewAt(int position) {
+ return mIndexRemoteViews.indexOfKey(position) >= 0;
+ }
+ public boolean containsMetaDataAt(int position) {
+ return mIndexMetaData.indexOfKey(position) >= 0;
+ }
+
+ public void reset() {
+ // Note: We do not try and reset the meta data, since that information is still used by
+ // collection views to validate it's own contents (and will be re-requested if the data
+ // is invalidated through the notifyDataSetChanged() flow).
+
+ mPreloadLowerBound = 0;
+ mPreloadUpperBound = -1;
+ mLastRequestedIndex = -1;
+ mIndexRemoteViews.clear();
+ mIndexMetaData.clear();
+ synchronized (mIndicesToLoad) {
+ mIndicesToLoad.clear();
+ }
+ }
+ }
+
+ static class RemoteViewsCacheKey {
+ final Intent.FilterComparison filter;
+ final int widgetId;
+
+ RemoteViewsCacheKey(Intent.FilterComparison filter, int widgetId) {
+ this.filter = filter;
+ this.widgetId = widgetId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RemoteViewsCacheKey)) {
+ return false;
+ }
+ RemoteViewsCacheKey other = (RemoteViewsCacheKey) o;
+ return other.filter.equals(filter) && other.widgetId == widgetId;
+ }
+
+ @Override
+ public int hashCode() {
+ return (filter == null ? 0 : filter.hashCode()) ^ (widgetId << 2);
+ }
+ }
+
+ public RemoteViewsAdapter(Context context, Intent intent,
+ RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) {
+ mContext = context;
+ mIntent = intent;
+
+ if (mIntent == null) {
+ throw new IllegalArgumentException("Non-null Intent must be specified.");
+ }
+
+ mAppWidgetId = intent.getIntExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1);
+ mRequestedViews = new RemoteViewsFrameLayoutRefSet();
+
+ // Strip the previously injected app widget id from service intent
+ if (intent.hasExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID)) {
+ intent.removeExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID);
+ }
+
+ // Initialize the worker thread
+ mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
+ mWorkerThread.start();
+ mWorkerQueue = new Handler(mWorkerThread.getLooper());
+ mMainQueue = new Handler(Looper.myLooper(), this);
+ mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null;
+
+ if (sCacheRemovalThread == null) {
+ sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
+ sCacheRemovalThread.start();
+ sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper());
+ }
+
+ // Initialize the cache and the service connection on startup
+ mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
+ mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
+
+ RemoteViewsCacheKey key = new RemoteViewsCacheKey(new Intent.FilterComparison(mIntent),
+ mAppWidgetId);
+
+ synchronized(sCachedRemoteViewsCaches) {
+ if (sCachedRemoteViewsCaches.containsKey(key)) {
+ mCache = sCachedRemoteViewsCaches.get(key);
+ synchronized (mCache.mMetaData) {
+ if (mCache.mMetaData.count > 0) {
+ // As a precautionary measure, we verify that the meta data indicates a
+ // non-zero count before declaring that data is ready.
+ mDataReady = true;
+ }
+ }
+ } else {
+ mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
+ }
+ if (!mDataReady) {
+ requestBindService();
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mWorkerThread != null) {
+ mWorkerThread.quit();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public boolean isDataReady() {
+ return mDataReady;
+ }
+
+ public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+ mRemoteViewsOnClickHandler = handler;
+ }
+
+ public void saveRemoteViewsCache() {
+ final RemoteViewsCacheKey key = new RemoteViewsCacheKey(
+ new Intent.FilterComparison(mIntent), mAppWidgetId);
+
+ synchronized(sCachedRemoteViewsCaches) {
+ // If we already have a remove runnable posted for this key, remove it.
+ if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+ sCacheRemovalQueue.removeCallbacks(sRemoteViewsCacheRemoveRunnables.get(key));
+ sRemoteViewsCacheRemoveRunnables.remove(key);
+ }
+
+ int metaDataCount = 0;
+ int numRemoteViewsCached = 0;
+ synchronized (mCache.mMetaData) {
+ metaDataCount = mCache.mMetaData.count;
+ }
+ synchronized (mCache) {
+ numRemoteViewsCached = mCache.mIndexRemoteViews.size();
+ }
+ if (metaDataCount > 0 && numRemoteViewsCached > 0) {
+ sCachedRemoteViewsCaches.put(key, mCache);
+ }
+
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (sCachedRemoteViewsCaches) {
+ if (sCachedRemoteViewsCaches.containsKey(key)) {
+ sCachedRemoteViewsCaches.remove(key);
+ }
+ if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+ sRemoteViewsCacheRemoveRunnables.remove(key);
+ }
+ }
+ }
+ };
+ sRemoteViewsCacheRemoveRunnables.put(key, r);
+ sCacheRemovalQueue.postDelayed(r, REMOTE_VIEWS_CACHE_DURATION);
+ }
+ }
+
+ private void loadNextIndexInBackground() {
+ mWorkerQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mServiceConnection.isConnected()) {
+ // Get the next index to load
+ int position = -1;
+ synchronized (mCache) {
+ position = mCache.getNextIndexToLoad();
+ }
+ if (position > -1) {
+ // Load the item, and notify any existing RemoteViewsFrameLayouts
+ updateRemoteViews(position, true);
+
+ // Queue up for the next one to load
+ loadNextIndexInBackground();
+ } else {
+ // No more items to load, so queue unbind
+ enqueueDeferredUnbindServiceMessage();
+ }
+ }
+ }
+ });
+ }
+
+ private void processException(String method, Exception e) {
+ Log.e("RemoteViewsAdapter", "Error in " + method + ": " + e.getMessage());
+
+ // If we encounter a crash when updating, we should reset the metadata & cache and trigger
+ // a notifyDataSetChanged to update the widget accordingly
+ final RemoteViewsMetaData metaData = mCache.getMetaData();
+ synchronized (metaData) {
+ metaData.reset();
+ }
+ synchronized (mCache) {
+ mCache.reset();
+ }
+ mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ superNotifyDataSetChanged();
+ }
+ });
+ }
+
+ private void updateTemporaryMetaData() {
+ IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+ try {
+ // get the properties/first view (so that we can use it to
+ // measure our dummy views)
+ boolean hasStableIds = factory.hasStableIds();
+ int viewTypeCount = factory.getViewTypeCount();
+ int count = factory.getCount();
+ LoadingViewTemplate loadingTemplate =
+ new LoadingViewTemplate(factory.getLoadingView(), mContext);
+ if ((count > 0) && (loadingTemplate.remoteViews == null)) {
+ RemoteViews firstView = factory.getViewAt(0);
+ if (firstView != null) {
+ loadingTemplate.loadFirstViewHeight(firstView, mContext,
+ new HandlerThreadExecutor(mWorkerThread));
+ }
+ }
+ final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData();
+ synchronized (tmpMetaData) {
+ tmpMetaData.hasStableIds = hasStableIds;
+ // We +1 because the base view type is the loading view
+ tmpMetaData.viewTypeCount = viewTypeCount + 1;
+ tmpMetaData.count = count;
+ tmpMetaData.loadingTemplate = loadingTemplate;
+ }
+ } catch(RemoteException e) {
+ processException("updateMetaData", e);
+ } catch(RuntimeException e) {
+ processException("updateMetaData", e);
+ }
+ }
+
+ private void updateRemoteViews(final int position, boolean notifyWhenLoaded) {
+ IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+ // Load the item information from the remote service
+ RemoteViews remoteViews = null;
+ long itemId = 0;
+ try {
+ remoteViews = factory.getViewAt(position);
+ itemId = factory.getItemId(position);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
+
+ // Return early to prevent additional work in re-centering the view cache, and
+ // swapping from the loading view
+ return;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
+ return;
+ }
+
+ if (remoteViews == null) {
+ // If a null view was returned, we break early to prevent it from getting
+ // into our cache and causing problems later. The effect is that the child at this
+ // position will remain as a loading view until it is updated.
+ Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " +
+ "returned from RemoteViewsFactory.");
+ return;
+ }
+
+ int layoutId = remoteViews.getLayoutId();
+ RemoteViewsMetaData metaData = mCache.getMetaData();
+ boolean viewTypeInRange;
+ int cacheCount;
+ synchronized (metaData) {
+ viewTypeInRange = metaData.isViewTypeInRange(layoutId);
+ cacheCount = mCache.mMetaData.count;
+ }
+ synchronized (mCache) {
+ if (viewTypeInRange) {
+ int[] visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
+ mVisibleWindowUpperBound, cacheCount);
+ // Cache the RemoteViews we loaded
+ mCache.insert(position, remoteViews, itemId, visibleWindow);
+
+ // Notify all the views that we have previously returned for this index that
+ // there is new data for it.
+ final RemoteViews rv = remoteViews;
+ if (notifyWhenLoaded) {
+ mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ mRequestedViews.notifyOnRemoteViewsLoaded(position, rv);
+ }
+ });
+ }
+ } else {
+ // We need to log an error here, as the the view type count specified by the
+ // factory is less than the number of view types returned. We don't return this
+ // view to the AdapterView, as this will cause an exception in the hosting process,
+ // which contains the associated AdapterView.
+ Log.e(TAG, "Error: widget's RemoteViewsFactory returns more view types than " +
+ " indicated by getViewTypeCount() ");
+ }
+ }
+ }
+
+ public Intent getRemoteViewsServiceIntent() {
+ return mIntent;
+ }
+
+ public int getCount() {
+ final RemoteViewsMetaData metaData = mCache.getMetaData();
+ synchronized (metaData) {
+ return metaData.count;
+ }
+ }
+
+ public Object getItem(int position) {
+ // Disallow arbitrary object to be associated with an item for the time being
+ return null;
+ }
+
+ public long getItemId(int position) {
+ synchronized (mCache) {
+ if (mCache.containsMetaDataAt(position)) {
+ return mCache.getMetaDataAt(position).itemId;
+ }
+ return 0;
+ }
+ }
+
+ public int getItemViewType(int position) {
+ int typeId = 0;
+ synchronized (mCache) {
+ if (mCache.containsMetaDataAt(position)) {
+ typeId = mCache.getMetaDataAt(position).typeId;
+ } else {
+ return 0;
+ }
+ }
+
+ final RemoteViewsMetaData metaData = mCache.getMetaData();
+ synchronized (metaData) {
+ return metaData.getMappedViewType(typeId);
+ }
+ }
+
+ /**
+ * This method allows an AdapterView using this Adapter to provide information about which
+ * views are currently being displayed. This allows for certain optimizations and preloading
+ * which wouldn't otherwise be possible.
+ */
+ public void setVisibleRangeHint(int lowerBound, int upperBound) {
+ mVisibleWindowLowerBound = lowerBound;
+ mVisibleWindowUpperBound = upperBound;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // "Request" an index so that we can queue it for loading, initiate subsequent
+ // preloading, etc.
+ synchronized (mCache) {
+ RemoteViews rv = mCache.getRemoteViewsAt(position);
+ boolean isInCache = (rv != null);
+ boolean isConnected = mServiceConnection.isConnected();
+ boolean hasNewItems = false;
+
+ if (convertView != null && convertView instanceof RemoteViewsFrameLayout) {
+ mRequestedViews.removeView((RemoteViewsFrameLayout) convertView);
+ }
+
+ if (!isInCache && !isConnected) {
+ // Requesting bind service will trigger a super.notifyDataSetChanged(), which will
+ // in turn trigger another request to getView()
+ requestBindService();
+ } else {
+ // Queue up other indices to be preloaded based on this position
+ hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
+ }
+
+ final RemoteViewsFrameLayout layout;
+ if (convertView instanceof RemoteViewsFrameLayout) {
+ layout = (RemoteViewsFrameLayout) convertView;
+ } else {
+ layout = new RemoteViewsFrameLayout(parent.getContext(), mCache);
+ layout.setExecutor(mAsyncViewLoadExecutor);
+ }
+
+ if (isInCache) {
+ // Apply the view synchronously if possible, to avoid flickering
+ layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false);
+ if (hasNewItems) loadNextIndexInBackground();
+ } else {
+ // If the views is not loaded, apply the loading view. If the loading view doesn't
+ // exist, the layout will create a default view based on the firstView height.
+ layout.onRemoteViewsLoaded(
+ mCache.getMetaData().getLoadingTemplate(mContext).remoteViews,
+ mRemoteViewsOnClickHandler,
+ false);
+ mRequestedViews.add(position, layout);
+ mCache.queueRequestedPositionToLoad(position);
+ loadNextIndexInBackground();
+ }
+ return layout;
+ }
+ }
+
+ public int getViewTypeCount() {
+ final RemoteViewsMetaData metaData = mCache.getMetaData();
+ synchronized (metaData) {
+ return metaData.viewTypeCount;
+ }
+ }
+
+ public boolean hasStableIds() {
+ final RemoteViewsMetaData metaData = mCache.getMetaData();
+ synchronized (metaData) {
+ return metaData.hasStableIds;
+ }
+ }
+
+ public boolean isEmpty() {
+ return getCount() <= 0;
+ }
+
+ private void onNotifyDataSetChanged() {
+ // Complete the actual notifyDataSetChanged() call initiated earlier
+ IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+ try {
+ factory.onDataSetChanged();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+
+ // Return early to prevent from further being notified (since nothing has
+ // changed)
+ return;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+ return;
+ }
+
+ // Flush the cache so that we can reload new items from the service
+ synchronized (mCache) {
+ mCache.reset();
+ }
+
+ // Re-request the new metadata (only after the notification to the factory)
+ updateTemporaryMetaData();
+ int newCount;
+ int[] visibleWindow;
+ synchronized(mCache.getTemporaryMetaData()) {
+ newCount = mCache.getTemporaryMetaData().count;
+ visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
+ mVisibleWindowUpperBound, newCount);
+ }
+
+ // Pre-load (our best guess of) the views which are currently visible in the AdapterView.
+ // This mitigates flashing and flickering of loading views when a widget notifies that
+ // its data has changed.
+ for (int i: visibleWindow) {
+ // Because temporary meta data is only ever modified from this thread (ie.
+ // mWorkerThread), it is safe to assume that count is a valid representation.
+ if (i < newCount) {
+ updateRemoteViews(i, false);
+ }
+ }
+
+ // Propagate the notification back to the base adapter
+ mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mCache) {
+ mCache.commitTemporaryMetaData();
+ }
+
+ superNotifyDataSetChanged();
+ enqueueDeferredUnbindServiceMessage();
+ }
+ });
+
+ // Reset the notify flagflag
+ mNotifyDataSetChangedAfterOnServiceConnected = false;
+ }
+
+ /**
+ * Returns a sorted array of all integers between lower and upper.
+ */
+ private int[] getVisibleWindow(int lower, int upper, int count) {
+ // In the case that the window is invalid or uninitialized, return an empty window.
+ if ((lower == 0 && upper == 0) || lower < 0 || upper < 0) {
+ return new int[0];
+ }
+
+ int[] window;
+ if (lower <= upper) {
+ window = new int[upper + 1 - lower];
+ for (int i = lower, j = 0; i <= upper; i++, j++){
+ window[j] = i;
+ }
+ } else {
+ // If the upper bound is less than the lower bound it means that the visible window
+ // wraps around.
+ count = Math.max(count, lower);
+ window = new int[count - lower + upper + 1];
+ int j = 0;
+ // Add the entries in sorted order
+ for (int i = 0; i <= upper; i++, j++) {
+ window[j] = i;
+ }
+ for (int i = lower; i < count; i++, j++) {
+ window[j] = i;
+ }
+ }
+ return window;
+ }
+
+ public void notifyDataSetChanged() {
+ // Dequeue any unbind messages
+ mMainQueue.removeMessages(sUnbindServiceMessageType);
+
+ // If we are not connected, queue up the notifyDataSetChanged to be handled when we do
+ // connect
+ if (!mServiceConnection.isConnected()) {
+ mNotifyDataSetChangedAfterOnServiceConnected = true;
+ requestBindService();
+ return;
+ }
+
+ mWorkerQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ onNotifyDataSetChanged();
+ }
+ });
+ }
+
+ void superNotifyDataSetChanged() {
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ boolean result = false;
+ switch (msg.what) {
+ case sUnbindServiceMessageType:
+ if (mServiceConnection.isConnected()) {
+ mServiceConnection.unbind(mContext, mAppWidgetId, mIntent);
+ }
+ result = true;
+ break;
+ default:
+ break;
+ }
+ return result;
+ }
+
+ private void enqueueDeferredUnbindServiceMessage() {
+ // Remove any existing deferred-unbind messages
+ mMainQueue.removeMessages(sUnbindServiceMessageType);
+ mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
+ }
+
+ private boolean requestBindService() {
+ // Try binding the service (which will start it if it's not already running)
+ if (!mServiceConnection.isConnected()) {
+ mServiceConnection.bind(mContext, mAppWidgetId, mIntent);
+ }
+
+ // Remove any existing deferred-unbind messages
+ mMainQueue.removeMessages(sUnbindServiceMessageType);
+ return mServiceConnection.isConnected();
+ }
+
+ private static class HandlerThreadExecutor implements Executor {
+ private final HandlerThread mThread;
+
+ HandlerThreadExecutor(HandlerThread thread) {
+ mThread = thread;
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ if (Thread.currentThread().getId() == mThread.getId()) {
+ runnable.run();
+ } else {
+ new Handler(mThread.getLooper()).post(runnable);
+ }
+ }
+ }
+
+ private static class LoadingViewTemplate {
+ public final RemoteViews remoteViews;
+ public int defaultHeight;
+
+ LoadingViewTemplate(RemoteViews views, Context context) {
+ remoteViews = views;
+
+ float density = context.getResources().getDisplayMetrics().density;
+ defaultHeight = Math.round(sDefaultLoadingViewHeight * density);
+ }
+
+ public void loadFirstViewHeight(
+ RemoteViews firstView, Context context, Executor executor) {
+ // Inflate the first view on the worker thread
+ firstView.applyAsync(context, new RemoteViewsFrameLayout(context, null), executor,
+ new RemoteViews.OnViewAppliedListener() {
+ @Override
+ public void onViewApplied(View v) {
+ try {
+ v.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ defaultHeight = v.getMeasuredHeight();
+ } catch (Exception e) {
+ onError(e);
+ }
+ }
+
+ @Override
+ public void onError(Exception e) {
+ // Do nothing. The default height will stay the same.
+ Log.w(TAG, "Error inflating first RemoteViews", e);
+ }
+ });
+ }
+ }
+}
diff --git a/android/widget/RemoteViewsListAdapter.java b/android/widget/RemoteViewsListAdapter.java
new file mode 100644
index 0000000..e490458
--- /dev/null
+++ b/android/widget/RemoteViewsListAdapter.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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 java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class RemoteViewsListAdapter extends BaseAdapter {
+
+ private Context mContext;
+ private ArrayList<RemoteViews> mRemoteViewsList;
+ private ArrayList<Integer> mViewTypes = new ArrayList<Integer>();
+ private int mViewTypeCount;
+
+ public RemoteViewsListAdapter(Context context, ArrayList<RemoteViews> remoteViews,
+ int viewTypeCount) {
+ mContext = context;
+ mRemoteViewsList = remoteViews;
+ mViewTypeCount = viewTypeCount;
+ init();
+ }
+
+ public void setViewsList(ArrayList<RemoteViews> remoteViews) {
+ mRemoteViewsList = remoteViews;
+ init();
+ notifyDataSetChanged();
+ }
+
+ private void init() {
+ if (mRemoteViewsList == null) return;
+
+ mViewTypes.clear();
+ for (RemoteViews rv: mRemoteViewsList) {
+ if (!mViewTypes.contains(rv.getLayoutId())) {
+ mViewTypes.add(rv.getLayoutId());
+ }
+ }
+
+ if (mViewTypes.size() > mViewTypeCount || mViewTypeCount < 1) {
+ throw new RuntimeException("Invalid view type count -- view type count must be >= 1" +
+ "and must be as large as the total number of distinct view types");
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (mRemoteViewsList != null) {
+ return mRemoteViewsList.size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (position < getCount()) {
+ RemoteViews rv = mRemoteViewsList.get(position);
+ rv.setIsWidgetCollectionChild(true);
+ View v;
+ if (convertView != null && rv != null &&
+ convertView.getId() == rv.getLayoutId()) {
+ v = convertView;
+ rv.reapply(mContext, v);
+ } else {
+ v = rv.apply(mContext, parent);
+ }
+ return v;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position < getCount()) {
+ int layoutId = mRemoteViewsList.get(position).getLayoutId();
+ return mViewTypes.indexOf(layoutId);
+ } else {
+ return 0;
+ }
+ }
+
+ public int getViewTypeCount() {
+ return mViewTypeCount;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+}
diff --git a/android/widget/RemoteViewsService.java b/android/widget/RemoteViewsService.java
new file mode 100644
index 0000000..2827f63
--- /dev/null
+++ b/android/widget/RemoteViewsService.java
@@ -0,0 +1,258 @@
+/*
+ * 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.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.internal.widget.IRemoteViewsFactory;
+
+import java.util.HashMap;
+
+/**
+ * The service to be connected to for a remote adapter to request RemoteViews. Users should
+ * extend the RemoteViewsService to provide the appropriate RemoteViewsFactory's used to
+ * populate the remote collection view (ListView, GridView, etc).
+ */
+public abstract class RemoteViewsService extends Service {
+
+ private static final String LOG_TAG = "RemoteViewsService";
+
+ // Used for reference counting of RemoteViewsFactories
+ // Because we are now unbinding when we are not using the Service (to allow them to be
+ // reclaimed), the references to the factories that are created need to be stored and used when
+ // the service is restarted (in response to user input for example). When the process is
+ // destroyed, so is this static cache of RemoteViewsFactories.
+ private static final HashMap<Intent.FilterComparison, RemoteViewsFactory> sRemoteViewFactories =
+ new HashMap<Intent.FilterComparison, RemoteViewsFactory>();
+ private static final Object sLock = new Object();
+
+ /**
+ * An interface for an adapter between a remote collection view (ListView, GridView, etc) and
+ * the underlying data for that view. The implementor is responsible for making a RemoteView
+ * for each item in the data set. This interface is a thin wrapper around {@link Adapter}.
+ *
+ * @see android.widget.Adapter
+ * @see android.appwidget.AppWidgetManager
+ */
+ public interface RemoteViewsFactory {
+ /**
+ * Called when your factory is first constructed. The same factory may be shared across
+ * multiple RemoteViewAdapters depending on the intent passed.
+ */
+ public void onCreate();
+
+ /**
+ * Called when notifyDataSetChanged() is triggered on the remote adapter. This allows a
+ * RemoteViewsFactory to respond to data changes by updating any internal references.
+ *
+ * Note: expensive tasks can be safely performed synchronously within this method. In the
+ * interim, the old data will be displayed within the widget.
+ *
+ * @see android.appwidget.AppWidgetManager#notifyAppWidgetViewDataChanged(int[], int)
+ */
+ public void onDataSetChanged();
+
+ /**
+ * Called when the last RemoteViewsAdapter that is associated with this factory is
+ * unbound.
+ */
+ public void onDestroy();
+
+ /**
+ * See {@link Adapter#getCount()}
+ *
+ * @return Count of items.
+ */
+ public int getCount();
+
+ /**
+ * See {@link Adapter#getView(int, android.view.View, android.view.ViewGroup)}.
+ *
+ * Note: expensive tasks can be safely performed synchronously within this method, and a
+ * loading view will be displayed in the interim. See {@link #getLoadingView()}.
+ *
+ * @param position The position of the item within the Factory's data set of the item whose
+ * view we want.
+ * @return A RemoteViews object corresponding to the data at the specified position.
+ */
+ public RemoteViews getViewAt(int position);
+
+ /**
+ * This allows for the use of a custom loading view which appears between the time that
+ * {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
+ * view will be used.
+ *
+ * @return The RemoteViews representing the desired loading view.
+ */
+ public RemoteViews getLoadingView();
+
+ /**
+ * See {@link Adapter#getViewTypeCount()}.
+ *
+ * @return The number of types of Views that will be returned by this factory.
+ */
+ public int getViewTypeCount();
+
+ /**
+ * See {@link Adapter#getItemId(int)}.
+ *
+ * @param position The position of the item within the data set whose row id we want.
+ * @return The id of the item at the specified position.
+ */
+ public long getItemId(int position);
+
+ /**
+ * See {@link Adapter#hasStableIds()}.
+ *
+ * @return True if the same id always refers to the same object.
+ */
+ public boolean hasStableIds();
+ }
+
+ /**
+ * A private proxy class for the private IRemoteViewsFactory interface through the
+ * public RemoteViewsFactory interface.
+ */
+ private static class RemoteViewsFactoryAdapter extends IRemoteViewsFactory.Stub {
+ public RemoteViewsFactoryAdapter(RemoteViewsFactory factory, boolean isCreated) {
+ mFactory = factory;
+ mIsCreated = isCreated;
+ }
+ public synchronized boolean isCreated() {
+ return mIsCreated;
+ }
+ public synchronized void onDataSetChanged() {
+ try {
+ mFactory.onDataSetChanged();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ }
+ public synchronized void onDataSetChangedAsync() {
+ onDataSetChanged();
+ }
+ public synchronized int getCount() {
+ int count = 0;
+ try {
+ count = mFactory.getCount();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return count;
+ }
+ public synchronized RemoteViews getViewAt(int position) {
+ RemoteViews rv = null;
+ try {
+ rv = mFactory.getViewAt(position);
+ if (rv != null) {
+ rv.setIsWidgetCollectionChild(true);
+ }
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return rv;
+ }
+ public synchronized RemoteViews getLoadingView() {
+ RemoteViews rv = null;
+ try {
+ rv = mFactory.getLoadingView();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return rv;
+ }
+ public synchronized int getViewTypeCount() {
+ int count = 0;
+ try {
+ count = mFactory.getViewTypeCount();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return count;
+ }
+ public synchronized long getItemId(int position) {
+ long id = 0;
+ try {
+ id = mFactory.getItemId(position);
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return id;
+ }
+ public synchronized boolean hasStableIds() {
+ boolean hasStableIds = false;
+ try {
+ hasStableIds = mFactory.hasStableIds();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ return hasStableIds;
+ }
+ public void onDestroy(Intent intent) {
+ synchronized (sLock) {
+ Intent.FilterComparison fc = new Intent.FilterComparison(intent);
+ if (RemoteViewsService.sRemoteViewFactories.containsKey(fc)) {
+ RemoteViewsFactory factory = RemoteViewsService.sRemoteViewFactories.get(fc);
+ try {
+ factory.onDestroy();
+ } catch (Exception ex) {
+ Thread t = Thread.currentThread();
+ Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+ }
+ RemoteViewsService.sRemoteViewFactories.remove(fc);
+ }
+ }
+ }
+
+ private RemoteViewsFactory mFactory;
+ private boolean mIsCreated;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ synchronized (sLock) {
+ Intent.FilterComparison fc = new Intent.FilterComparison(intent);
+ RemoteViewsFactory factory = null;
+ boolean isCreated = false;
+ if (!sRemoteViewFactories.containsKey(fc)) {
+ factory = onGetViewFactory(intent);
+ sRemoteViewFactories.put(fc, factory);
+ factory.onCreate();
+ isCreated = false;
+ } else {
+ factory = sRemoteViewFactories.get(fc);
+ isCreated = true;
+ }
+ return new RemoteViewsFactoryAdapter(factory, isCreated);
+ }
+ }
+
+ /**
+ * To be implemented by the derived service to generate appropriate factories for
+ * the data.
+ */
+ public abstract RemoteViewsFactory onGetViewFactory(Intent intent);
+}
diff --git a/android/widget/ResourceCursorAdapter.java b/android/widget/ResourceCursorAdapter.java
new file mode 100644
index 0000000..9732bb1
--- /dev/null
+++ b/android/widget/ResourceCursorAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * 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.Resources;
+import android.database.Cursor;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * 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;
+ private LayoutInflater mDropDownInflater;
+
+ /**
+ * Constructor the enables auto-requery.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ */
+ @Deprecated
+ public ResourceCursorAdapter(Context context, int layout, Cursor c) {
+ super(context, c);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDropDownInflater = mInflater;
+ }
+
+ /**
+ * Constructor with default behavior as per
+ * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended
+ * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}.
+ * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+ * will always be set.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ * @param c The cursor from which to get the data.
+ * @param autoRequery If true the adapter will call requery() on the
+ * cursor whenever it changes so the most recent
+ * data is always displayed. Using true here is discouraged.
+ */
+ public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
+ super(context, c, autoRequery);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDropDownInflater = mInflater;
+ }
+
+ /**
+ * Standard constructor.
+ *
+ * @param context The context where the ListView associated with this adapter is running
+ * @param layout Resource identifier of a layout file that defines the views
+ * for this list item. Unless you override them later, this will
+ * define both the item views and the drop down views.
+ * @param c The cursor from which to get the data.
+ * @param flags Flags used to determine the behavior of the adapter,
+ * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+ */
+ public ResourceCursorAdapter(Context context, int layout, Cursor c, int flags) {
+ super(context, c, flags);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDropDownInflater = mInflater;
+ }
+
+ /**
+ * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are
+ * inflated.
+ * <p>
+ * By default, drop-down views are inflated against the theme of the
+ * {@link Context} passed to the adapter's constructor.
+ *
+ * @param theme the theme against which to inflate drop-down views or
+ * {@code null} to use the theme from the adapter's context
+ * @see #newDropDownView(Context, Cursor, ViewGroup)
+ */
+ @Override
+ public void setDropDownViewTheme(Resources.Theme theme) {
+ super.setDropDownViewTheme(theme);
+
+ if (theme == null) {
+ mDropDownInflater = null;
+ } else if (theme == mInflater.getContext().getTheme()) {
+ mDropDownInflater = mInflater;
+ } else {
+ final Context context = new ContextThemeWrapper(mContext, theme);
+ mDropDownInflater = LayoutInflater.from(context);
+ }
+ }
+
+ /**
+ * 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 mDropDownInflater.inflate(mDropDownLayout, parent, false);
+ }
+
+ /**
+ * <p>Sets the layout resource of the item views.</p>
+ *
+ * @param layout the layout resources used to create item views
+ */
+ public void setViewResource(int layout) {
+ mLayout = layout;
+ }
+
+ /**
+ * <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/android/widget/ResourceCursorTreeAdapter.java b/android/widget/ResourceCursorTreeAdapter.java
new file mode 100644
index 0000000..0894dba
--- /dev/null
+++ b/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.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * 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/android/widget/RtlSpacingHelper.java b/android/widget/RtlSpacingHelper.java
new file mode 100644
index 0000000..f6b116f
--- /dev/null
+++ b/android/widget/RtlSpacingHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * RtlSpacingHelper manages the relationship between left/right and start/end for views
+ * that need to maintain both absolute and relative settings for a form of spacing similar
+ * to view padding.
+ */
+class RtlSpacingHelper {
+ public static final int UNDEFINED = Integer.MIN_VALUE;
+
+ private int mLeft = 0;
+ private int mRight = 0;
+ private int mStart = UNDEFINED;
+ private int mEnd = UNDEFINED;
+ private int mExplicitLeft = 0;
+ private int mExplicitRight = 0;
+
+ private boolean mIsRtl = false;
+ private boolean mIsRelative = false;
+
+ public int getLeft() {
+ return mLeft;
+ }
+
+ public int getRight() {
+ return mRight;
+ }
+
+ public int getStart() {
+ return mIsRtl ? mRight : mLeft;
+ }
+
+ public int getEnd() {
+ return mIsRtl ? mLeft : mRight;
+ }
+
+ public void setRelative(int start, int end) {
+ mStart = start;
+ mEnd = end;
+ mIsRelative = true;
+ if (mIsRtl) {
+ if (end != UNDEFINED) mLeft = end;
+ if (start != UNDEFINED) mRight = start;
+ } else {
+ if (start != UNDEFINED) mLeft = start;
+ if (end != UNDEFINED) mRight = end;
+ }
+ }
+
+ public void setAbsolute(int left, int right) {
+ mIsRelative = false;
+ if (left != UNDEFINED) mLeft = mExplicitLeft = left;
+ if (right != UNDEFINED) mRight = mExplicitRight = right;
+ }
+
+ public void setDirection(boolean isRtl) {
+ if (isRtl == mIsRtl) {
+ return;
+ }
+ mIsRtl = isRtl;
+ if (mIsRelative) {
+ if (isRtl) {
+ mLeft = mEnd != UNDEFINED ? mEnd : mExplicitLeft;
+ mRight = mStart != UNDEFINED ? mStart : mExplicitRight;
+ } else {
+ mLeft = mStart != UNDEFINED ? mStart : mExplicitLeft;
+ mRight = mEnd != UNDEFINED ? mEnd : mExplicitRight;
+ }
+ } else {
+ mLeft = mExplicitLeft;
+ mRight = mExplicitRight;
+ }
+ }
+}
diff --git a/android/widget/ScrollBarDrawable.java b/android/widget/ScrollBarDrawable.java
new file mode 100644
index 0000000..2ae38c9
--- /dev/null
+++ b/android/widget/ScrollBarDrawable.java
@@ -0,0 +1,387 @@
+/*
+ * 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.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.internal.widget.ScrollBarUtils;
+
+/**
+ * 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 implements Drawable.Callback {
+ 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 mBoundsChanged;
+ private boolean mRangeChanged;
+ private boolean mAlwaysDrawHorizontalTrack;
+ private boolean mAlwaysDrawVerticalTrack;
+ private boolean mMutated;
+
+ private int mAlpha = 255;
+ private boolean mHasSetAlpha;
+
+ private ColorFilter mColorFilter;
+ private boolean mHasSetColorFilter;
+
+ /**
+ * Indicate whether the horizontal scrollbar track should always be drawn
+ * regardless of the extent. Defaults to false.
+ *
+ * @param alwaysDrawTrack Whether the track should always be drawn
+ *
+ * @see #getAlwaysDrawHorizontalTrack()
+ */
+ 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 Whether the track should always be drawn
+ *
+ * @see #getAlwaysDrawVerticalTrack()
+ */
+ public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) {
+ mAlwaysDrawVerticalTrack = alwaysDrawTrack;
+ }
+
+ /**
+ * @return whether the vertical scrollbar track should always be drawn
+ * regardless of the extent.
+ *
+ * @see #setAlwaysDrawVerticalTrack(boolean)
+ */
+ public boolean getAlwaysDrawVerticalTrack() {
+ return mAlwaysDrawVerticalTrack;
+ }
+
+ /**
+ * @return whether the horizontal scrollbar track should always be drawn
+ * regardless of the extent.
+ *
+ * @see #setAlwaysDrawHorizontalTrack(boolean)
+ */
+ public boolean getAlwaysDrawHorizontalTrack() {
+ return mAlwaysDrawHorizontalTrack;
+ }
+
+ public void setParameters(int range, int offset, int extent, boolean vertical) {
+ if (mVertical != vertical) {
+ mVertical = vertical;
+
+ mBoundsChanged = true;
+ }
+
+ if (mRange != range || mOffset != offset || mExtent != extent) {
+ mRange = range;
+ mOffset = offset;
+ mExtent = extent;
+
+ mRangeChanged = true;
+ }
+ }
+
+ @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;
+ }
+
+ final Rect r = getBounds();
+ if (canvas.quickReject(r.left, r.top, r.right, r.bottom, Canvas.EdgeType.AA)) {
+ return;
+ }
+
+ if (drawTrack) {
+ drawTrack(canvas, r, vertical);
+ }
+
+ if (drawThumb) {
+ final int scrollBarLength = vertical ? r.height() : r.width();
+ final int thickness = vertical ? r.width() : r.height();
+ final int thumbLength =
+ ScrollBarUtils.getThumbLength(scrollBarLength, thickness, extent, range);
+ final int thumbOffset =
+ ScrollBarUtils.getThumbOffset(scrollBarLength, thumbLength, extent, range,
+ mOffset);
+
+ drawThumb(canvas, r, thumbOffset, thumbLength, vertical);
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mBoundsChanged = true;
+ }
+
+ @Override
+ public boolean isStateful() {
+ return (mVerticalTrack != null && mVerticalTrack.isStateful())
+ || (mVerticalThumb != null && mVerticalThumb.isStateful())
+ || (mHorizontalTrack != null && mHorizontalTrack.isStateful())
+ || (mHorizontalThumb != null && mHorizontalThumb.isStateful())
+ || super.isStateful();
+ }
+
+ @Override
+ protected boolean onStateChange(int[] state) {
+ boolean changed = super.onStateChange(state);
+ if (mVerticalTrack != null) {
+ changed |= mVerticalTrack.setState(state);
+ }
+ if (mVerticalThumb != null) {
+ changed |= mVerticalThumb.setState(state);
+ }
+ if (mHorizontalTrack != null) {
+ changed |= mHorizontalTrack.setState(state);
+ }
+ if (mHorizontalThumb != null) {
+ changed |= mHorizontalThumb.setState(state);
+ }
+ return changed;
+ }
+
+ private void drawTrack(Canvas canvas, Rect bounds, boolean vertical) {
+ final Drawable track;
+ if (vertical) {
+ track = mVerticalTrack;
+ } else {
+ track = mHorizontalTrack;
+ }
+
+ if (track != null) {
+ if (mBoundsChanged) {
+ track.setBounds(bounds);
+ }
+ track.draw(canvas);
+ }
+ }
+
+ private void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) {
+ final boolean changed = mRangeChanged || mBoundsChanged;
+ if (vertical) {
+ if (mVerticalThumb != null) {
+ final Drawable thumb = mVerticalThumb;
+ if (changed) {
+ thumb.setBounds(bounds.left, bounds.top + offset,
+ bounds.right, bounds.top + offset + length);
+ }
+
+ thumb.draw(canvas);
+ }
+ } else {
+ if (mHorizontalThumb != null) {
+ final Drawable thumb = mHorizontalThumb;
+ if (changed) {
+ thumb.setBounds(bounds.left + offset, bounds.top,
+ bounds.left + offset + length, bounds.bottom);
+ }
+
+ thumb.draw(canvas);
+ }
+ }
+ }
+
+ public void setVerticalThumbDrawable(Drawable thumb) {
+ if (mVerticalThumb != null) {
+ mVerticalThumb.setCallback(null);
+ }
+
+ propagateCurrentState(thumb);
+ mVerticalThumb = thumb;
+ }
+
+ public void setVerticalTrackDrawable(Drawable track) {
+ if (mVerticalTrack != null) {
+ mVerticalTrack.setCallback(null);
+ }
+
+ propagateCurrentState(track);
+ mVerticalTrack = track;
+ }
+
+ public void setHorizontalThumbDrawable(Drawable thumb) {
+ if (mHorizontalThumb != null) {
+ mHorizontalThumb.setCallback(null);
+ }
+
+ propagateCurrentState(thumb);
+ mHorizontalThumb = thumb;
+ }
+
+ public void setHorizontalTrackDrawable(Drawable track) {
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.setCallback(null);
+ }
+
+ propagateCurrentState(track);
+ mHorizontalTrack = track;
+ }
+
+ private void propagateCurrentState(Drawable d) {
+ if (d != null) {
+ if (mMutated) {
+ d.mutate();
+ }
+
+ d.setState(getState());
+ d.setCallback(this);
+
+ if (mHasSetAlpha) {
+ d.setAlpha(mAlpha);
+ }
+
+ if (mHasSetColorFilter) {
+ d.setColorFilter(mColorFilter);
+ }
+ }
+ }
+
+ public int getSize(boolean vertical) {
+ if (vertical) {
+ return mVerticalTrack != null ? mVerticalTrack.getIntrinsicWidth() :
+ mVerticalThumb != null ? mVerticalThumb.getIntrinsicWidth() : 0;
+ } else {
+ return mHorizontalTrack != null ? mHorizontalTrack.getIntrinsicHeight() :
+ mHorizontalThumb != null ? mHorizontalThumb.getIntrinsicHeight() : 0;
+ }
+ }
+
+ @Override
+ public ScrollBarDrawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ if (mVerticalTrack != null) {
+ mVerticalTrack.mutate();
+ }
+ if (mVerticalThumb != null) {
+ mVerticalThumb.mutate();
+ }
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.mutate();
+ }
+ if (mHorizontalThumb != null) {
+ mHorizontalThumb.mutate();
+ }
+ mMutated = true;
+ }
+ return this;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ mHasSetAlpha = true;
+
+ if (mVerticalTrack != null) {
+ mVerticalTrack.setAlpha(alpha);
+ }
+ if (mVerticalThumb != null) {
+ mVerticalThumb.setAlpha(alpha);
+ }
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.setAlpha(alpha);
+ }
+ if (mHorizontalThumb != null) {
+ mHorizontalThumb.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ mColorFilter = colorFilter;
+ mHasSetColorFilter = true;
+
+ if (mVerticalTrack != null) {
+ mVerticalTrack.setColorFilter(colorFilter);
+ }
+ if (mVerticalThumb != null) {
+ mVerticalThumb.setColorFilter(colorFilter);
+ }
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.setColorFilter(colorFilter);
+ }
+ if (mHorizontalThumb != null) {
+ mHorizontalThumb.setColorFilter(colorFilter);
+ }
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ @Override
+ public String toString() {
+ return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset +
+ " extent=" + mExtent + (mVertical ? " V" : " H");
+ }
+}
+
+
diff --git a/android/widget/ScrollView.java b/android/widget/ScrollView.java
new file mode 100644
index 0000000..97d32f1
--- /dev/null
+++ b/android/widget/ScrollView.java
@@ -0,0 +1,1897 @@
+/*
+ * 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.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * A view group that allows the view hierarchy placed within it to be scrolled.
+ * Scroll view may have only one direct child placed within it.
+ * To add multiple views within the scroll view, make
+ * the direct child you add a view group, for example {@link LinearLayout}, and
+ * place additional views within that LinearLayout.
+ *
+ * <p>Scroll view supports vertical scrolling only. For horizontal scrolling,
+ * use {@link HorizontalScrollView} instead.</p>
+ *
+ * <p>Never add a {@link android.support.v7.widget.RecyclerView} or {@link ListView} to
+ * a scroll view. Doing so results in poor user interface performance and a poor user
+ * experience.</p>
+ *
+ * <p class="note">
+ * For vertical scrolling, consider {@link android.support.v4.widget.NestedScrollView}
+ * instead of scroll view which offers greater user interface flexibility and
+ * support for the material design scrolling patterns.</p>
+ *
+ * <p>To learn more about material design patterns for handling scrolling, see
+ * <a href="https://material.io/guidelines/patterns/scrolling-techniques.html#">
+ * Scrolling techniques</a>.</p>
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+public class ScrollView extends FrameLayout {
+ static final int ANIMATED_SCROLL_GAP = 250;
+
+ static final float MAX_SCROLL_FACTOR = 0.5f;
+
+ private static final String TAG = "ScrollView";
+
+ private long mLastScroll;
+
+ private final Rect mTempRect = new Rect();
+ private OverScroller mScroller;
+ private EdgeEffect mEdgeGlowTop;
+ private EdgeEffect mEdgeGlowBottom;
+
+ /**
+ * Position of the last motion event.
+ */
+ private int 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.
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ private boolean mFillViewport;
+
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ private boolean mSmoothScrollingEnabled = true;
+
+ private int mTouchSlop;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+
+ private int mOverscrollDistance;
+ private int mOverflingDistance;
+
+ private float mVerticalScrollFactor;
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+
+ /**
+ * Used during scrolling to retrieve the new offset within the window.
+ */
+ private final int[] mScrollOffset = new int[2];
+ private final int[] mScrollConsumed = new int[2];
+ private int mNestedYOffset;
+
+ /**
+ * The StrictMode "critical time span" objects to catch animation
+ * stutters. Non-null when a time-sensitive animation is
+ * in-flight. Must call finish() on them when done animating.
+ * These are no-ops on user builds.
+ */
+ private StrictMode.Span mScrollStrictSpan = null; // aka "drag"
+ private StrictMode.Span mFlingStrictSpan = null;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ private SavedState mSavedState;
+
+ public ScrollView(Context context) {
+ this(context, null);
+ }
+
+ public ScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
+ }
+
+ public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initScrollView();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
+
+ setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
+
+ a.recycle();
+
+ if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+ setRevealOnFocusHint(false);
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return true;
+ }
+
+ @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 bottomEdge = getHeight() - mPaddingBottom;
+ final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
+ 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 OverScroller(getContext());
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setWillNotDraw(false);
+ final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
+ }
+
+ @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.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+ 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.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+ 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;
+ }
+
+ if (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ final int widthPadding;
+ final int heightPadding;
+ final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+ final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (targetSdkVersion >= VERSION_CODES.M) {
+ widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
+ heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
+ } else {
+ widthPadding = mPaddingLeft + mPaddingRight;
+ heightPadding = mPaddingTop + mPaddingBottom;
+ }
+
+ final int desiredHeight = getMeasuredHeight() - heightPadding;
+ if (child.getMeasuredHeight() < desiredHeight) {
+ final int childWidthMeasureSpec = getChildMeasureSpec(
+ widthMeasureSpec, widthPadding, lp.width);
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ desiredHeight, MeasureSpec.EXACTLY);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || 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() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
+ 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;
+ }
+
+ private boolean inChild(int x, int y) {
+ if (getChildCount() > 0) {
+ final int scrollY = mScrollY;
+ final View child = getChildAt(0);
+ return !(y < child.getTop() - scrollY
+ || y >= child.getBottom() - scrollY
+ || x < child.getLeft()
+ || x >= child.getRight());
+ }
+ return false;
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+
+ @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 (super.onInterceptTouchEvent(ev)) {
+ return true;
+ }
+
+ /*
+ * Don't try to intercept touch if we can't scroll anyway.
+ */
+ if (getScrollY() == 0 && !canScrollVertically(1)) {
+ return false;
+ }
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ 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 activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = ev.findPointerIndex(activePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + activePointerId
+ + " in onInterceptTouchEvent");
+ break;
+ }
+
+ final int y = (int) ev.getY(pointerIndex);
+ final int yDiff = Math.abs(y - mLastMotionY);
+ if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+ mIsBeingDragged = true;
+ mLastMotionY = y;
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ mNestedYOffset = 0;
+ if (mScrollStrictSpan == null) {
+ mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
+ }
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ final int y = (int) ev.getY();
+ if (!inChild((int) ev.getX(), (int) y)) {
+ mIsBeingDragged = false;
+ recycleVelocityTracker();
+ break;
+ }
+
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionY = y;
+ mActivePointerId = ev.getPointerId(0);
+
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ /*
+ * If being flinged and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when
+ * being flinged. We need to call computeScrollOffset() first so that
+ * isFinished() is correct.
+ */
+ mScroller.computeScrollOffset();
+ mIsBeingDragged = !mScroller.isFinished();
+ if (mIsBeingDragged && mScrollStrictSpan == null) {
+ mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
+ }
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ /* Release the drag */
+ mIsBeingDragged = false;
+ mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+ if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
+ postInvalidateOnAnimation();
+ }
+ stopNestedScroll();
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ 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) {
+ initVelocityTrackerIfNotExists();
+
+ MotionEvent vtev = MotionEvent.obtain(ev);
+
+ final int actionMasked = ev.getActionMasked();
+
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ mNestedYOffset = 0;
+ }
+ vtev.offsetLocation(0, mNestedYOffset);
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN: {
+ if (getChildCount() == 0) {
+ return false;
+ }
+ if ((mIsBeingDragged = !mScroller.isFinished())) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+
+ // Remember where the motion event started
+ mLastMotionY = (int) ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (activePointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+ break;
+ }
+
+ final int y = (int) ev.getY(activePointerIndex);
+ int deltaY = mLastMotionY - y;
+ if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
+ deltaY -= mScrollConsumed[1];
+ vtev.offsetLocation(0, mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ }
+ if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ mIsBeingDragged = true;
+ if (deltaY > 0) {
+ deltaY -= mTouchSlop;
+ } else {
+ deltaY += mTouchSlop;
+ }
+ }
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ mLastMotionY = y - mScrollOffset[1];
+
+ final int oldY = mScrollY;
+ final int range = getScrollRange();
+ final int overscrollMode = getOverScrollMode();
+ boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+ // Calling overScrollBy will call onOverScrolled, which
+ // calls onScrollChanged if applicable.
+ if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
+ && !hasNestedScrollingParent()) {
+ // Break our velocity if we hit a scroll barrier.
+ mVelocityTracker.clear();
+ }
+
+ final int scrolledDeltaY = mScrollY - oldY;
+ final int unconsumedY = deltaY - scrolledDeltaY;
+ if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
+ mLastMotionY -= mScrollOffset[1];
+ vtev.offsetLocation(0, mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ } else if (canOverscroll) {
+ final int pulledToY = oldY + deltaY;
+ if (pulledToY < 0) {
+ mEdgeGlowTop.onPull((float) deltaY / getHeight(),
+ ev.getX(activePointerIndex) / getWidth());
+ if (!mEdgeGlowBottom.isFinished()) {
+ mEdgeGlowBottom.onRelease();
+ }
+ } else if (pulledToY > range) {
+ mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
+ 1.f - ev.getX(activePointerIndex) / getWidth());
+ if (!mEdgeGlowTop.isFinished()) {
+ mEdgeGlowTop.onRelease();
+ }
+ }
+ if (mEdgeGlowTop != null
+ && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
+ postInvalidateOnAnimation();
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+ if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+ flingWithNestedDispatch(-initialVelocity);
+ } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
+ getScrollRange())) {
+ postInvalidateOnAnimation();
+ }
+
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged && getChildCount() > 0) {
+ if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
+ postInvalidateOnAnimation();
+ }
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int index = ev.getActionIndex();
+ mLastMotionY = (int) ev.getY(index);
+ mActivePointerId = ev.getPointerId(index);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+ break;
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(vtev);
+ }
+ vtev.recycle();
+ return true;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+ MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionY = (int) ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL:
+ final float axisValue;
+ if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+ axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ } else {
+ axisValue = 0;
+ }
+
+ final int delta = Math.round(axisValue * mVerticalScrollFactor);
+ if (delta != 0) {
+ final int range = getScrollRange();
+ int oldScrollY = mScrollY;
+ int newScrollY = oldScrollY - delta;
+ if (newScrollY < 0) {
+ newScrollY = 0;
+ } else if (newScrollY > range) {
+ newScrollY = range;
+ }
+ if (newScrollY != oldScrollY) {
+ super.scrollTo(mScrollX, newScrollY);
+ return true;
+ }
+ }
+ break;
+ }
+
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY,
+ boolean clampedX, boolean clampedY) {
+ // Treat animating scrolls differently; see #computeScroll() for why.
+ if (!mScroller.isFinished()) {
+ final int oldX = mScrollX;
+ final int oldY = mScrollY;
+ mScrollX = scrollX;
+ mScrollY = scrollY;
+ invalidateParentIfNeeded();
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ if (clampedY) {
+ mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
+ }
+ } else {
+ super.scrollTo(scrollX, scrollY);
+ }
+
+ awakenScrollBars();
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (!isEnabled()) {
+ return false;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case R.id.accessibilityActionScrollDown: {
+ final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+ final int targetScrollY = Math.min(mScrollY + viewportHeight, getScrollRange());
+ if (targetScrollY != mScrollY) {
+ smoothScrollTo(0, targetScrollY);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+ case R.id.accessibilityActionScrollUp: {
+ final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+ final int targetScrollY = Math.max(mScrollY - viewportHeight, 0);
+ if (targetScrollY != mScrollY) {
+ smoothScrollTo(0, targetScrollY);
+ return true;
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ScrollView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ if (isEnabled()) {
+ final int scrollRange = getScrollRange();
+ if (scrollRange > 0) {
+ info.setScrollable(true);
+ if (mScrollY > 0) {
+ info.addAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
+ }
+ if (mScrollY < scrollRange) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
+ }
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ final boolean scrollable = getScrollRange() > 0;
+ event.setScrollable(scrollable);
+ event.setScrollX(mScrollX);
+ event.setScrollY(mScrollY);
+ event.setMaxScrollX(mScrollX);
+ event.setMaxScrollY(getScrollRange());
+ }
+
+ private int getScrollRange() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0,
+ child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
+ }
+ return scrollRange;
+ }
+
+ /**
+ * <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() + mPaddingBottom;
+ 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);
+
+ 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, getHeight())) {
+ 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) {
+ if (getChildCount() > 0) {
+ int daBottom = getChildAt(0).getBottom();
+ int screenBottom = getScrollY() + getHeight() - mPaddingBottom;
+ 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, getHeight());
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+ return (mTempRect.bottom + delta) >= getScrollY()
+ && (mTempRect.top - delta) <= (getScrollY() + height);
+ }
+
+ /**
+ * Smooth scroll by a Y delta
+ *
+ * @param delta the number of pixels to scroll by on the Y 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) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ return;
+ }
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ if (duration > ANIMATED_SCROLL_GAP) {
+ final int height = getHeight() - mPaddingBottom - mPaddingTop;
+ final int bottom = getChildAt(0).getHeight();
+ final int maxY = Math.max(0, bottom - height);
+ final int scrollY = mScrollY;
+ dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
+
+ mScroller.startScroll(mScrollX, scrollY, 0, dy);
+ postInvalidateOnAnimation();
+ } else {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+ 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() {
+ final int count = getChildCount();
+ final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop;
+ if (count == 0) {
+ return contentHeight;
+ }
+
+ int scrollRange = getChildAt(0).getBottom();
+ final int scrollY = mScrollY;
+ final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
+ if (scrollY < 0) {
+ scrollRange -= scrollY;
+ } else if (scrollY > overscrollBottom) {
+ scrollRange += scrollY - overscrollBottom;
+ }
+
+ return scrollRange;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return Math.max(0, super.computeVerticalScrollOffset());
+ }
+
+ @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);
+ final int verticalPadding = mPaddingTop + mPaddingBottom;
+ childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
+ 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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
+ heightUsed;
+ final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+ Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
+ 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 (oldX != x || oldY != y) {
+ final int range = getScrollRange();
+ final int overscrollMode = getOverScrollMode();
+ final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+ (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+ overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
+ 0, mOverflingDistance, false);
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+
+ if (canOverscroll) {
+ if (y < 0 && oldY >= 0) {
+ mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
+ } else if (y > range && oldY <= range) {
+ mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
+ }
+ }
+ }
+
+ if (!awakenScrollBars()) {
+ // Keep on drawing until the animation has finished.
+ postInvalidateOnAnimation();
+ }
+ } else {
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ if (getChildCount() == 0) return 0;
+
+ 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(0).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 (focused != null && focused.getRevealOnFocusHint()) {
+ 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());
+
+ return scrollToChildRect(rectangle, immediate);
+ }
+
+ @Override
+ public void requestLayout() {
+ mIsLayoutDirty = true;
+ super.requestLayout();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+ if (mFlingStrictSpan != null) {
+ mFlingStrictSpan.finish();
+ mFlingStrictSpan = null;
+ }
+ }
+
+ @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;
+
+ if (!isLaidOut()) {
+ if (mSavedState != null) {
+ mScrollY = mSavedState.scrollPosition;
+ mSavedState = null;
+ } // mScrollY default value is "0"
+
+ final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
+ final int scrollRange = Math.max(0,
+ childHeight - (b - t - mPaddingBottom - mPaddingTop));
+
+ // Don't forget to clamp
+ if (mScrollY > scrollRange) {
+ mScrollY = scrollRange;
+ } else if (mScrollY < 0) {
+ mScrollY = 0;
+ }
+ }
+
+ // Calling this with the present values causes it to re-claim them
+ scrollTo(mScrollX, mScrollY);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ View currentFocused = findFocus();
+ if (null == currentFocused || this == currentFocused)
+ return;
+
+ // If the currently-focused view was visible on the screen when the
+ // screen was at the old height, then scroll the screen to make that
+ // view visible with the new screen height.
+ if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
+ currentFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollY(scrollDelta);
+ }
+ }
+
+ /**
+ * Return true if child is a descendant of parent, (or equal to the parent).
+ */
+ private static 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/cursor is moving down the screen,
+ * which means we want to scroll towards the top.
+ */
+ public void fling(int velocityY) {
+ if (getChildCount() > 0) {
+ int height = getHeight() - mPaddingBottom - mPaddingTop;
+ int bottom = getChildAt(0).getHeight();
+
+ mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
+ Math.max(0, bottom - height), 0, height/2);
+
+ if (mFlingStrictSpan == null) {
+ mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
+ }
+
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private void flingWithNestedDispatch(int velocityY) {
+ final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
+ (mScrollY < getScrollRange() || velocityY < 0);
+ if (!dispatchNestedPreFling(0, velocityY)) {
+ dispatchNestedFling(0, velocityY, canFling);
+ if (canFling) {
+ fling(velocityY);
+ }
+ }
+ }
+
+ private void endDrag() {
+ mIsBeingDragged = false;
+
+ recycleVelocityTracker();
+
+ if (mEdgeGlowTop != null) {
+ mEdgeGlowTop.onRelease();
+ mEdgeGlowBottom.onRelease();
+ }
+
+ if (mScrollStrictSpan != null) {
+ mScrollStrictSpan.finish();
+ mScrollStrictSpan = null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This version also clamps the scrolling to the bounds of our child.
+ */
+ @Override
+ 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, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
+ y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
+ if (x != mScrollX || y != mScrollY) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ @Override
+ public void setOverScrollMode(int mode) {
+ if (mode != OVER_SCROLL_NEVER) {
+ if (mEdgeGlowTop == null) {
+ Context context = getContext();
+ mEdgeGlowTop = new EdgeEffect(context);
+ mEdgeGlowBottom = new EdgeEffect(context);
+ }
+ } else {
+ mEdgeGlowTop = null;
+ mEdgeGlowBottom = null;
+ }
+ super.setOverScrollMode(mode);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ startNestedScroll(SCROLL_AXIS_VERTICAL);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void onStopNestedScroll(View target) {
+ super.onStopNestedScroll(target);
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ final int oldScrollY = mScrollY;
+ scrollBy(0, dyUnconsumed);
+ final int myConsumed = mScrollY - oldScrollY;
+ final int myUnconsumed = dyUnconsumed - myConsumed;
+ dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (!consumed) {
+ flingWithNestedDispatch((int) velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mEdgeGlowTop != null) {
+ final int scrollY = mScrollY;
+ final boolean clipToPadding = getClipToPadding();
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width;
+ final int height;
+ final float translateX;
+ final float translateY;
+ if (clipToPadding) {
+ width = getWidth() - mPaddingLeft - mPaddingRight;
+ height = getHeight() - mPaddingTop - mPaddingBottom;
+ translateX = mPaddingLeft;
+ translateY = mPaddingTop;
+ } else {
+ width = getWidth();
+ height = getHeight();
+ translateX = 0;
+ translateY = 0;
+ }
+ canvas.translate(translateX, Math.min(0, scrollY) + translateY);
+ mEdgeGlowTop.setSize(width, height);
+ if (mEdgeGlowTop.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowBottom.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width;
+ final int height;
+ final float translateX;
+ final float translateY;
+ if (clipToPadding) {
+ width = getWidth() - mPaddingLeft - mPaddingRight;
+ height = getHeight() - mPaddingTop - mPaddingBottom;
+ translateX = mPaddingLeft;
+ translateY = mPaddingTop;
+ } else {
+ width = getWidth();
+ height = getHeight();
+ translateX = 0;
+ translateY = 0;
+ }
+ canvas.translate(-width + translateX,
+ Math.max(getScrollRange(), scrollY) + height + translateY);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+ }
+
+ private static 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;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ // Some old apps reused IDs in ways they shouldn't have.
+ // Don't break them, but they don't get scroll state restoration.
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mSavedState = ss;
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ // Some old apps reused IDs in ways they shouldn't have.
+ // Don't break them, but they don't get scroll state restoration.
+ return super.onSaveInstanceState();
+ }
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.scrollPosition = mScrollY;
+ return ss;
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("fillViewport", mFillViewport);
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int scrollPosition;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ scrollPosition = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(scrollPosition);
+ }
+
+ @Override
+ public String toString() {
+ return "ScrollView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " scrollPosition=" + scrollPosition + "}";
+ }
+
+ 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];
+ }
+ };
+ }
+
+}
diff --git a/android/widget/Scroller.java b/android/widget/Scroller.java
new file mode 100644
index 0000000..357c9c3
--- /dev/null
+++ b/android/widget/Scroller.java
@@ -0,0 +1,597 @@
+/*
+ * 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.hardware.SensorManager;
+import android.os.Build;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
+ * or {@link OverScroller}) to collect the data you need to produce a scrolling
+ * animation—for example, in response to a fling gesture. Scrollers track
+ * scroll offsets for you over time, but they don't automatically apply those
+ * positions to your view. It's your responsibility to get and apply new
+ * coordinates at a rate that will make the scrolling animation look smooth.</p>
+ *
+ * <p>Here is a simple example:</p>
+ *
+ * <pre> private Scroller mScroller = new Scroller(context);
+ * ...
+ * public void zoomIn() {
+ * // Revert any animation currently in progress
+ * mScroller.forceFinished(true);
+ * // Start scrolling by providing a starting point and
+ * // the distance to travel
+ * mScroller.startScroll(0, 0, 100, 0);
+ * // Invalidate to request a redraw
+ * invalidate();
+ * }</pre>
+ *
+ * <p>To track the changing positions of the x/y coordinates, use
+ * {@link #computeScrollOffset}. The method returns a boolean to indicate
+ * whether the scroller is finished. If it isn't, it means that a fling or
+ * programmatic pan operation is still in progress. You can use this method to
+ * find the current offsets of the x and y coordinates, for example:</p>
+ *
+ * <pre>if (mScroller.computeScrollOffset()) {
+ * // Get current x and y positions
+ * int currX = mScroller.getCurrX();
+ * int currY = mScroller.getCurrY();
+ * ...
+ * }</pre>
+ */
+public class Scroller {
+ private final Interpolator mInterpolator;
+
+ 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 boolean mFinished;
+ private boolean mFlywheel;
+
+ private float mVelocity;
+ private float mCurrVelocity;
+ private int mDistance;
+
+ private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+ private static final int DEFAULT_DURATION = 250;
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+ private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+ private static final float START_TENSION = 0.5f;
+ private static final float END_TENSION = 1.0f;
+ private static final float P1 = START_TENSION * INFLEXION;
+ private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+ private static final int NB_SAMPLES = 100;
+ private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+ private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+ private float mDeceleration;
+ private final float mPpi;
+
+ // A context-specific coefficient adjusted to physical values.
+ private float mPhysicalCoeff;
+
+ static {
+ float x_min = 0.0f;
+ float y_min = 0.0f;
+ for (int i = 0; i < NB_SAMPLES; i++) {
+ final float alpha = (float) i / NB_SAMPLES;
+
+ float x_max = 1.0f;
+ float x, tx, coef;
+ while (true) {
+ x = x_min + (x_max - x_min) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+ if (Math.abs(tx - alpha) < 1E-5) break;
+ if (tx > alpha) x_max = x;
+ else x_min = x;
+ }
+ SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+ float y_max = 1.0f;
+ float y, dy;
+ while (true) {
+ y = y_min + (y_max - y_min) / 2.0f;
+ coef = 3.0f * y * (1.0f - y);
+ dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+ if (Math.abs(dy - alpha) < 1E-5) break;
+ if (dy > alpha) y_max = y;
+ else y_min = y;
+ }
+ SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+ }
+ SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+ }
+
+ /**
+ * 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. "Flywheel" behavior will
+ * be in effect for apps targeting Honeycomb or newer.
+ */
+ public Scroller(Context context, Interpolator interpolator) {
+ this(context, interpolator,
+ context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
+ }
+
+ /**
+ * Create a Scroller with the specified interpolator. If the interpolator is
+ * null, the default (viscous) interpolator will be used. Specify whether or
+ * not to support progressive "flywheel" behavior in flinging.
+ */
+ public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
+ mFinished = true;
+ if (interpolator == null) {
+ mInterpolator = new ViscousFluidInterpolator();
+ } else {
+ mInterpolator = interpolator;
+ }
+ mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
+ mFlywheel = flywheel;
+
+ mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
+ }
+
+ /**
+ * The amount of friction applied to flings. The default value
+ * is {@link ViewConfiguration#getScrollFriction}.
+ *
+ * @param friction A scalar dimension-less value representing the coefficient of
+ * friction.
+ */
+ public final void setFriction(float friction) {
+ mDeceleration = computeDeceleration(friction);
+ mFlingFriction = friction;
+ }
+
+ private float computeDeceleration(float friction) {
+ return SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * mPpi // pixels per inch
+ * friction;
+ }
+
+ /**
+ *
+ * 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 the current velocity.
+ *
+ * @return The original velocity less the deceleration. Result may be
+ * negative.
+ */
+ public float getCurrVelocity() {
+ return mMode == FLING_MODE ?
+ mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
+ }
+
+ /**
+ * Returns the start X offset in the scroll.
+ *
+ * @return The start X offset as an absolute distance from the origin.
+ */
+ public final int getStartX() {
+ return mStartX;
+ }
+
+ /**
+ * Returns the start Y offset in the scroll.
+ *
+ * @return The start Y offset as an absolute distance from the origin.
+ */
+ public final int getStartY() {
+ return mStartY;
+ }
+
+ /**
+ * 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.
+ */
+ public boolean computeScrollOffset() {
+ if (mFinished) {
+ return false;
+ }
+
+ int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+ if (timePassed < mDuration) {
+ switch (mMode) {
+ case SCROLL_MODE:
+ final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
+ mCurrX = mStartX + Math.round(x * mDeltaX);
+ mCurrY = mStartY + Math.round(x * mDeltaY);
+ break;
+ case FLING_MODE:
+ final float t = (float) timePassed / mDuration;
+ final int index = (int) (NB_SAMPLES * t);
+ float distanceCoef = 1.f;
+ float velocityCoef = 0.f;
+ if (index < NB_SAMPLES) {
+ final float t_inf = (float) index / NB_SAMPLES;
+ final float t_sup = (float) (index + 1) / NB_SAMPLES;
+ final float d_inf = SPLINE_POSITION[index];
+ final float d_sup = SPLINE_POSITION[index + 1];
+ velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+ distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+ }
+
+ mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
+
+ mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
+ // Pin to mMinX <= mCurrX <= mMaxX
+ mCurrX = Math.min(mCurrX, mMaxX);
+ mCurrX = Math.max(mCurrX, mMinX);
+
+ mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
+ // 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, the distance to travel,
+ * and the duration of the scroll.
+ *
+ * @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;
+ }
+
+ /**
+ * 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) {
+ // Continue a scroll or fling in progress
+ if (mFlywheel && !mFinished) {
+ float oldVel = getCurrVelocity();
+
+ float dx = (float) (mFinalX - mStartX);
+ float dy = (float) (mFinalY - mStartY);
+ float hyp = (float) Math.hypot(dx, dy);
+
+ float ndx = dx / hyp;
+ float ndy = dy / hyp;
+
+ float oldVelocityX = ndx * oldVel;
+ float oldVelocityY = ndy * oldVel;
+ if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+ Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+ velocityX += oldVelocityX;
+ velocityY += oldVelocityY;
+ }
+ }
+
+ mMode = FLING_MODE;
+ mFinished = false;
+
+ float velocity = (float) Math.hypot(velocityX, velocityY);
+
+ mVelocity = velocity;
+ mDuration = getSplineFlingDuration(velocity);
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartX = startX;
+ mStartY = startY;
+
+ float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+ float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+ double totalDistance = getSplineFlingDistance(velocity);
+ mDistance = (int) (totalDistance * Math.signum(velocity));
+
+ mMinX = minX;
+ mMaxX = maxX;
+ mMinY = minY;
+ mMaxY = maxY;
+
+ mFinalX = startX + (int) Math.round(totalDistance * coeffX);
+ // Pin to mMinX <= mFinalX <= mMaxX
+ mFinalX = Math.min(mFinalX, mMaxX);
+ mFinalX = Math.max(mFinalX, mMinX);
+
+ mFinalY = startY + (int) Math.round(totalDistance * coeffY);
+ // Pin to mMinY <= mFinalY <= mMaxY
+ mFinalY = Math.min(mFinalY, mMaxY);
+ mFinalY = Math.max(mFinalY, mMinY);
+ }
+
+ private double getSplineDeceleration(float velocity) {
+ return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+ }
+
+ private int getSplineFlingDuration(float velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return (int) (1000.0 * Math.exp(l / decelMinusOne));
+ }
+
+ private double getSplineFlingDistance(float velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating cause the scroller to move to the final x and y
+ * position
+ *
+ * @see #forceFinished(boolean)
+ */
+ 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 {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+ *
+ * @param extend Additional time to scroll in milliseconds.
+ * @see #setFinalX(int)
+ * @see #setFinalY(int)
+ */
+ public void extendDuration(int extend) {
+ int passed = timePassed();
+ mDuration = passed + extend;
+ mDurationReciprocal = 1.0f / mDuration;
+ mFinished = false;
+ }
+
+ /**
+ * Returns the time elapsed since the beginning of the scrolling.
+ *
+ * @return The elapsed time in milliseconds.
+ */
+ public int timePassed() {
+ return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+ }
+
+ /**
+ * Sets the final position (X) for this scroller.
+ *
+ * @param newX The new X offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalY(int)
+ */
+ public void setFinalX(int newX) {
+ mFinalX = newX;
+ mDeltaX = mFinalX - mStartX;
+ mFinished = false;
+ }
+
+ /**
+ * Sets the final position (Y) for this scroller.
+ *
+ * @param newY The new Y offset as an absolute distance from the origin.
+ * @see #extendDuration(int)
+ * @see #setFinalX(int)
+ */
+ public void setFinalY(int newY) {
+ mFinalY = newY;
+ mDeltaY = mFinalY - mStartY;
+ mFinished = false;
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isScrollingInDirection(float xvel, float yvel) {
+ return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
+ Math.signum(yvel) == Math.signum(mFinalY - mStartY);
+ }
+
+ static class ViscousFluidInterpolator implements Interpolator {
+ /** Controls the viscous fluid effect (how much of it). */
+ private static final float VISCOUS_FLUID_SCALE = 8.0f;
+
+ private static final float VISCOUS_FLUID_NORMALIZE;
+ private static final float VISCOUS_FLUID_OFFSET;
+
+ static {
+
+ // must be set to 1.0 (used in viscousFluid())
+ VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
+ // account for very small floating-point error
+ VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
+ }
+
+ private static float viscousFluid(float x) {
+ x *= VISCOUS_FLUID_SCALE;
+ 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);
+ }
+ return x;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
+ if (interpolated > 0) {
+ return interpolated + VISCOUS_FLUID_OFFSET;
+ }
+ return interpolated;
+ }
+ }
+}
diff --git a/android/widget/SearchView.java b/android/widget/SearchView.java
new file mode 100644
index 0000000..519a7dd
--- /dev/null
+++ b/android/widget/SearchView.java
@@ -0,0 +1,2080 @@
+/*
+ * Copyright (C) 2010 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 static android.widget.SuggestionsAdapter.getColumnString;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.speech.RecognizerIntent;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.CollapsibleActionView;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.R;
+
+import java.util.WeakHashMap;
+
+/**
+ * A widget that provides a user interface for the user to enter a search query and submit a request
+ * to a search provider. Shows a list of query suggestions or results, if available, and allows the
+ * user to pick a suggestion or result to launch into.
+ *
+ * <p>
+ * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
+ * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
+ * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
+ * </p>
+ * <p>
+ * If you want the search field to always be visible, then call setIconifiedByDefault(false).
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about using {@code SearchView}, read the
+ * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
+ * </div>
+ *
+ * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
+ * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+ * @attr ref android.R.styleable#SearchView_imeOptions
+ * @attr ref android.R.styleable#SearchView_inputType
+ * @attr ref android.R.styleable#SearchView_maxWidth
+ * @attr ref android.R.styleable#SearchView_queryHint
+ */
+public class SearchView extends LinearLayout implements CollapsibleActionView {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SearchView";
+
+ /**
+ * Private constant for removing the microphone in the keyboard.
+ */
+ private static final String IME_OPTION_NO_MICROPHONE = "nm";
+
+ private final SearchAutoComplete mSearchSrcTextView;
+ private final View mSearchEditFrame;
+ private final View mSearchPlate;
+ private final View mSubmitArea;
+ private final ImageView mSearchButton;
+ private final ImageView mGoButton;
+ private final ImageView mCloseButton;
+ private final ImageView mVoiceButton;
+ private final View mDropDownAnchor;
+
+ private UpdatableTouchDelegate mTouchDelegate;
+ private Rect mSearchSrcTextViewBounds = new Rect();
+ private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
+ private int[] mTemp = new int[2];
+ private int[] mTemp2 = new int[2];
+
+ /** Icon optionally displayed when the SearchView is collapsed. */
+ private final ImageView mCollapsedIcon;
+
+ /** Drawable used as an EditText hint. */
+ private final Drawable mSearchHintIcon;
+
+ // Resources used by SuggestionsAdapter to display suggestions.
+ private final int mSuggestionRowLayout;
+ private final int mSuggestionCommitIconResId;
+
+ // Intents used for voice searching.
+ private final Intent mVoiceWebSearchIntent;
+ private final Intent mVoiceAppSearchIntent;
+
+ private final CharSequence mDefaultQueryHint;
+
+ private OnQueryTextListener mOnQueryChangeListener;
+ private OnCloseListener mOnCloseListener;
+ private OnFocusChangeListener mOnQueryTextFocusChangeListener;
+ private OnSuggestionListener mOnSuggestionListener;
+ private OnClickListener mOnSearchClickListener;
+
+ private boolean mIconifiedByDefault;
+ private boolean mIconified;
+ private CursorAdapter mSuggestionsAdapter;
+ private boolean mSubmitButtonEnabled;
+ private CharSequence mQueryHint;
+ private boolean mQueryRefinement;
+ private boolean mClearingFocus;
+ private int mMaxWidth;
+ private boolean mVoiceButtonEnabled;
+ private CharSequence mOldQueryText;
+ private CharSequence mUserQuery;
+ private boolean mExpandedInActionView;
+ private int mCollapsedImeOptions;
+
+ private SearchableInfo mSearchable;
+ private Bundle mAppSearchData;
+
+ private Runnable mUpdateDrawableStateRunnable = new Runnable() {
+ public void run() {
+ updateFocusedState();
+ }
+ };
+
+ private Runnable mReleaseCursorRunnable = new Runnable() {
+ public void run() {
+ if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
+ mSuggestionsAdapter.changeCursor(null);
+ }
+ }
+ };
+
+ // A weak map of drawables we've gotten from other packages, so we don't load them
+ // more than once.
+ private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
+ new WeakHashMap<String, Drawable.ConstantState>();
+
+ /**
+ * Callbacks for changes to the query text.
+ */
+ public interface OnQueryTextListener {
+
+ /**
+ * Called when the user submits the query. This could be due to a key press on the
+ * keyboard or due to pressing a submit button.
+ * The listener can override the standard behavior by returning true
+ * to indicate that it has handled the submit request. Otherwise return false to
+ * let the SearchView handle the submission by launching any associated intent.
+ *
+ * @param query the query text that is to be submitted
+ *
+ * @return true if the query has been handled by the listener, false to let the
+ * SearchView perform the default action.
+ */
+ boolean onQueryTextSubmit(String query);
+
+ /**
+ * Called when the query text is changed by the user.
+ *
+ * @param newText the new content of the query text field.
+ *
+ * @return false if the SearchView should perform the default action of showing any
+ * suggestions if available, true if the action was handled by the listener.
+ */
+ boolean onQueryTextChange(String newText);
+ }
+
+ public interface OnCloseListener {
+
+ /**
+ * The user is attempting to close the SearchView.
+ *
+ * @return true if the listener wants to override the default behavior of clearing the
+ * text field and dismissing it, false otherwise.
+ */
+ boolean onClose();
+ }
+
+ /**
+ * Callback interface for selection events on suggestions. These callbacks
+ * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
+ */
+ public interface OnSuggestionListener {
+
+ /**
+ * Called when a suggestion was selected by navigating to it.
+ * @param position the absolute position in the list of suggestions.
+ *
+ * @return true if the listener handles the event and wants to override the default
+ * behavior of possibly rewriting the query based on the selected item, false otherwise.
+ */
+ boolean onSuggestionSelect(int position);
+
+ /**
+ * Called when a suggestion was clicked.
+ * @param position the absolute position of the clicked item in the list of suggestions.
+ *
+ * @return true if the listener handles the event and wants to override the default
+ * behavior of launching any intent or submitting a search query specified on that item.
+ * Return false otherwise.
+ */
+ boolean onSuggestionClick(int position);
+ }
+
+ public SearchView(Context context) {
+ this(context, null);
+ }
+
+ public SearchView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.searchViewStyle);
+ }
+
+ public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
+ final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final int layoutResId = a.getResourceId(
+ R.styleable.SearchView_layout, R.layout.search_view);
+ inflater.inflate(layoutResId, this, true);
+
+ mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
+ mSearchSrcTextView.setSearchView(this);
+
+ mSearchEditFrame = findViewById(R.id.search_edit_frame);
+ mSearchPlate = findViewById(R.id.search_plate);
+ mSubmitArea = findViewById(R.id.submit_area);
+ mSearchButton = (ImageView) findViewById(R.id.search_button);
+ mGoButton = (ImageView) findViewById(R.id.search_go_btn);
+ mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
+ mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
+ mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
+
+ // Set up icons and backgrounds.
+ mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
+ mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
+ mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
+ mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
+ mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
+ mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
+ mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
+
+ // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
+ // style does not have an explicit value set, fall back to that.
+ if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
+ mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
+ } else {
+ mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
+ }
+
+ // Extract dropdown layout resource IDs for later use.
+ mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
+ R.layout.search_dropdown_item_icons_2line);
+ mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
+
+ mSearchButton.setOnClickListener(mOnClickListener);
+ mCloseButton.setOnClickListener(mOnClickListener);
+ mGoButton.setOnClickListener(mOnClickListener);
+ mVoiceButton.setOnClickListener(mOnClickListener);
+ mSearchSrcTextView.setOnClickListener(mOnClickListener);
+
+ mSearchSrcTextView.addTextChangedListener(mTextWatcher);
+ mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
+ mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
+ mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
+ mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
+
+ // Inform any listener of focus changes
+ mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
+
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (mOnQueryTextFocusChangeListener != null) {
+ mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
+ }
+ }
+ });
+ setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
+
+ final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
+ if (maxWidth != -1) {
+ setMaxWidth(maxWidth);
+ }
+
+ mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
+ mQueryHint = a.getText(R.styleable.SearchView_queryHint);
+
+ final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
+ if (imeOptions != -1) {
+ setImeOptions(imeOptions);
+ }
+
+ final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
+ if (inputType != -1) {
+ setInputType(inputType);
+ }
+
+ if (getFocusable() == FOCUSABLE_AUTO) {
+ setFocusable(FOCUSABLE);
+ }
+
+ a.recycle();
+
+ // Save voice intent for later queries/launching
+ mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+ mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+
+ mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
+ if (mDropDownAnchor != null) {
+ mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ adjustDropDownSizeAndPosition();
+ }
+ });
+ }
+
+ updateViewsVisibility(mIconifiedByDefault);
+ updateQueryHint();
+ }
+
+ int getSuggestionRowLayout() {
+ return mSuggestionRowLayout;
+ }
+
+ int getSuggestionCommitIconResId() {
+ return mSuggestionCommitIconResId;
+ }
+
+ /**
+ * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
+ * to display labels, hints, suggestions, create intents for launching search results screens
+ * and controlling other affordances such as a voice button.
+ *
+ * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
+ * activity or a global search provider.
+ */
+ public void setSearchableInfo(SearchableInfo searchable) {
+ mSearchable = searchable;
+ if (mSearchable != null) {
+ updateSearchAutoComplete();
+ updateQueryHint();
+ }
+ // Cache the voice search capability
+ mVoiceButtonEnabled = hasVoiceSearch();
+
+ if (mVoiceButtonEnabled) {
+ // Disable the microphone on the keyboard, as a mic is displayed near the text box
+ // TODO: use imeOptions to disable voice input when the new API will be available
+ mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
+ }
+ updateViewsVisibility(isIconified());
+ }
+
+ /**
+ * Sets the APP_DATA for legacy SearchDialog use.
+ * @param appSearchData bundle provided by the app when launching the search dialog
+ * @hide
+ */
+ public void setAppSearchData(Bundle appSearchData) {
+ mAppSearchData = appSearchData;
+ }
+
+ /**
+ * Sets the IME options on the query text field.
+ *
+ * @see TextView#setImeOptions(int)
+ * @param imeOptions the options to set on the query text field
+ *
+ * @attr ref android.R.styleable#SearchView_imeOptions
+ */
+ public void setImeOptions(int imeOptions) {
+ mSearchSrcTextView.setImeOptions(imeOptions);
+ }
+
+ /**
+ * Returns the IME options set on the query text field.
+ * @return the ime options
+ * @see TextView#setImeOptions(int)
+ *
+ * @attr ref android.R.styleable#SearchView_imeOptions
+ */
+ public int getImeOptions() {
+ return mSearchSrcTextView.getImeOptions();
+ }
+
+ /**
+ * Sets the input type on the query text field.
+ *
+ * @see TextView#setInputType(int)
+ * @param inputType the input type to set on the query text field
+ *
+ * @attr ref android.R.styleable#SearchView_inputType
+ */
+ public void setInputType(int inputType) {
+ mSearchSrcTextView.setInputType(inputType);
+ }
+
+ /**
+ * Returns the input type set on the query text field.
+ * @return the input type
+ *
+ * @attr ref android.R.styleable#SearchView_inputType
+ */
+ public int getInputType() {
+ return mSearchSrcTextView.getInputType();
+ }
+
+ /** @hide */
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ // Don't accept focus if in the middle of clearing focus
+ if (mClearingFocus) return false;
+ // Check if SearchView is focusable.
+ if (!isFocusable()) return false;
+ // If it is not iconified, then give the focus to the text field
+ if (!isIconified()) {
+ boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
+ if (result) {
+ updateViewsVisibility(false);
+ }
+ return result;
+ } else {
+ return super.requestFocus(direction, previouslyFocusedRect);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void clearFocus() {
+ mClearingFocus = true;
+ super.clearFocus();
+ mSearchSrcTextView.clearFocus();
+ mSearchSrcTextView.setImeVisibility(false);
+ mClearingFocus = false;
+ }
+
+ /**
+ * Sets a listener for user actions within the SearchView.
+ *
+ * @param listener the listener object that receives callbacks when the user performs
+ * actions in the SearchView such as clicking on buttons or typing a query.
+ */
+ public void setOnQueryTextListener(OnQueryTextListener listener) {
+ mOnQueryChangeListener = listener;
+ }
+
+ /**
+ * Sets a listener to inform when the user closes the SearchView.
+ *
+ * @param listener the listener to call when the user closes the SearchView.
+ */
+ public void setOnCloseListener(OnCloseListener listener) {
+ mOnCloseListener = listener;
+ }
+
+ /**
+ * Sets a listener to inform when the focus of the query text field changes.
+ *
+ * @param listener the listener to inform of focus changes.
+ */
+ public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
+ mOnQueryTextFocusChangeListener = listener;
+ }
+
+ /**
+ * Sets a listener to inform when a suggestion is focused or clicked.
+ *
+ * @param listener the listener to inform of suggestion selection events.
+ */
+ public void setOnSuggestionListener(OnSuggestionListener listener) {
+ mOnSuggestionListener = listener;
+ }
+
+ /**
+ * Sets a listener to inform when the search button is pressed. This is only
+ * relevant when the text field is not visible by default. Calling {@link #setIconified
+ * setIconified(false)} can also cause this listener to be informed.
+ *
+ * @param listener the listener to inform when the search button is clicked or
+ * the text field is programmatically de-iconified.
+ */
+ public void setOnSearchClickListener(OnClickListener listener) {
+ mOnSearchClickListener = listener;
+ }
+
+ /**
+ * Returns the query string currently in the text field.
+ *
+ * @return the query string
+ */
+ public CharSequence getQuery() {
+ return mSearchSrcTextView.getText();
+ }
+
+ /**
+ * Sets a query string in the text field and optionally submits the query as well.
+ *
+ * @param query the query string. This replaces any query text already present in the
+ * text field.
+ * @param submit whether to submit the query right now or only update the contents of
+ * text field.
+ */
+ public void setQuery(CharSequence query, boolean submit) {
+ mSearchSrcTextView.setText(query);
+ if (query != null) {
+ mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
+ mUserQuery = query;
+ }
+
+ // If the query is not empty and submit is requested, submit the query
+ if (submit && !TextUtils.isEmpty(query)) {
+ onSubmitQuery();
+ }
+ }
+
+ /**
+ * Sets the hint text to display in the query text field. This overrides
+ * any hint specified in the {@link SearchableInfo}.
+ * <p>
+ * This value may be specified as an empty string to prevent any query hint
+ * from being displayed.
+ *
+ * @param hint the hint text to display or {@code null} to clear
+ * @attr ref android.R.styleable#SearchView_queryHint
+ */
+ public void setQueryHint(@Nullable CharSequence hint) {
+ mQueryHint = hint;
+ updateQueryHint();
+ }
+
+ /**
+ * Returns the hint text that will be displayed in the query text field.
+ * <p>
+ * The displayed query hint is chosen in the following order:
+ * <ol>
+ * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
+ * <li>Value specified in XML using
+ * {@link android.R.styleable#SearchView_queryHint android:queryHint}
+ * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
+ * {@link SearchableInfo#getHintId()}
+ * <li>Default hint provided by the theme against which the view was
+ * inflated
+ * </ol>
+ *
+ * @return the displayed query hint text, or {@code null} if none set
+ * @attr ref android.R.styleable#SearchView_queryHint
+ */
+ @Nullable
+ public CharSequence getQueryHint() {
+ final CharSequence hint;
+ if (mQueryHint != null) {
+ hint = mQueryHint;
+ } else if (mSearchable != null && mSearchable.getHintId() != 0) {
+ hint = getContext().getText(mSearchable.getHintId());
+ } else {
+ hint = mDefaultQueryHint;
+ }
+ return hint;
+ }
+
+ /**
+ * Sets the default or resting state of the search field. If true, a single search icon is
+ * shown by default and expands to show the text field and other buttons when pressed. Also,
+ * if the default state is iconified, then it collapses to that state when the close button
+ * is pressed. Changes to this property will take effect immediately.
+ *
+ * <p>The default value is true.</p>
+ *
+ * @param iconified whether the search field should be iconified by default
+ *
+ * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+ */
+ public void setIconifiedByDefault(boolean iconified) {
+ if (mIconifiedByDefault == iconified) return;
+ mIconifiedByDefault = iconified;
+ updateViewsVisibility(iconified);
+ updateQueryHint();
+ }
+
+ /**
+ * Returns the default iconified state of the search field.
+ * @return
+ *
+ * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+ */
+ public boolean isIconfiedByDefault() {
+ return mIconifiedByDefault;
+ }
+
+ /**
+ * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
+ * a temporary state and does not override the default iconified state set by
+ * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
+ * a false here will only be valid until the user closes the field. And if the default
+ * state is expanded, then a true here will only clear the text field and not close it.
+ *
+ * @param iconify a true value will collapse the SearchView to an icon, while a false will
+ * expand it.
+ */
+ public void setIconified(boolean iconify) {
+ if (iconify) {
+ onCloseClicked();
+ } else {
+ onSearchClicked();
+ }
+ }
+
+ /**
+ * Returns the current iconified state of the SearchView.
+ *
+ * @return true if the SearchView is currently iconified, false if the search field is
+ * fully visible.
+ */
+ public boolean isIconified() {
+ return mIconified;
+ }
+
+ /**
+ * Enables showing a submit button when the query is non-empty. In cases where the SearchView
+ * is being used to filter the contents of the current activity and doesn't launch a separate
+ * results activity, then the submit button should be disabled.
+ *
+ * @param enabled true to show a submit button for submitting queries, false if a submit
+ * button is not required.
+ */
+ public void setSubmitButtonEnabled(boolean enabled) {
+ mSubmitButtonEnabled = enabled;
+ updateViewsVisibility(isIconified());
+ }
+
+ /**
+ * Returns whether the submit button is enabled when necessary or never displayed.
+ *
+ * @return whether the submit button is enabled automatically when necessary
+ */
+ public boolean isSubmitButtonEnabled() {
+ return mSubmitButtonEnabled;
+ }
+
+ /**
+ * Specifies if a query refinement button should be displayed alongside each suggestion
+ * or if it should depend on the flags set in the individual items retrieved from the
+ * suggestions provider. Clicking on the query refinement button will replace the text
+ * in the query text field with the text from the suggestion. This flag only takes effect
+ * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
+ * and not when using a custom adapter.
+ *
+ * @param enable true if all items should have a query refinement button, false if only
+ * those items that have a query refinement flag set should have the button.
+ *
+ * @see SearchManager#SUGGEST_COLUMN_FLAGS
+ * @see SearchManager#FLAG_QUERY_REFINEMENT
+ */
+ public void setQueryRefinementEnabled(boolean enable) {
+ mQueryRefinement = enable;
+ if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
+ ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
+ enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
+ }
+ }
+
+ /**
+ * Returns whether query refinement is enabled for all items or only specific ones.
+ * @return true if enabled for all items, false otherwise.
+ */
+ public boolean isQueryRefinementEnabled() {
+ return mQueryRefinement;
+ }
+
+ /**
+ * You can set a custom adapter if you wish. Otherwise the default adapter is used to
+ * display the suggestions from the suggestions provider associated with the SearchableInfo.
+ *
+ * @see #setSearchableInfo(SearchableInfo)
+ */
+ public void setSuggestionsAdapter(CursorAdapter adapter) {
+ mSuggestionsAdapter = adapter;
+
+ mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
+ }
+
+ /**
+ * Returns the adapter used for suggestions, if any.
+ * @return the suggestions adapter
+ */
+ public CursorAdapter getSuggestionsAdapter() {
+ return mSuggestionsAdapter;
+ }
+
+ /**
+ * Makes the view at most this many pixels wide
+ *
+ * @attr ref android.R.styleable#SearchView_maxWidth
+ */
+ public void setMaxWidth(int maxpixels) {
+ mMaxWidth = maxpixels;
+
+ requestLayout();
+ }
+
+ /**
+ * Gets the specified maximum width in pixels, if set. Returns zero if
+ * no maximum width was specified.
+ * @return the maximum width of the view
+ *
+ * @attr ref android.R.styleable#SearchView_maxWidth
+ */
+ public int getMaxWidth() {
+ return mMaxWidth;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Let the standard measurements take effect in iconified state.
+ if (isIconified()) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+
+ switch (widthMode) {
+ case MeasureSpec.AT_MOST:
+ // If there is an upper limit, don't exceed maximum width (explicit or implicit)
+ if (mMaxWidth > 0) {
+ width = Math.min(mMaxWidth, width);
+ } else {
+ width = Math.min(getPreferredWidth(), width);
+ }
+ break;
+ case MeasureSpec.EXACTLY:
+ // If an exact width is specified, still don't exceed any specified maximum width
+ if (mMaxWidth > 0) {
+ width = Math.min(mMaxWidth, width);
+ }
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ // Use maximum width, if specified, else preferred width
+ width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
+ break;
+ }
+ widthMode = MeasureSpec.EXACTLY;
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ switch (heightMode) {
+ case MeasureSpec.AT_MOST:
+ height = Math.min(getPreferredHeight(), height);
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ height = getPreferredHeight();
+ break;
+ }
+ heightMode = MeasureSpec.EXACTLY;
+
+ super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
+ MeasureSpec.makeMeasureSpec(height, heightMode));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (changed) {
+ // Expand mSearchSrcTextView touch target to be the height of the parent in order to
+ // allow it to be up to 48dp.
+ getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
+ mSearchSrtTextViewBoundsExpanded.set(
+ mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
+ if (mTouchDelegate == null) {
+ mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
+ mSearchSrcTextViewBounds, mSearchSrcTextView);
+ setTouchDelegate(mTouchDelegate);
+ } else {
+ mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
+ }
+ }
+ }
+
+ private void getChildBoundsWithinSearchView(View view, Rect rect) {
+ view.getLocationInWindow(mTemp);
+ getLocationInWindow(mTemp2);
+ final int top = mTemp[1] - mTemp2[1];
+ final int left = mTemp[0] - mTemp2[0];
+ rect.set(left , top, left + view.getWidth(), top + view.getHeight());
+ }
+
+ private int getPreferredWidth() {
+ return getContext().getResources()
+ .getDimensionPixelSize(R.dimen.search_view_preferred_width);
+ }
+
+ private int getPreferredHeight() {
+ return getContext().getResources()
+ .getDimensionPixelSize(R.dimen.search_view_preferred_height);
+ }
+
+ private void updateViewsVisibility(final boolean collapsed) {
+ mIconified = collapsed;
+ // Visibility of views that are visible when collapsed
+ final int visCollapsed = collapsed ? VISIBLE : GONE;
+ // Is there text in the query
+ final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
+
+ mSearchButton.setVisibility(visCollapsed);
+ updateSubmitButton(hasText);
+ mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
+
+ final int iconVisibility;
+ if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
+ iconVisibility = GONE;
+ } else {
+ iconVisibility = VISIBLE;
+ }
+ mCollapsedIcon.setVisibility(iconVisibility);
+
+ updateCloseButton();
+ updateVoiceButton(!hasText);
+ updateSubmitArea();
+ }
+
+ private boolean hasVoiceSearch() {
+ if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
+ Intent testIntent = null;
+ if (mSearchable.getVoiceSearchLaunchWebSearch()) {
+ testIntent = mVoiceWebSearchIntent;
+ } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
+ testIntent = mVoiceAppSearchIntent;
+ }
+ if (testIntent != null) {
+ ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return ri != null;
+ }
+ }
+ return false;
+ }
+
+ private boolean isSubmitAreaEnabled() {
+ return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
+ }
+
+ private void updateSubmitButton(boolean hasText) {
+ int visibility = GONE;
+ if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
+ && (hasText || !mVoiceButtonEnabled)) {
+ visibility = VISIBLE;
+ }
+ mGoButton.setVisibility(visibility);
+ }
+
+ private void updateSubmitArea() {
+ int visibility = GONE;
+ if (isSubmitAreaEnabled()
+ && (mGoButton.getVisibility() == VISIBLE
+ || mVoiceButton.getVisibility() == VISIBLE)) {
+ visibility = VISIBLE;
+ }
+ mSubmitArea.setVisibility(visibility);
+ }
+
+ private void updateCloseButton() {
+ final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
+ // Should we show the close button? It is not shown if there's no focus,
+ // field is not iconified by default and there is no text in it.
+ final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
+ mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
+ final Drawable closeButtonImg = mCloseButton.getDrawable();
+ if (closeButtonImg != null){
+ closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
+ }
+ }
+
+ private void postUpdateFocusedState() {
+ post(mUpdateDrawableStateRunnable);
+ }
+
+ private void updateFocusedState() {
+ final boolean focused = mSearchSrcTextView.hasFocus();
+ final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
+ final Drawable searchPlateBg = mSearchPlate.getBackground();
+ if (searchPlateBg != null) {
+ searchPlateBg.setState(stateSet);
+ }
+ final Drawable submitAreaBg = mSubmitArea.getBackground();
+ if (submitAreaBg != null) {
+ submitAreaBg.setState(stateSet);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ removeCallbacks(mUpdateDrawableStateRunnable);
+ post(mReleaseCursorRunnable);
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * Called by the SuggestionsAdapter
+ * @hide
+ */
+ /* package */void onQueryRefine(CharSequence queryText) {
+ setQuery(queryText);
+ }
+
+ private final OnClickListener mOnClickListener = new OnClickListener() {
+
+ public void onClick(View v) {
+ if (v == mSearchButton) {
+ onSearchClicked();
+ } else if (v == mCloseButton) {
+ onCloseClicked();
+ } else if (v == mGoButton) {
+ onSubmitQuery();
+ } else if (v == mVoiceButton) {
+ onVoiceClicked();
+ } else if (v == mSearchSrcTextView) {
+ forceSuggestionQuery();
+ }
+ }
+ };
+
+ /**
+ * Handles the key down event for dealing with action keys.
+ *
+ * @param keyCode This is the keycode of the typed key, and is the same value as
+ * found in the KeyEvent parameter.
+ * @param event The complete event record for the typed key
+ *
+ * @return true if the event was handled here, or false if not.
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mSearchable == null) {
+ return false;
+ }
+
+ // if it's an action specified by the searchable activity, launch the
+ // entered query with the action key
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+ launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
+ .toString());
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * React to the user typing "enter" or other hardwired keys while typing in
+ * the search box. This handles these special keys while the edit box has
+ * focus.
+ */
+ View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return false;
+ }
+
+ if (DBG) {
+ Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
+ + mSearchSrcTextView.getListSelection());
+ }
+
+ // If a suggestion is selected, handle enter, search key, and action keys
+ // as presses on the selected suggestion
+ if (mSearchSrcTextView.isPopupShowing()
+ && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
+ return onSuggestionsKey(v, keyCode, event);
+ }
+
+ // If there is text in the query box, handle enter, and action keys
+ // The search key is handled by the dialog's onKeyDown().
+ if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ v.cancelLongPress();
+
+ // Launch as a regular search.
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
+ .toString());
+ return true;
+ }
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+ launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
+ .getText().toString());
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ /**
+ * React to the user typing while in the suggestions list. First, check for
+ * action keys. If not handled, try refocusing regular characters into the
+ * EditText.
+ */
+ private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
+ // guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable == null) {
+ return false;
+ }
+ if (mSuggestionsAdapter == null) {
+ return false;
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
+ // First, check for enter or search (both of which we'll treat as a
+ // "click")
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
+ || keyCode == KeyEvent.KEYCODE_TAB) {
+ int position = mSearchSrcTextView.getListSelection();
+ return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+
+ // Next, check for left/right moves, which we use to "return" the
+ // user to the edit view
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // give "focus" to text editor, with cursor at the beginning if
+ // left key, at end if right key
+ // TODO: Reverse left/right for right-to-left languages, e.g.
+ // Arabic
+ int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
+ .length();
+ mSearchSrcTextView.setSelection(selPoint);
+ mSearchSrcTextView.setListSelection(0);
+ mSearchSrcTextView.clearListSelection();
+ mSearchSrcTextView.ensureImeVisible(true);
+
+ return true;
+ }
+
+ // Next, check for an "up and out" move
+ if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
+ // TODO: restoreUserQuery();
+ // let ACTV complete the move
+ return false;
+ }
+
+ // Next, check for an "action key"
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null)
+ && ((actionKey.getSuggestActionMsg() != null) || (actionKey
+ .getSuggestActionMsgColumn() != null))) {
+ // launch suggestion using action key column
+ int position = mSearchSrcTextView.getListSelection();
+ if (position != ListView.INVALID_POSITION) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if (c.moveToPosition(position)) {
+ final String actionMsg = getActionKeyMessage(c, actionKey);
+ if (actionMsg != null && (actionMsg.length() > 0)) {
+ return onItemClicked(position, keyCode, actionMsg);
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * For a given suggestion and a given cursor row, get the action message. If
+ * not provided by the specific row/column, also check for a single
+ * definition (for the action key).
+ *
+ * @param c The cursor providing suggestions
+ * @param actionKey The actionkey record being examined
+ *
+ * @return Returns a string, or null if no action key message for this
+ * suggestion
+ */
+ private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
+ String result = null;
+ // check first in the cursor data, for a suggestion-specific message
+ final String column = actionKey.getSuggestActionMsgColumn();
+ if (column != null) {
+ result = SuggestionsAdapter.getColumnString(c, column);
+ }
+ // If the cursor didn't give us a message, see if there's a single
+ // message defined
+ // for the actionkey (for all suggestions)
+ if (result == null) {
+ result = actionKey.getSuggestActionMsg();
+ }
+ return result;
+ }
+
+ private CharSequence getDecoratedHint(CharSequence hintText) {
+ // If the field is always expanded or we don't have a search hint icon,
+ // then don't add the search icon to the hint.
+ if (!mIconifiedByDefault || mSearchHintIcon == null) {
+ return hintText;
+ }
+
+ final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
+ mSearchHintIcon.setBounds(0, 0, textSize, textSize);
+
+ final SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
+ ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ ssb.append(hintText);
+ return ssb;
+ }
+
+ private void updateQueryHint() {
+ final CharSequence hint = getQueryHint();
+ mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
+ }
+
+ /**
+ * Updates the auto-complete text view.
+ */
+ private void updateSearchAutoComplete() {
+ mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
+ mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
+ mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
+ int inputType = mSearchable.getInputType();
+ // We only touch this if the input type is set up for text (which it almost certainly
+ // should be, in the case of search!)
+ if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+ // The existence of a suggestions authority is the proxy for "suggestions
+ // are available here"
+ inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ if (mSearchable.getSuggestAuthority() != null) {
+ inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
+ // auto-completion based on its own semantics, which it will present to the user
+ // as they type. This generally means that the input method should not show its
+ // own candidates, and the spell checker should not be in action. The text editor
+ // supplies its candidates by calling InputMethodManager.displayCompletions(),
+ // which in turn will call InputMethodSession.displayCompletions().
+ inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+ }
+ }
+ mSearchSrcTextView.setInputType(inputType);
+ if (mSuggestionsAdapter != null) {
+ mSuggestionsAdapter.changeCursor(null);
+ }
+ // attach the suggestions adapter, if suggestions are available
+ // The existence of a suggestions authority is the proxy for "suggestions available here"
+ if (mSearchable.getSuggestAuthority() != null) {
+ mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
+ this, mSearchable, mOutsideDrawablesCache);
+ mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
+ ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
+ mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
+ : SuggestionsAdapter.REFINE_BY_ENTRY);
+ }
+ }
+
+ /**
+ * Update the visibility of the voice button. There are actually two voice search modes,
+ * either of which will activate the button.
+ * @param empty whether the search query text field is empty. If it is, then the other
+ * criteria apply to make the voice button visible.
+ */
+ private void updateVoiceButton(boolean empty) {
+ int visibility = GONE;
+ if (mVoiceButtonEnabled && !isIconified() && empty) {
+ visibility = VISIBLE;
+ mGoButton.setVisibility(GONE);
+ }
+ mVoiceButton.setVisibility(visibility);
+ }
+
+ private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
+
+ /**
+ * Called when the input method default action key is pressed.
+ */
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ onSubmitQuery();
+ return true;
+ }
+ };
+
+ private void onTextChanged(CharSequence newText) {
+ CharSequence text = mSearchSrcTextView.getText();
+ mUserQuery = text;
+ boolean hasText = !TextUtils.isEmpty(text);
+ updateSubmitButton(hasText);
+ updateVoiceButton(!hasText);
+ updateCloseButton();
+ updateSubmitArea();
+ if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
+ mOnQueryChangeListener.onQueryTextChange(newText.toString());
+ }
+ mOldQueryText = newText.toString();
+ }
+
+ private void onSubmitQuery() {
+ CharSequence query = mSearchSrcTextView.getText();
+ if (query != null && TextUtils.getTrimmedLength(query) > 0) {
+ if (mOnQueryChangeListener == null
+ || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
+ if (mSearchable != null) {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
+ }
+ mSearchSrcTextView.setImeVisibility(false);
+ dismissSuggestions();
+ }
+ }
+ }
+
+ private void dismissSuggestions() {
+ mSearchSrcTextView.dismissDropDown();
+ }
+
+ private void onCloseClicked() {
+ CharSequence text = mSearchSrcTextView.getText();
+ if (TextUtils.isEmpty(text)) {
+ if (mIconifiedByDefault) {
+ // If the app doesn't override the close behavior
+ if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
+ // hide the keyboard and remove focus
+ clearFocus();
+ // collapse the search field
+ updateViewsVisibility(true);
+ }
+ }
+ } else {
+ mSearchSrcTextView.setText("");
+ mSearchSrcTextView.requestFocus();
+ mSearchSrcTextView.setImeVisibility(true);
+ }
+
+ }
+
+ private void onSearchClicked() {
+ updateViewsVisibility(false);
+ mSearchSrcTextView.requestFocus();
+ mSearchSrcTextView.setImeVisibility(true);
+ if (mOnSearchClickListener != null) {
+ mOnSearchClickListener.onClick(this);
+ }
+ }
+
+ private void onVoiceClicked() {
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return;
+ }
+ SearchableInfo searchable = mSearchable;
+ try {
+ if (searchable.getVoiceSearchLaunchWebSearch()) {
+ Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
+ searchable);
+ getContext().startActivity(webSearchIntent);
+ } else if (searchable.getVoiceSearchLaunchRecognizer()) {
+ Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
+ searchable);
+ getContext().startActivity(appSearchIntent);
+ }
+ } catch (ActivityNotFoundException e) {
+ // Should not happen, since we check the availability of
+ // voice search before showing the button. But just in case...
+ Log.w(LOG_TAG, "Could not find voice search activity");
+ }
+ }
+
+ void onTextFocusChanged() {
+ updateViewsVisibility(isIconified());
+ // Delayed update to make sure that the focus has settled down and window focus changes
+ // don't affect it. A synchronous update was not working.
+ postUpdateFocusedState();
+ if (mSearchSrcTextView.hasFocus()) {
+ forceSuggestionQuery();
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ postUpdateFocusedState();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActionViewCollapsed() {
+ setQuery("", false);
+ clearFocus();
+ updateViewsVisibility(true);
+ mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
+ mExpandedInActionView = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActionViewExpanded() {
+ if (mExpandedInActionView) return;
+
+ mExpandedInActionView = true;
+ mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
+ mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
+ mSearchSrcTextView.setText("");
+ setIconified(false);
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean isIconified;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ isIconified = (Boolean) source.readValue(null);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeValue(isIconified);
+ }
+
+ @Override
+ public String toString() {
+ return "SearchView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " isIconified=" + isIconified + "}";
+ }
+
+ 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
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.isIconified = isIconified();
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ updateViewsVisibility(ss.isIconified);
+ requestLayout();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SearchView.class.getName();
+ }
+
+ private void adjustDropDownSizeAndPosition() {
+ if (mDropDownAnchor.getWidth() > 1) {
+ Resources res = getContext().getResources();
+ int anchorPadding = mSearchPlate.getPaddingLeft();
+ Rect dropDownPadding = new Rect();
+ final boolean isLayoutRtl = isLayoutRtl();
+ int iconOffset = mIconifiedByDefault
+ ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
+ + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
+ : 0;
+ mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
+ int offset;
+ if (isLayoutRtl) {
+ offset = - dropDownPadding.left;
+ } else {
+ offset = anchorPadding - (dropDownPadding.left + iconOffset);
+ }
+ mSearchSrcTextView.setDropDownHorizontalOffset(offset);
+ final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
+ + dropDownPadding.right + iconOffset - anchorPadding;
+ mSearchSrcTextView.setDropDownWidth(width);
+ }
+ }
+
+ private boolean onItemClicked(int position, int actionKey, String actionMsg) {
+ if (mOnSuggestionListener == null
+ || !mOnSuggestionListener.onSuggestionClick(position)) {
+ launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
+ mSearchSrcTextView.setImeVisibility(false);
+ dismissSuggestions();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onItemSelected(int position) {
+ if (mOnSuggestionListener == null
+ || !mOnSuggestionListener.onSuggestionSelect(position)) {
+ rewriteQueryFromSuggestion(position);
+ return true;
+ }
+ return false;
+ }
+
+ private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+
+ /**
+ * Implements OnItemClickListener
+ */
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
+ onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+ };
+
+ private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
+
+ /**
+ * Implements OnItemSelectedListener
+ */
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
+ SearchView.this.onItemSelected(position);
+ }
+
+ /**
+ * Implements OnItemSelectedListener
+ */
+ public void onNothingSelected(AdapterView<?> parent) {
+ if (DBG)
+ Log.d(LOG_TAG, "onNothingSelected()");
+ }
+ };
+
+ /**
+ * Query rewriting.
+ */
+ private void rewriteQueryFromSuggestion(int position) {
+ CharSequence oldQuery = mSearchSrcTextView.getText();
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if (c == null) {
+ return;
+ }
+ if (c.moveToPosition(position)) {
+ // Get the new query from the suggestion.
+ CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
+ if (newQuery != null) {
+ // The suggestion rewrites the query.
+ // Update the text field, without getting new suggestions.
+ setQuery(newQuery);
+ } else {
+ // The suggestion does not rewrite the query, restore the user's query.
+ setQuery(oldQuery);
+ }
+ } else {
+ // We got a bad position, restore the user's query.
+ setQuery(oldQuery);
+ }
+ }
+
+ /**
+ * Launches an intent based on a suggestion.
+ *
+ * @param position The index of the suggestion to create the intent from.
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or <code>null</code> if none.
+ * @return true if a successful launch, false if could not (e.g. bad position).
+ */
+ private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if ((c != null) && c.moveToPosition(position)) {
+
+ Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
+
+ // launch the intent
+ launchIntent(intent);
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Launches an intent, including any special intent handling.
+ */
+ private void launchIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ try {
+ // If the intent was created from a suggestion, it will always have an explicit
+ // component here.
+ getContext().startActivity(intent);
+ } catch (RuntimeException ex) {
+ Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
+ }
+ }
+
+ /**
+ * Sets the text in the query box, without updating the suggestions.
+ */
+ private void setQuery(CharSequence query) {
+ mSearchSrcTextView.setText(query, true);
+ // Move the cursor to the end
+ mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
+ }
+
+ private void launchQuerySearch(int actionKey, String actionMsg, String query) {
+ String action = Intent.ACTION_SEARCH;
+ Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
+ getContext().startActivity(intent);
+ }
+
+ /**
+ * Constructs an intent from the given information and the search dialog state.
+ *
+ * @param action Intent action.
+ * @param data Intent data, or <code>null</code>.
+ * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
+ * @param query Intent query, or <code>null</code>.
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or <code>null</code> if none.
+ * @param mode The search mode, one of the acceptable values for
+ * {@link SearchManager#SEARCH_MODE}, or {@code null}.
+ * @return The intent.
+ */
+ private Intent createIntent(String action, Uri data, String extraData, String query,
+ int actionKey, String actionMsg) {
+ // Now build the Intent
+ Intent intent = new Intent(action);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // We need CLEAR_TOP to avoid reusing an old task that has other activities
+ // on top of the one we want. We don't want to do this in in-app search though,
+ // as it can be destructive to the activity stack.
+ if (data != null) {
+ intent.setData(data);
+ }
+ intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
+ if (query != null) {
+ intent.putExtra(SearchManager.QUERY, query);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ if (mAppSearchData != null) {
+ intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+ }
+ if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+ intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+ intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ }
+ intent.setComponent(mSearchable.getSearchActivity());
+ return intent;
+ }
+
+ /**
+ * Create and return an Intent that can launch the voice search activity for web search.
+ */
+ private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
+ Intent voiceIntent = new Intent(baseIntent);
+ ComponentName searchActivity = searchable.getSearchActivity();
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
+ : searchActivity.flattenToShortString());
+ return voiceIntent;
+ }
+
+ /**
+ * Create and return an Intent that can launch the voice search activity, perform a specific
+ * voice transcription, and forward the results to the searchable activity.
+ *
+ * @param baseIntent The voice app search intent to start from
+ * @return A completely-configured intent ready to send to the voice search activity
+ */
+ private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
+ ComponentName searchActivity = searchable.getSearchActivity();
+
+ // create the necessary intent to set up a search-and-forward operation
+ // in the voice search system. We have to keep the bundle separate,
+ // because it becomes immutable once it enters the PendingIntent
+ Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
+ queryIntent.setComponent(searchActivity);
+ PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
+ PendingIntent.FLAG_ONE_SHOT);
+
+ // Now set up the bundle that will be inserted into the pending intent
+ // when it's time to do the search. We always build it here (even if empty)
+ // because the voice search activity will always need to insert "QUERY" into
+ // it anyway.
+ Bundle queryExtras = new Bundle();
+ if (mAppSearchData != null) {
+ queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
+ }
+
+ // Now build the intent to launch the voice search. Add all necessary
+ // extras to launch the voice recognizer, and then all the necessary extras
+ // to forward the results to the searchable activity
+ Intent voiceIntent = new Intent(baseIntent);
+
+ // Add all of the configuration options supplied by the searchable's metadata
+ String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
+ String prompt = null;
+ String language = null;
+ int maxResults = 1;
+
+ Resources resources = getResources();
+ if (searchable.getVoiceLanguageModeId() != 0) {
+ languageModel = resources.getString(searchable.getVoiceLanguageModeId());
+ }
+ if (searchable.getVoicePromptTextId() != 0) {
+ prompt = resources.getString(searchable.getVoicePromptTextId());
+ }
+ if (searchable.getVoiceLanguageId() != 0) {
+ language = resources.getString(searchable.getVoiceLanguageId());
+ }
+ if (searchable.getVoiceMaxResults() != 0) {
+ maxResults = searchable.getVoiceMaxResults();
+ }
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
+ : searchActivity.flattenToShortString());
+
+ // Add the values that configure forwarding the results
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
+
+ return voiceIntent;
+ }
+
+ /**
+ * When a particular suggestion has been selected, perform the various lookups required
+ * to use the suggestion. This includes checking the cursor for suggestion-specific data,
+ * and/or falling back to the XML for defaults; It also creates REST style Uri data when
+ * the suggestion includes a data id.
+ *
+ * @param c The suggestions cursor, moved to the row of the user's selection
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or <code>null</code> if none.
+ * @return An intent for the suggestion at the cursor's position.
+ */
+ private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
+ try {
+ // use specific action if supplied, or default action if supplied, or fixed default
+ String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+
+ if (action == null) {
+ action = mSearchable.getSuggestIntentAction();
+ }
+ if (action == null) {
+ action = Intent.ACTION_SEARCH;
+ }
+
+ // use specific data if supplied, or default data if supplied
+ String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+ if (data == null) {
+ data = mSearchable.getSuggestIntentData();
+ }
+ // then, if an ID was provided, append it.
+ if (data != null) {
+ String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+ if (id != null) {
+ data = data + "/" + Uri.encode(id);
+ }
+ }
+ Uri dataUri = (data == null) ? null : Uri.parse(data);
+
+ String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
+ String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+
+ return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
+ } catch (RuntimeException e ) {
+ int rowNum;
+ try { // be really paranoid now
+ rowNum = c.getPosition();
+ } catch (RuntimeException e2 ) {
+ rowNum = -1;
+ }
+ Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
+ " returned exception.", e);
+ return null;
+ }
+ }
+
+ private void forceSuggestionQuery() {
+ mSearchSrcTextView.doBeforeTextChanged();
+ mSearchSrcTextView.doAfterTextChanged();
+ }
+
+ static boolean isLandscapeMode(Context context) {
+ return context.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ /**
+ * Callback to watch the text field for empty/non-empty
+ */
+ private TextWatcher mTextWatcher = new TextWatcher() {
+
+ public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
+
+ public void onTextChanged(CharSequence s, int start,
+ int before, int after) {
+ SearchView.this.onTextChanged(s);
+ }
+
+ public void afterTextChanged(Editable s) {
+ }
+ };
+
+ private static class UpdatableTouchDelegate extends TouchDelegate {
+ /**
+ * View that should receive forwarded touch events
+ */
+ private final View mDelegateView;
+
+ /**
+ * Bounds in local coordinates of the containing view that should be mapped to the delegate
+ * view. This rect is used for initial hit testing.
+ */
+ private final Rect mTargetBounds;
+
+ /**
+ * Bounds in local coordinates of the containing view that are actual bounds of the delegate
+ * view. This rect is used for event coordinate mapping.
+ */
+ private final Rect mActualBounds;
+
+ /**
+ * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
+ * should be considered to be be within the delegate view.
+ */
+ private final Rect mSlopBounds;
+
+ private final int mSlop;
+
+ /**
+ * True if the delegate had been targeted on a down event (intersected mTargetBounds).
+ */
+ private boolean mDelegateTargeted;
+
+ public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
+ super(targetBounds, delegateView);
+ mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
+ mTargetBounds = new Rect();
+ mSlopBounds = new Rect();
+ mActualBounds = new Rect();
+ setBounds(targetBounds, actualBounds);
+ mDelegateView = delegateView;
+ }
+
+ public void setBounds(Rect desiredBounds, Rect actualBounds) {
+ mTargetBounds.set(desiredBounds);
+ mSlopBounds.set(desiredBounds);
+ mSlopBounds.inset(-mSlop, -mSlop);
+ mActualBounds.set(actualBounds);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ boolean sendToDelegate = false;
+ boolean hit = true;
+ boolean handled = false;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mTargetBounds.contains(x, y)) {
+ mDelegateTargeted = true;
+ sendToDelegate = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_MOVE:
+ sendToDelegate = mDelegateTargeted;
+ if (sendToDelegate) {
+ if (!mSlopBounds.contains(x, y)) {
+ hit = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ sendToDelegate = mDelegateTargeted;
+ mDelegateTargeted = false;
+ break;
+ }
+ if (sendToDelegate) {
+ if (hit && !mActualBounds.contains(x, y)) {
+ // Offset event coordinates to be in the center of the target view since we
+ // are within the targetBounds, but not inside the actual bounds of
+ // mDelegateView
+ event.setLocation(mDelegateView.getWidth() / 2,
+ mDelegateView.getHeight() / 2);
+ } else {
+ // Offset event coordinates to the target view coordinates.
+ event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
+ }
+
+ handled = mDelegateView.dispatchTouchEvent(event);
+ }
+ return handled;
+ }
+ }
+
+ /**
+ * Local subclass for AutoCompleteTextView.
+ * @hide
+ */
+ public static class SearchAutoComplete extends AutoCompleteTextView {
+
+ private int mThreshold;
+ private SearchView mSearchView;
+
+ private boolean mHasPendingShowSoftInputRequest;
+ final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
+
+ public SearchAutoComplete(Context context) {
+ super(context);
+ mThreshold = getThreshold();
+ }
+
+ public SearchAutoComplete(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mThreshold = getThreshold();
+ }
+
+ public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
+ super(context, attrs, defStyleAttrs);
+ mThreshold = getThreshold();
+ }
+
+ public SearchAutoComplete(
+ Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ super(context, attrs, defStyleAttrs, defStyleRes);
+ mThreshold = getThreshold();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ getSearchViewTextMinWidthDp(), metrics));
+ }
+
+ void setSearchView(SearchView searchView) {
+ mSearchView = searchView;
+ }
+
+ @Override
+ public void setThreshold(int threshold) {
+ super.setThreshold(threshold);
+ mThreshold = threshold;
+ }
+
+ /**
+ * Returns true if the text field is empty, or contains only whitespace.
+ */
+ private boolean isEmpty() {
+ return TextUtils.getTrimmedLength(getText()) == 0;
+ }
+
+ /**
+ * We override this method to avoid replacing the query box text when a
+ * suggestion is clicked.
+ */
+ @Override
+ protected void replaceText(CharSequence text) {
+ }
+
+ /**
+ * We override this method to avoid an extra onItemClick being called on
+ * the drop-down's OnItemClickListener by
+ * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
+ * clicked with the trackball.
+ */
+ @Override
+ public void performCompletion() {
+ }
+
+ /**
+ * We override this method to be sure and show the soft keyboard if
+ * appropriate when the TextView has focus.
+ */
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
+ // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
+ // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
+ // need to wait until the system calls back onCreateInputConnection() to call
+ // InputMethodManager#showSoftInput().
+ mHasPendingShowSoftInputRequest = true;
+
+ // If in landscape mode, then make sure that the ime is in front of the dropdown.
+ if (isLandscapeMode(getContext())) {
+ ensureImeVisible(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ mSearchView.onTextFocusChanged();
+ }
+
+ /**
+ * We override this method so that we can allow a threshold of zero,
+ * which ACTV does not.
+ */
+ @Override
+ public boolean enoughToFilter() {
+ return mThreshold <= 0 || super.enoughToFilter();
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ // 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 (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.handleUpEvent(event);
+ }
+ if (event.isTracking() && !event.isCanceled()) {
+ mSearchView.clearFocus();
+ setImeVisibility(false);
+ return true;
+ }
+ }
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ /**
+ * Get minimum width of the search view text entry area.
+ */
+ private int getSearchViewTextMinWidthDp() {
+ final Configuration configuration = getResources().getConfiguration();
+ final int width = configuration.screenWidthDp;
+ final int height = configuration.screenHeightDp;
+ final int orientation = configuration.orientation;
+ if (width >= 960 && height >= 720
+ && orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return 256;
+ } else if (width >= 600 || (width >= 640 && height >= 480)) {
+ return 192;
+ };
+ return 160;
+ }
+
+ /**
+ * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
+ * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
+ */
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+ final InputConnection ic = super.onCreateInputConnection(editorInfo);
+ if (mHasPendingShowSoftInputRequest) {
+ removeCallbacks(mRunShowSoftInputIfNecessary);
+ post(mRunShowSoftInputIfNecessary);
+ }
+ return ic;
+ }
+
+ private void showSoftInputIfNecessary() {
+ if (mHasPendingShowSoftInputRequest) {
+ final InputMethodManager imm =
+ getContext().getSystemService(InputMethodManager.class);
+ imm.showSoftInput(this, 0);
+ mHasPendingShowSoftInputRequest = false;
+ }
+ }
+
+ private void setImeVisibility(final boolean visible) {
+ final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+ if (!visible) {
+ mHasPendingShowSoftInputRequest = false;
+ removeCallbacks(mRunShowSoftInputIfNecessary);
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ return;
+ }
+
+ if (imm.isActive(this)) {
+ // This means that SearchAutoComplete is already connected to the IME.
+ // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
+ mHasPendingShowSoftInputRequest = false;
+ removeCallbacks(mRunShowSoftInputIfNecessary);
+ imm.showSoftInput(this, 0);
+ return;
+ }
+
+ // Otherwise, InputMethodManager#showSoftInput() should be deferred after
+ // onCreateInputConnection().
+ mHasPendingShowSoftInputRequest = true;
+ }
+ }
+}
diff --git a/android/widget/SectionIndexer.java b/android/widget/SectionIndexer.java
new file mode 100644
index 0000000..f6333d1
--- /dev/null
+++ b/android/widget/SectionIndexer.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+/**
+ * Interface that may implemented on {@link Adapter}s to enable fast scrolling
+ * between sections of an {@link AbsListView}.
+ * <p>
+ * A section is a group of list items that have something in common. For
+ * example, they may begin with the same letter or they may be songs from the
+ * same artist.
+ * <p>
+ * {@link ExpandableListAdapter}s that consider groups and sections as
+ * synonymous should account for collapsed groups and return an appropriate
+ * section/position.
+ *
+ * @see AbsListView#setFastScrollEnabled(boolean)
+ */
+public interface SectionIndexer {
+ /**
+ * Returns an array of objects representing sections of the list. The
+ * returned array and its contents should be non-null.
+ * <p>
+ * The list view will call toString() on the objects to get the preview text
+ * to display while scrolling. For example, an adapter may return an array
+ * of Strings representing letters of the alphabet. Or, it may return an
+ * array of objects whose toString() methods return their section titles.
+ *
+ * @return the array of section objects
+ */
+ Object[] getSections();
+
+ /**
+ * Given the index of a section within the array of section objects, returns
+ * the starting position of that section within the adapter.
+ * <p>
+ * If the section's starting position is outside of the adapter bounds, the
+ * position must be clipped to fall within the size of the adapter.
+ *
+ * @param sectionIndex the index of the section within the array of section
+ * objects
+ * @return the starting position of that section within the adapter,
+ * constrained to fall within the adapter bounds
+ */
+ int getPositionForSection(int sectionIndex);
+
+ /**
+ * Given a position within the adapter, returns the index of the
+ * corresponding section within the array of section objects.
+ * <p>
+ * If the section index is outside of the section array bounds, the index
+ * must be clipped to fall within the size of the section array.
+ * <p>
+ * For example, consider an indexer where the section at array index 0
+ * starts at adapter position 100. Calling this method with position 10,
+ * which is before the first section, must return index 0.
+ *
+ * @param position the position within the adapter for which to return the
+ * corresponding section index
+ * @return the index of the corresponding section within the array of
+ * section objects, constrained to fall within the array bounds
+ */
+ int getSectionForPosition(int position);
+}
diff --git a/android/widget/SeekBar.java b/android/widget/SeekBar.java
new file mode 100644
index 0000000..f9aced0
--- /dev/null
+++ b/android/widget/SeekBar.java
@@ -0,0 +1,141 @@
+/*
+ * 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.accessibility.AccessibilityNodeInfo;
+
+
+/**
+ * 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 or use the arrow keys.
+ * Placing focusable widgets to the left or right of a SeekBar is discouraged.
+ * <p>
+ * Clients of the SeekBar can attach a {@link SeekBar.OnSeekBarChangeListener} to
+ * be notified of the user's actions.
+ *
+ * @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 or arrow key/trackball as well as changes that were initiated
+ * programmatically.
+ */
+ public interface OnSeekBarChangeListener {
+
+ /**
+ * Notification that the progress level has changed. Clients can use the fromUser 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 min..max where min
+ * and max were set by {@link ProgressBar#setMin(int)} and
+ * {@link ProgressBar#setMax(int)}, respectively. (The default values for
+ * min is 0 and max is 100.)
+ * @param fromUser True if the progress change was initiated by the user.
+ */
+ void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser);
+
+ /**
+ * 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromUser, int progress) {
+ super.onProgressRefresh(scale, fromUser, progress);
+
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);
+ }
+ }
+
+ /**
+ * 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() {
+ super.onStartTrackingTouch();
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStartTrackingTouch(this);
+ }
+ }
+
+ @Override
+ void onStopTrackingTouch() {
+ super.onStopTrackingTouch();
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStopTrackingTouch(this);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SeekBar.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (canUserSetProgress()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
+ }
+ }
+}
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
new file mode 100644
index 0000000..36dc330
--- /dev/null
+++ b/android/widget/SelectionActionModeHelper.java
@@ -0,0 +1,857 @@
+/*
+ * Copyright (C) 2017 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.annotation.WorkerThread;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.AsyncTask;
+import android.os.LocaleList;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextSelection;
+import android.view.textclassifier.logging.SmartSelectionEventTracker;
+import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
+import android.widget.Editor.SelectionModifierCursorController;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for starting selection action mode
+ * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
+ * @hide
+ */
+@UiThread
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class SelectionActionModeHelper {
+
+ /**
+ * Maximum time (in milliseconds) to wait for a result before timing out.
+ */
+ // TODO: Consider making this a ViewConfiguration.
+ private static final int TIMEOUT_DURATION = 200;
+
+ private static final boolean SMART_SELECT_ANIMATION_ENABLED = true;
+
+ private final Editor mEditor;
+ private final TextView mTextView;
+ private final TextClassificationHelper mTextClassificationHelper;
+
+ private TextClassification mTextClassification;
+ private AsyncTask mTextClassificationAsyncTask;
+
+ private final SelectionTracker mSelectionTracker;
+
+ // TODO remove nullable marker once the switch gating the feature gets removed
+ @Nullable
+ private final SmartSelectSprite mSmartSelectSprite;
+
+ SelectionActionModeHelper(@NonNull Editor editor) {
+ mEditor = Preconditions.checkNotNull(editor);
+ mTextView = mEditor.getTextView();
+ mTextClassificationHelper = new TextClassificationHelper(
+ mTextView.getTextClassifier(), mTextView.getText(),
+ 0, 1, mTextView.getTextLocales());
+ mSelectionTracker = new SelectionTracker(mTextView);
+
+ if (SMART_SELECT_ANIMATION_ENABLED) {
+ mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
+ mTextView::invalidate);
+ } else {
+ mSmartSelectSprite = null;
+ }
+ }
+
+ public void startActionModeAsync(boolean adjustSelection) {
+ mSelectionTracker.onOriginalSelection(
+ mTextView.getText(),
+ mTextView.getSelectionStart(),
+ mTextView.getSelectionEnd(),
+ mTextView.isTextEditable());
+ cancelAsyncTask();
+ if (skipTextClassification()) {
+ startActionMode(null);
+ } else {
+ resetTextClassificationHelper();
+ mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+ mTextView,
+ TIMEOUT_DURATION,
+ adjustSelection
+ ? mTextClassificationHelper::suggestSelection
+ : mTextClassificationHelper::classifyText,
+ mSmartSelectSprite != null
+ ? this::startActionModeWithSmartSelectAnimation
+ : this::startActionMode)
+ .execute();
+ }
+ }
+
+ public void invalidateActionModeAsync() {
+ cancelAsyncTask();
+ if (skipTextClassification()) {
+ invalidateActionMode(null);
+ } else {
+ resetTextClassificationHelper();
+ mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+ mTextView,
+ TIMEOUT_DURATION,
+ mTextClassificationHelper::classifyText,
+ this::invalidateActionMode)
+ .execute();
+ }
+ }
+
+ public void onSelectionAction(int menuItemId) {
+ mSelectionTracker.onSelectionAction(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+ getActionType(menuItemId), mTextClassification);
+ }
+
+ public void onSelectionDrag() {
+ mSelectionTracker.onSelectionAction(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+ SelectionEvent.ActionType.DRAG, mTextClassification);
+ }
+
+ public void onTextChanged(int start, int end) {
+ mSelectionTracker.onTextChanged(start, end, mTextClassification);
+ }
+
+ public boolean resetSelection(int textIndex) {
+ if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
+ invalidateActionModeAsync();
+ return true;
+ }
+ return false;
+ }
+
+ @Nullable
+ public TextClassification getTextClassification() {
+ return mTextClassification;
+ }
+
+ public void onDestroyActionMode() {
+ cancelSmartSelectAnimation();
+ mSelectionTracker.onSelectionDestroyed();
+ cancelAsyncTask();
+ }
+
+ public void onDraw(final Canvas canvas) {
+ if (mSmartSelectSprite != null) {
+ mSmartSelectSprite.draw(canvas);
+ }
+ }
+
+ private void cancelAsyncTask() {
+ if (mTextClassificationAsyncTask != null) {
+ mTextClassificationAsyncTask.cancel(true);
+ mTextClassificationAsyncTask = null;
+ }
+ mTextClassification = null;
+ }
+
+ private boolean skipTextClassification() {
+ // No need to make an async call for a no-op TextClassifier.
+ final boolean noOpTextClassifier = mTextView.getTextClassifier() == TextClassifier.NO_OP;
+ // Do not call the TextClassifier if there is no selection.
+ final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
+ // Do not call the TextClassifier if this is a password field.
+ final boolean password = mTextView.hasPasswordTransformationMethod()
+ || TextView.isPasswordInputType(mTextView.getInputType());
+ return noOpTextClassifier || noSelection || password;
+ }
+
+ private void startActionMode(@Nullable SelectionResult result) {
+ final CharSequence text = mTextView.getText();
+ if (result != null && text instanceof Spannable) {
+ Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
+ mTextClassification = result.mClassification;
+ } else {
+ mTextClassification = null;
+ }
+ if (mEditor.startSelectionActionModeInternal()) {
+ final SelectionModifierCursorController controller = mEditor.getSelectionController();
+ if (controller != null) {
+ controller.show();
+ }
+ if (result != null) {
+ mSelectionTracker.onSmartSelection(result);
+ }
+ }
+ mEditor.setRestartActionModeOnNextRefresh(false);
+ mTextClassificationAsyncTask = null;
+ }
+
+ private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) {
+ final Layout layout = mTextView.getLayout();
+
+ final Runnable onAnimationEndCallback = () -> startActionMode(result);
+ // TODO do not trigger the animation if the change included only non-printable characters
+ final boolean didSelectionChange =
+ result != null && (mTextView.getSelectionStart() != result.mStart
+ || mTextView.getSelectionEnd() != result.mEnd);
+
+ if (!didSelectionChange) {
+ onAnimationEndCallback.run();
+ return;
+ }
+
+ final List<RectF> selectionRectangles =
+ convertSelectionToRectangles(layout, result.mStart, result.mEnd);
+
+ final PointF touchPoint = new PointF(
+ mEditor.getLastUpPositionX(),
+ mEditor.getLastUpPositionY());
+
+ final PointF animationStartPoint =
+ movePointInsideNearestRectangle(touchPoint, selectionRectangles);
+
+ mSmartSelectSprite.startAnimation(
+ animationStartPoint,
+ selectionRectangles,
+ onAnimationEndCallback);
+ }
+
+ private List<RectF> convertSelectionToRectangles(final Layout layout, final int start,
+ final int end) {
+ final List<RectF> result = new ArrayList<>();
+ layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
+ mergeRectangleIntoList(result, new RectF(left, top, right, bottom)));
+
+ result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR);
+ return result;
+ }
+
+ /**
+ * Merges a {@link RectF} into an existing list of rectangles. While merging, this method
+ * makes sure that:
+ *
+ * <ol>
+ * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
+ * <li>Rectangles of the same height and vertical position that intersect get merged</li>
+ * </ol>
+ *
+ * @param list the list of rectangles to merge the new rectangle in
+ * @param candidate the {@link RectF} to merge into the list
+ * @hide
+ */
+ @VisibleForTesting
+ public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) {
+ if (candidate.isEmpty()) {
+ return;
+ }
+
+ final int elementCount = list.size();
+ for (int index = 0; index < elementCount; ++index) {
+ final RectF existingRectangle = list.get(index);
+ if (existingRectangle.contains(candidate)) {
+ return;
+ }
+ if (candidate.contains(existingRectangle)) {
+ existingRectangle.setEmpty();
+ continue;
+ }
+
+ final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
+ || candidate.right == existingRectangle.left;
+ final boolean canMerge = candidate.top == existingRectangle.top
+ && candidate.bottom == existingRectangle.bottom
+ && (RectF.intersects(candidate, existingRectangle)
+ || rectanglesContinueEachOther);
+
+ if (canMerge) {
+ candidate.union(existingRectangle);
+ existingRectangle.setEmpty();
+ }
+ }
+
+ for (int index = elementCount - 1; index >= 0; --index) {
+ if (list.get(index).isEmpty()) {
+ list.remove(index);
+ }
+ }
+
+ list.add(candidate);
+ }
+
+
+ /** @hide */
+ @VisibleForTesting
+ public static PointF movePointInsideNearestRectangle(final PointF point,
+ final List<RectF> rectangles) {
+ float bestX = -1;
+ float bestY = -1;
+ double bestDistance = Double.MAX_VALUE;
+
+ final int elementCount = rectangles.size();
+ for (int index = 0; index < elementCount; ++index) {
+ final RectF rectangle = rectangles.get(index);
+ final float candidateY = rectangle.centerY();
+ final float candidateX;
+
+ if (point.x > rectangle.right) {
+ candidateX = rectangle.right;
+ } else if (point.x < rectangle.left) {
+ candidateX = rectangle.left;
+ } else {
+ candidateX = point.x;
+ }
+
+ final double candidateDistance = Math.pow(point.x - candidateX, 2)
+ + Math.pow(point.y - candidateY, 2);
+
+ if (candidateDistance < bestDistance) {
+ bestX = candidateX;
+ bestY = candidateY;
+ bestDistance = candidateDistance;
+ }
+ }
+
+ return new PointF(bestX, bestY);
+ }
+
+ private void invalidateActionMode(@Nullable SelectionResult result) {
+ cancelSmartSelectAnimation();
+ mTextClassification = result != null ? result.mClassification : null;
+ final ActionMode actionMode = mEditor.getTextActionMode();
+ if (actionMode != null) {
+ actionMode.invalidate();
+ }
+ mSelectionTracker.onSelectionUpdated(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
+ mTextClassificationAsyncTask = null;
+ }
+
+ private void resetTextClassificationHelper() {
+ mTextClassificationHelper.reset(mTextView.getTextClassifier(), mTextView.getText(),
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+ mTextView.getTextLocales());
+ }
+
+ private void cancelSmartSelectAnimation() {
+ if (mSmartSelectSprite != null) {
+ mSmartSelectSprite.cancelAnimation();
+ }
+ }
+
+ /**
+ * Tracks and logs smart selection changes.
+ * It is important to trigger this object's methods at the appropriate event so that it tracks
+ * smart selection events appropriately.
+ */
+ private static final class SelectionTracker {
+
+ private final TextView mTextView;
+ private SelectionMetricsLogger mLogger;
+
+ private int mOriginalStart;
+ private int mOriginalEnd;
+ private int mSelectionStart;
+ private int mSelectionEnd;
+ private boolean mAllowReset;
+
+ SelectionTracker(TextView textView) {
+ mTextView = Preconditions.checkNotNull(textView);
+ mLogger = new SelectionMetricsLogger(textView);
+ }
+
+ /**
+ * Called when the original selection happens, before smart selection is triggered.
+ */
+ public void onOriginalSelection(
+ CharSequence text, int selectionStart, int selectionEnd, boolean editableText) {
+ mOriginalStart = mSelectionStart = selectionStart;
+ mOriginalEnd = mSelectionEnd = selectionEnd;
+ mAllowReset = false;
+ maybeInvalidateLogger();
+ mLogger.logSelectionStarted(text, selectionStart);
+ }
+
+ /**
+ * Called when selection action mode is started and the results come from a classifier.
+ */
+ public void onSmartSelection(SelectionResult result) {
+ if (isSelectionStarted()) {
+ mSelectionStart = result.mStart;
+ mSelectionEnd = result.mEnd;
+ mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
+ mLogger.logSelectionModified(
+ result.mStart, result.mEnd, result.mClassification, result.mSelection);
+ }
+ }
+
+ /**
+ * Called when selection bounds change.
+ */
+ public void onSelectionUpdated(
+ int selectionStart, int selectionEnd,
+ @Nullable TextClassification classification) {
+ if (isSelectionStarted()) {
+ mSelectionStart = selectionStart;
+ mSelectionEnd = selectionEnd;
+ mAllowReset = false;
+ mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
+ }
+ }
+
+ /**
+ * Called when the selection action mode is destroyed.
+ */
+ public void onSelectionDestroyed() {
+ mAllowReset = false;
+ // Wait a few ms to see if the selection was destroyed because of a text change event.
+ mTextView.postDelayed(() -> {
+ mLogger.logSelectionAction(
+ mSelectionStart, mSelectionEnd,
+ SelectionEvent.ActionType.ABANDON, null /* classification */);
+ mSelectionStart = mSelectionEnd = -1;
+ }, 100 /* ms */);
+ }
+
+ /**
+ * Called when an action is taken on a smart selection.
+ */
+ public void onSelectionAction(
+ int selectionStart, int selectionEnd,
+ @SelectionEvent.ActionType int action,
+ @Nullable TextClassification classification) {
+ if (isSelectionStarted()) {
+ mAllowReset = false;
+ mLogger.logSelectionAction(selectionStart, selectionEnd, action, classification);
+ }
+ }
+
+ /**
+ * Returns true if the current smart selection should be reset to normal selection based on
+ * information that has been recorded about the original selection and the smart selection.
+ * The expected UX here is to allow the user to select a word inside of the smart selection
+ * on a single tap.
+ */
+ public boolean resetSelection(int textIndex, Editor editor) {
+ final TextView textView = editor.getTextView();
+ if (isSelectionStarted()
+ && mAllowReset
+ && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
+ && textView.getText() instanceof Spannable) {
+ mAllowReset = false;
+ boolean selected = editor.selectCurrentWord();
+ if (selected) {
+ mSelectionStart = editor.getTextView().getSelectionStart();
+ mSelectionEnd = editor.getTextView().getSelectionEnd();
+ mLogger.logSelectionAction(
+ textView.getSelectionStart(), textView.getSelectionEnd(),
+ SelectionEvent.ActionType.RESET, null /* classification */);
+ }
+ return selected;
+ }
+ return false;
+ }
+
+ public void onTextChanged(int start, int end, TextClassification classification) {
+ if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
+ onSelectionAction(start, end, SelectionEvent.ActionType.OVERTYPE, classification);
+ }
+ }
+
+ private void maybeInvalidateLogger() {
+ if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
+ mLogger = new SelectionMetricsLogger(mTextView);
+ }
+ }
+
+ private boolean isSelectionStarted() {
+ return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
+ }
+ }
+
+ // TODO: Write tests
+ /**
+ * Metrics logging helper.
+ *
+ * This logger logs selection by word indices. The initial (start) single word selection is
+ * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
+ * initial single word selection.
+ * e.g. New York city, NY. Suppose the initial selection is "York" in
+ * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
+ * "New York" is at [-1, 1).
+ * Part selection of a word e.g. "or" is counted as selecting the
+ * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
+ * "," is at [2, 3). Whitespaces are ignored.
+ */
+ private static final class SelectionMetricsLogger {
+
+ private static final String LOG_TAG = "SelectionMetricsLogger";
+ private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
+
+ private final SmartSelectionEventTracker mDelegate;
+ private final boolean mEditTextLogger;
+ private final BreakIterator mWordIterator;
+ private int mStartIndex;
+ private String mText;
+
+ SelectionMetricsLogger(TextView textView) {
+ Preconditions.checkNotNull(textView);
+ final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable()
+ ? SmartSelectionEventTracker.WidgetType.EDITTEXT
+ : SmartSelectionEventTracker.WidgetType.TEXTVIEW;
+ mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType);
+ mEditTextLogger = textView.isTextEditable();
+ mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale());
+ }
+
+ public void logSelectionStarted(CharSequence text, int index) {
+ try {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
+ if (mText == null || !mText.contentEquals(text)) {
+ mText = text.toString();
+ }
+ mWordIterator.setText(mText);
+ mStartIndex = index;
+ mDelegate.logEvent(SelectionEvent.selectionStarted(0));
+ } catch (Exception e) {
+ // Avoid crashes due to logging.
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ }
+
+ public void logSelectionModified(int start, int end,
+ @Nullable TextClassification classification, @Nullable TextSelection selection) {
+ try {
+ Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+ Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
+ int[] wordIndices = getWordDelta(start, end);
+ if (selection != null) {
+ mDelegate.logEvent(SelectionEvent.selectionModified(
+ wordIndices[0], wordIndices[1], selection));
+ } else if (classification != null) {
+ mDelegate.logEvent(SelectionEvent.selectionModified(
+ wordIndices[0], wordIndices[1], classification));
+ } else {
+ mDelegate.logEvent(SelectionEvent.selectionModified(
+ wordIndices[0], wordIndices[1]));
+ }
+ } catch (Exception e) {
+ // Avoid crashes due to logging.
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ }
+
+ public void logSelectionAction(
+ int start, int end,
+ @SelectionEvent.ActionType int action,
+ @Nullable TextClassification classification) {
+ try {
+ Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+ Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
+ int[] wordIndices = getWordDelta(start, end);
+ if (classification != null) {
+ mDelegate.logEvent(SelectionEvent.selectionAction(
+ wordIndices[0], wordIndices[1], action, classification));
+ } else {
+ mDelegate.logEvent(SelectionEvent.selectionAction(
+ wordIndices[0], wordIndices[1], action));
+ }
+ } catch (Exception e) {
+ // Avoid crashes due to logging.
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ }
+
+ public boolean isEditTextLogger() {
+ return mEditTextLogger;
+ }
+
+ private int[] getWordDelta(int start, int end) {
+ int[] wordIndices = new int[2];
+
+ if (start == mStartIndex) {
+ wordIndices[0] = 0;
+ } else if (start < mStartIndex) {
+ wordIndices[0] = -countWordsForward(start);
+ } else { // start > mStartIndex
+ wordIndices[0] = countWordsBackward(start);
+
+ // For the selection start index, avoid counting a partial word backwards.
+ if (!mWordIterator.isBoundary(start)
+ && !isWhitespace(
+ mWordIterator.preceding(start),
+ mWordIterator.following(start))) {
+ // We counted a partial word. Remove it.
+ wordIndices[0]--;
+ }
+ }
+
+ if (end == mStartIndex) {
+ wordIndices[1] = 0;
+ } else if (end < mStartIndex) {
+ wordIndices[1] = -countWordsForward(end);
+ } else { // end > mStartIndex
+ wordIndices[1] = countWordsBackward(end);
+ }
+
+ return wordIndices;
+ }
+
+ private int countWordsBackward(int from) {
+ Preconditions.checkArgument(from >= mStartIndex);
+ int wordCount = 0;
+ int offset = from;
+ while (offset > mStartIndex) {
+ int start = mWordIterator.preceding(offset);
+ if (!isWhitespace(start, offset)) {
+ wordCount++;
+ }
+ offset = start;
+ }
+ return wordCount;
+ }
+
+ private int countWordsForward(int from) {
+ Preconditions.checkArgument(from <= mStartIndex);
+ int wordCount = 0;
+ int offset = from;
+ while (offset < mStartIndex) {
+ int end = mWordIterator.following(offset);
+ if (!isWhitespace(offset, end)) {
+ wordCount++;
+ }
+ offset = end;
+ }
+ return wordCount;
+ }
+
+ private boolean isWhitespace(int start, int end) {
+ return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
+ }
+ }
+
+ /**
+ * AsyncTask for running a query on a background thread and returning the result on the
+ * UiThread. The AsyncTask times out after a specified time, returning a null result if the
+ * query has not yet returned.
+ */
+ private static final class TextClassificationAsyncTask
+ extends AsyncTask<Void, Void, SelectionResult> {
+
+ private final int mTimeOutDuration;
+ private final Supplier<SelectionResult> mSelectionResultSupplier;
+ private final Consumer<SelectionResult> mSelectionResultCallback;
+ private final TextView mTextView;
+ private final String mOriginalText;
+
+ /**
+ * @param textView the TextView
+ * @param timeOut time in milliseconds to timeout the query if it has not completed
+ * @param selectionResultSupplier fetches the selection results. Runs on a background thread
+ * @param selectionResultCallback receives the selection results. Runs on the UiThread
+ */
+ TextClassificationAsyncTask(
+ @NonNull TextView textView, int timeOut,
+ @NonNull Supplier<SelectionResult> selectionResultSupplier,
+ @NonNull Consumer<SelectionResult> selectionResultCallback) {
+ super(textView != null ? textView.getHandler() : null);
+ mTextView = Preconditions.checkNotNull(textView);
+ mTimeOutDuration = timeOut;
+ mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
+ mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
+ // Make a copy of the original text.
+ mOriginalText = mTextView.getText().toString();
+ }
+
+ @Override
+ @WorkerThread
+ protected SelectionResult doInBackground(Void... params) {
+ final Runnable onTimeOut = this::onTimeOut;
+ mTextView.postDelayed(onTimeOut, mTimeOutDuration);
+ final SelectionResult result = mSelectionResultSupplier.get();
+ mTextView.removeCallbacks(onTimeOut);
+ return result;
+ }
+
+ @Override
+ @UiThread
+ protected void onPostExecute(SelectionResult result) {
+ result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null;
+ mSelectionResultCallback.accept(result);
+ }
+
+ private void onTimeOut() {
+ if (getStatus() == Status.RUNNING) {
+ onPostExecute(null);
+ }
+ cancel(true);
+ }
+ }
+
+ /**
+ * Helper class for querying the TextClassifier.
+ * It trims text so that only text necessary to provide context of the selected text is
+ * sent to the TextClassifier.
+ */
+ private static final class TextClassificationHelper {
+
+ private static final int TRIM_DELTA = 120; // characters
+
+ private TextClassifier mTextClassifier;
+
+ /** The original TextView text. **/
+ private String mText;
+ /** Start index relative to mText. */
+ private int mSelectionStart;
+ /** End index relative to mText. */
+ private int mSelectionEnd;
+ private LocaleList mLocales;
+
+ /** Trimmed text starting from mTrimStart in mText. */
+ private CharSequence mTrimmedText;
+ /** Index indicating the start of mTrimmedText in mText. */
+ private int mTrimStart;
+ /** Start index relative to mTrimmedText */
+ private int mRelativeStart;
+ /** End index relative to mTrimmedText */
+ private int mRelativeEnd;
+
+ /** Information about the last classified text to avoid re-running a query. */
+ private CharSequence mLastClassificationText;
+ private int mLastClassificationSelectionStart;
+ private int mLastClassificationSelectionEnd;
+ private LocaleList mLastClassificationLocales;
+ private SelectionResult mLastClassificationResult;
+
+ TextClassificationHelper(TextClassifier textClassifier,
+ CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
+ reset(textClassifier, text, selectionStart, selectionEnd, locales);
+ }
+
+ @UiThread
+ public void reset(TextClassifier textClassifier,
+ CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
+ mTextClassifier = Preconditions.checkNotNull(textClassifier);
+ mText = Preconditions.checkNotNull(text).toString();
+ mLastClassificationText = null; // invalidate.
+ Preconditions.checkArgument(selectionEnd > selectionStart);
+ mSelectionStart = selectionStart;
+ mSelectionEnd = selectionEnd;
+ mLocales = locales;
+ }
+
+ @WorkerThread
+ public SelectionResult classifyText() {
+ return performClassification(null /* selection */);
+ }
+
+ @WorkerThread
+ public SelectionResult suggestSelection() {
+ trimText();
+ final TextSelection selection = mTextClassifier.suggestSelection(
+ mTrimmedText, mRelativeStart, mRelativeEnd, mLocales);
+ mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart);
+ mSelectionEnd = Math.min(mText.length(), selection.getSelectionEndIndex() + mTrimStart);
+ return performClassification(selection);
+ }
+
+ private SelectionResult performClassification(@Nullable TextSelection selection) {
+ if (!Objects.equals(mText, mLastClassificationText)
+ || mSelectionStart != mLastClassificationSelectionStart
+ || mSelectionEnd != mLastClassificationSelectionEnd
+ || !Objects.equals(mLocales, mLastClassificationLocales)) {
+
+ mLastClassificationText = mText;
+ mLastClassificationSelectionStart = mSelectionStart;
+ mLastClassificationSelectionEnd = mSelectionEnd;
+ mLastClassificationLocales = mLocales;
+
+ trimText();
+ mLastClassificationResult = new SelectionResult(
+ mSelectionStart,
+ mSelectionEnd,
+ mTextClassifier.classifyText(
+ mTrimmedText, mRelativeStart, mRelativeEnd, mLocales),
+ selection);
+
+ }
+ return mLastClassificationResult;
+ }
+
+ private void trimText() {
+ mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
+ final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
+ mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
+ mRelativeStart = mSelectionStart - mTrimStart;
+ mRelativeEnd = mSelectionEnd - mTrimStart;
+ }
+ }
+
+ /**
+ * Selection result.
+ */
+ private static final class SelectionResult {
+ private final int mStart;
+ private final int mEnd;
+ private final TextClassification mClassification;
+ @Nullable private final TextSelection mSelection;
+
+ SelectionResult(int start, int end,
+ TextClassification classification, @Nullable TextSelection selection) {
+ mStart = start;
+ mEnd = end;
+ mClassification = Preconditions.checkNotNull(classification);
+ mSelection = selection;
+ }
+ }
+
+ @SelectionEvent.ActionType
+ private static int getActionType(int menuItemId) {
+ switch (menuItemId) {
+ case TextView.ID_SELECT_ALL:
+ return SelectionEvent.ActionType.SELECT_ALL;
+ case TextView.ID_CUT:
+ return SelectionEvent.ActionType.CUT;
+ case TextView.ID_COPY:
+ return SelectionEvent.ActionType.COPY;
+ case TextView.ID_PASTE: // fall through
+ case TextView.ID_PASTE_AS_PLAIN_TEXT:
+ return SelectionEvent.ActionType.PASTE;
+ case TextView.ID_SHARE:
+ return SelectionEvent.ActionType.SHARE;
+ case TextView.ID_ASSIST:
+ return SelectionEvent.ActionType.SMART_SHARE;
+ default:
+ return SelectionEvent.ActionType.OTHER;
+ }
+ }
+}
diff --git a/android/widget/ShareActionProvider.java b/android/widget/ShareActionProvider.java
new file mode 100644
index 0000000..9a24061
--- /dev/null
+++ b/android/widget/ShareActionProvider.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2011 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.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.ActionProvider;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.SubMenu;
+import android.view.View;
+import android.widget.ActivityChooserModel.OnChooseActivityListener;
+
+import com.android.internal.R;
+
+/**
+ * This is a provider for a share action. It is responsible for creating views
+ * that enable data sharing and also to show a sub menu with sharing activities
+ * if the hosting item is placed on the overflow menu.
+ * <p>
+ * Here is how to use the action provider with custom backing file in a {@link MenuItem}:
+ * </p>
+ * <pre>
+ * // In Activity#onCreateOptionsMenu
+ * public boolean onCreateOptionsMenu(Menu menu) {
+ * // Get the menu item.
+ * MenuItem menuItem = menu.findItem(R.id.my_menu_item);
+ * // Get the provider and hold onto it to set/change the share intent.
+ * mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
+ * // Set history different from the default before getting the action
+ * // view since a call to {@link MenuItem#getActionView() MenuItem.getActionView()} calls
+ * // {@link ActionProvider#onCreateActionView()} which uses the backing file name. Omit this
+ * // line if using the default share history file is desired.
+ * mShareActionProvider.setShareHistoryFileName("custom_share_history.xml");
+ * . . .
+ * }
+ *
+ * // Somewhere in the application.
+ * public void doShare(Intent shareIntent) {
+ * // When you want to share set the share intent.
+ * mShareActionProvider.setShareIntent(shareIntent);
+ * }</pre>
+ * <p>
+ * <strong>Note:</strong> While the sample snippet demonstrates how to use this provider
+ * in the context of a menu item, the use of the provider is not limited to menu items.
+ * </p>
+ *
+ * @see ActionProvider
+ */
+public class ShareActionProvider extends ActionProvider {
+
+ /**
+ * Listener for the event of selecting a share target.
+ */
+ public interface OnShareTargetSelectedListener {
+
+ /**
+ * Called when a share target has been selected. The client can
+ * decide whether to perform some action before the sharing is
+ * actually performed.
+ * <p>
+ * <strong>Note:</strong> Modifying the intent is not permitted and
+ * any changes to the latter will be ignored.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> You should <strong>not</strong> handle the
+ * intent here. This callback aims to notify the client that a
+ * sharing is being performed, so the client can update the UI
+ * if necessary.
+ * </p>
+ *
+ * @param source The source of the notification.
+ * @param intent The intent for launching the chosen share target.
+ * @return The return result is ignored. Always return false for consistency.
+ */
+ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent);
+ }
+
+ /**
+ * The default for the maximal number of activities shown in the sub-menu.
+ */
+ private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4;
+
+ /**
+ * The the maximum number activities shown in the sub-menu.
+ */
+ private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT;
+
+ /**
+ * Listener for handling menu item clicks.
+ */
+ private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener =
+ new ShareMenuItemOnMenuItemClickListener();
+
+ /**
+ * The default name for storing share history.
+ */
+ public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml";
+
+ /**
+ * Context for accessing resources.
+ */
+ private final Context mContext;
+
+ /**
+ * The name of the file with share history data.
+ */
+ private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME;
+
+ private OnShareTargetSelectedListener mOnShareTargetSelectedListener;
+
+ private OnChooseActivityListener mOnChooseActivityListener;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context Context for accessing resources.
+ */
+ public ShareActionProvider(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /**
+ * Sets a listener to be notified when a share target has been selected.
+ * The listener can optionally decide to handle the selection and
+ * not rely on the default behavior which is to launch the activity.
+ * <p>
+ * <strong>Note:</strong> If you choose the backing share history file
+ * you will still be notified in this callback.
+ * </p>
+ * @param listener The listener.
+ */
+ public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) {
+ mOnShareTargetSelectedListener = listener;
+ setActivityChooserPolicyIfNeeded();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateActionView() {
+ // Create the view and set its data model.
+ ActivityChooserView activityChooserView = new ActivityChooserView(mContext);
+ if (!activityChooserView.isInEditMode()) {
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+ activityChooserView.setActivityChooserModel(dataModel);
+ }
+
+ // Lookup and set the expand action icon.
+ TypedValue outTypedValue = new TypedValue();
+ mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true);
+ Drawable drawable = mContext.getDrawable(outTypedValue.resourceId);
+ activityChooserView.setExpandActivityOverflowButtonDrawable(drawable);
+ activityChooserView.setProvider(this);
+
+ // Set content description.
+ activityChooserView.setDefaultActionButtonContentDescription(
+ R.string.shareactionprovider_share_with_application);
+ activityChooserView.setExpandActivityOverflowButtonContentDescription(
+ R.string.shareactionprovider_share_with);
+
+ return activityChooserView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasSubMenu() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onPrepareSubMenu(SubMenu subMenu) {
+ // Clear since the order of items may change.
+ subMenu.clear();
+
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+ PackageManager packageManager = mContext.getPackageManager();
+
+ final int expandedActivityCount = dataModel.getActivityCount();
+ final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount);
+
+ // Populate the sub-menu with a sub set of the activities.
+ for (int i = 0; i < collapsedActivityCount; i++) {
+ ResolveInfo activity = dataModel.getActivity(i);
+ subMenu.add(0, i, i, activity.loadLabel(packageManager))
+ .setIcon(activity.loadIcon(packageManager))
+ .setOnMenuItemClickListener(mOnMenuItemClickListener);
+ }
+
+ if (collapsedActivityCount < expandedActivityCount) {
+ // Add a sub-menu for showing all activities as a list item.
+ SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount,
+ collapsedActivityCount,
+ mContext.getString(R.string.activity_chooser_view_see_all));
+ for (int i = 0; i < expandedActivityCount; i++) {
+ ResolveInfo activity = dataModel.getActivity(i);
+ expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager))
+ .setIcon(activity.loadIcon(packageManager))
+ .setOnMenuItemClickListener(mOnMenuItemClickListener);
+ }
+ }
+ }
+
+ /**
+ * Sets the file name of a file for persisting the share history which
+ * history will be used for ordering share targets. This file will be used
+ * for all view created by {@link #onCreateActionView()}. Defaults to
+ * {@link #DEFAULT_SHARE_HISTORY_FILE_NAME}. Set to <code>null</code>
+ * if share history should not be persisted between sessions.
+ * <p>
+ * <strong>Note:</strong> The history file name can be set any time, however
+ * only the action views created by {@link #onCreateActionView()} after setting
+ * the file name will be backed by the provided file. Therefore, if you want to
+ * use different history files for sharing specific types of content, every time
+ * you change the history file {@link #setShareHistoryFileName(String)} you must
+ * call {@link android.app.Activity#invalidateOptionsMenu()} to recreate the
+ * action view. You should <strong>not</strong> call
+ * {@link android.app.Activity#invalidateOptionsMenu()} from
+ * {@link android.app.Activity#onCreateOptionsMenu(Menu)}.
+ * </p>
+ * <pre>
+ * private void doShare(Intent intent) {
+ * if (IMAGE.equals(intent.getMimeType())) {
+ * mShareActionProvider.setHistoryFileName(SHARE_IMAGE_HISTORY_FILE_NAME);
+ * } else if (TEXT.equals(intent.getMimeType())) {
+ * mShareActionProvider.setHistoryFileName(SHARE_TEXT_HISTORY_FILE_NAME);
+ * }
+ * mShareActionProvider.setIntent(intent);
+ * invalidateOptionsMenu();
+ * }</pre>
+ * @param shareHistoryFile The share history file name.
+ */
+ public void setShareHistoryFileName(String shareHistoryFile) {
+ mShareHistoryFileName = shareHistoryFile;
+ setActivityChooserPolicyIfNeeded();
+ }
+
+ /**
+ * Sets an intent with information about the share action. Here is a
+ * sample for constructing a share intent:
+ * <pre>
+ * Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ * shareIntent.setType("image/*");
+ * Uri uri = Uri.fromFile(new File(getFilesDir(), "foo.jpg"));
+ * shareIntent.putExtra(Intent.EXTRA_STREAM, uri));</pre>
+ *
+ * @param shareIntent The share intent.
+ *
+ * @see Intent#ACTION_SEND
+ * @see Intent#ACTION_SEND_MULTIPLE
+ */
+ public void setShareIntent(Intent shareIntent) {
+ if (shareIntent != null) {
+ final String action = shareIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ }
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
+ mShareHistoryFileName);
+ dataModel.setIntent(shareIntent);
+ }
+
+ /**
+ * Reusable listener for handling share item clicks.
+ */
+ private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
+ mShareHistoryFileName);
+ final int itemId = item.getItemId();
+ Intent launchIntent = dataModel.chooseActivity(itemId);
+ if (launchIntent != null) {
+ final String action = launchIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action) ||
+ Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mContext.startActivity(launchIntent);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Set the activity chooser policy of the model backed by the current
+ * share history file if needed which is if there is a registered callback.
+ */
+ private void setActivityChooserPolicyIfNeeded() {
+ if (mOnShareTargetSelectedListener == null) {
+ return;
+ }
+ if (mOnChooseActivityListener == null) {
+ mOnChooseActivityListener = new ShareActivityChooserModelPolicy();
+ }
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+ dataModel.setOnChooseActivityListener(mOnChooseActivityListener);
+ }
+
+ /**
+ * Policy that delegates to the {@link OnShareTargetSelectedListener}, if such.
+ */
+ private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
+ @Override
+ public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
+ if (mOnShareTargetSelectedListener != null) {
+ mOnShareTargetSelectedListener.onShareTargetSelected(
+ ShareActionProvider.this, intent);
+ }
+ return false;
+ }
+ }
+}
diff --git a/android/widget/SimpleAdapter.java b/android/widget/SimpleAdapter.java
new file mode 100644
index 0000000..9190117
--- /dev/null
+++ b/android/widget/SimpleAdapter.java
@@ -0,0 +1,423 @@
+/*
+ * 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.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+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 occurred.
+ * If the returned value is false, the following views are then tried in order:
+ * <ul>
+ * <li> A view that implements Checkable (e.g. CheckBox). The expected bind value is a boolean.
+ * <li> TextView. The expected bind value is a string and {@link #setViewText(TextView, String)}
+ * is invoked.
+ * <li> ImageView. The expected bind value is a resource id or a string and
+ * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked.
+ * </ul>
+ * If no appropriate binding can be found, an {@link IllegalStateException} is thrown.
+ */
+public class SimpleAdapter extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
+ private final LayoutInflater mInflater;
+
+ private int[] mTo;
+ private String[] mFrom;
+ private ViewBinder mViewBinder;
+
+ private List<? extends Map<String, ?>> mData;
+
+ private int mResource;
+ private int mDropDownResource;
+
+ /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
+ private LayoutInflater mDropDownInflater;
+
+ 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,
+ @LayoutRes int resource, String[] from, @IdRes 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(mInflater, position, convertView, parent, mResource);
+ }
+
+ private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
+ ViewGroup parent, int resource) {
+ View v;
+ if (convertView == null) {
+ v = inflater.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) {
+ mDropDownResource = resource;
+ }
+
+ /**
+ * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are
+ * inflated.
+ * <p>
+ * By default, drop-down views are inflated against the theme of the
+ * {@link Context} passed to the adapter's constructor.
+ *
+ * @param theme the theme against which to inflate drop-down views or
+ * {@code null} to use the theme from the adapter's context
+ * @see #getDropDownView(int, View, ViewGroup)
+ */
+ @Override
+ public void setDropDownViewTheme(Resources.Theme theme) {
+ if (theme == null) {
+ mDropDownInflater = null;
+ } else if (theme == mInflater.getContext().getTheme()) {
+ mDropDownInflater = mInflater;
+ } else {
+ final Context context = new ContextThemeWrapper(mInflater.getContext(), theme);
+ mDropDownInflater = LayoutInflater.from(context);
+ }
+ }
+
+ @Override
+ public Resources.Theme getDropDownViewTheme() {
+ return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
+ return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
+ }
+
+ private void bindView(int position, View view) {
+ final Map dataSet = mData.get(position);
+ if (dataSet == null) {
+ return;
+ }
+
+ final ViewBinder binder = mViewBinder;
+ final String[] from = mFrom;
+ final int[] to = mTo;
+ final int count = to.length;
+
+ for (int i = 0; i < count; 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 (binder != null) {
+ bound = binder.setViewValue(v, data, text);
+ }
+
+ if (!bound) {
+ if (v instanceof Checkable) {
+ if (data instanceof Boolean) {
+ ((Checkable) v).setChecked((Boolean) data);
+ } else if (v instanceof TextView) {
+ // Note: keep the instanceof TextView check at the bottom of these
+ // ifs since a lot of views are TextViews (e.g. CheckBoxes).
+ setViewText((TextView) v, text);
+ } else {
+ throw new IllegalStateException(v.getClass().getName() +
+ " should be bound to a Boolean, not a " +
+ (data == null ? "<unknown type>" : data.getClass()));
+ }
+ } else if (v instanceof TextView) {
+ // Note: keep the instanceof TextView check at the bottom of these
+ // ifs since a lot of views are TextViews (e.g. CheckBoxes).
+ 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 a 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/android/widget/SimpleCursorAdapter.java b/android/widget/SimpleCursorAdapter.java
new file mode 100644
index 0000000..3dd0a95
--- /dev/null
+++ b/android/widget/SimpleCursorAdapter.java
@@ -0,0 +1,418 @@
+/*
+ * 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;
+
+ String[] mOriginalFrom;
+
+ /**
+ * Constructor the enables auto-requery.
+ *
+ * @deprecated This option is discouraged, as it results in Cursor queries
+ * being performed on the application's UI thread and thus can cause poor
+ * responsiveness or even Application Not Responding errors. As an alternative,
+ * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+ */
+ @Deprecated
+ public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
+ super(context, layout, c);
+ mTo = to;
+ mOriginalFrom = from;
+ findColumns(c, from);
+ }
+
+ /**
+ * Standard 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. The layout 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. Can be null
+ * if the cursor is not available yet.
+ * @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. Can be null if the cursor is not available yet.
+ * @param flags Flags used to determine the behavior of the adapter,
+ * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+ */
+ public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from,
+ int[] to, int flags) {
+ super(context, layout, c, flags);
+ mTo = to;
+ mOriginalFrom = from;
+ findColumns(c, 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) {
+ final ViewBinder binder = mViewBinder;
+ final int count = mTo.length;
+ final int[] from = mFrom;
+ final int[] to = mTo;
+
+ for (int i = 0; i < count; i++) {
+ final View v = view.findViewById(to[i]);
+ if (v != null) {
+ boolean bound = false;
+ if (binder != null) {
+ bound = binder.setViewValue(v, cursor, from[i]);
+ }
+
+ if (!bound) {
+ String text = cursor.getString(from[i]);
+ if (text == null) {
+ text = "";
+ }
+
+ 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 a 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);
+ }
+
+ /**
+ * Create a map from an array of strings to an array of column-id integers in cursor c.
+ * If c is null, the array will be discarded.
+ *
+ * @param c the cursor to find the columns from
+ * @param from the Strings naming the columns of interest
+ */
+ private void findColumns(Cursor c, String[] from) {
+ if (c != null) {
+ int i;
+ int count = from.length;
+ if (mFrom == null || mFrom.length != count) {
+ mFrom = new int[count];
+ }
+ for (i = 0; i < count; i++) {
+ mFrom[i] = c.getColumnIndexOrThrow(from[i]);
+ }
+ } else {
+ mFrom = null;
+ }
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor c) {
+ // super.swapCursor() will notify observers before we have
+ // a valid mapping, make sure we have a mapping before this
+ // happens
+ findColumns(c, mOriginalFrom);
+ return super.swapCursor(c);
+ }
+
+ /**
+ * Change the cursor and change the column-to-view mappings at the same time.
+ *
+ * @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. Can be null
+ * if the cursor is not available yet.
+ * @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. Can be null if the cursor is not available yet.
+ */
+ public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
+ mOriginalFrom = from;
+ mTo = to;
+ // super.changeCursor() will notify observers before we have
+ // a valid mapping, make sure we have a mapping before this
+ // happens
+ findColumns(c, mOriginalFrom);
+ super.changeCursor(c);
+ }
+
+ /**
+ * 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/android/widget/SimpleCursorTreeAdapter.java b/android/widget/SimpleCursorTreeAdapter.java
new file mode 100644
index 0000000..6babf3e
--- /dev/null
+++ b/android/widget/SimpleCursorTreeAdapter.java
@@ -0,0 +1,326 @@
+/*
+ * 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.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorTreeAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occurred. 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.
+ */
+public abstract class SimpleCursorTreeAdapter extends ResourceCursorTreeAdapter {
+
+ /** The name of the columns that contain the data to display for a group. */
+ private String[] mGroupFromNames;
+
+ /** 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 name of the columns that contain the data to display for a child. */
+ private String[] mChildFromNames;
+
+ /** 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;
+
+ /**
+ * View binder, if supplied
+ */
+ private ViewBinder mViewBinder;
+
+ /**
+ * 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) {
+
+ mGroupFromNames = groupFromNames;
+ mGroupTo = groupTo;
+
+ mChildFromNames = childFromNames;
+ mChildTo = childTo;
+ }
+
+ /**
+ * 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.SimpleCursorTreeAdapter.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;
+ }
+
+ private void bindView(View view, Context context, Cursor cursor, int[] from, int[] to) {
+ final ViewBinder binder = mViewBinder;
+
+ for (int i = 0; i < to.length; i++) {
+ View v = view.findViewById(to[i]);
+ if (v != null) {
+ boolean bound = false;
+ if (binder != null) {
+ bound = binder.setViewValue(v, cursor, from[i]);
+ }
+
+ if (!bound) {
+ String text = cursor.getString(from[i]);
+ if (text == null) {
+ text = "";
+ }
+ if (v instanceof TextView) {
+ setViewText((TextView) v, text);
+ } else if (v instanceof ImageView) {
+ setViewImage((ImageView) v, text);
+ } else {
+ throw new IllegalStateException("SimpleCursorTreeAdapter can bind values" +
+ " only to TextView and ImageView!");
+ }
+ }
+ }
+ }
+ }
+
+ private void initFromColumns(Cursor cursor, String[] fromColumnNames, int[] fromColumns) {
+ for (int i = fromColumnNames.length - 1; i >= 0; i--) {
+ fromColumns[i] = cursor.getColumnIndexOrThrow(fromColumnNames[i]);
+ }
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+ if (mChildFrom == null) {
+ mChildFrom = new int[mChildFromNames.length];
+ initFromColumns(cursor, mChildFromNames, mChildFrom);
+ }
+
+ bindView(view, context, cursor, mChildFrom, mChildTo);
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+ if (mGroupFrom == null) {
+ mGroupFrom = new int[mGroupFromNames.length];
+ initFromColumns(cursor, mGroupFromNames, mGroupFrom);
+ }
+
+ 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));
+ }
+ }
+
+ /**
+ * 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 a 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);
+ }
+
+ /**
+ * This class can be used by external clients of SimpleCursorTreeAdapter
+ * to bind values from the Cursor to views.
+ *
+ * You should use this class to bind values from the Cursor to views
+ * that are not directly supported by SimpleCursorTreeAdapter or to
+ * change the way binding occurs for views supported by
+ * SimpleCursorTreeAdapter.
+ *
+ * @see SimpleCursorTreeAdapter#setViewImage(ImageView, String)
+ * @see SimpleCursorTreeAdapter#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, SimpleCursorTreeAdapter 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);
+ }
+}
diff --git a/android/widget/SimpleExpandableListAdapter.java b/android/widget/SimpleExpandableListAdapter.java
new file mode 100644
index 0000000..597502b
--- /dev/null
+++ b/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.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+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/android/widget/SimpleMonthView.java b/android/widget/SimpleMonthView.java
new file mode 100644
index 0000000..9982732
--- /dev/null
+++ b/android/widget/SimpleMonthView.java
@@ -0,0 +1,1161 @@
+/*
+ * Copyright (C) 2014 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.Nullable;
+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.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.icu.text.DisplayContext;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.os.Bundle;
+import android.text.TextPaint;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.MathUtils;
+import android.util.StateSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import com.android.internal.R;
+import com.android.internal.widget.ExploreByTouchHelper;
+
+import libcore.icu.LocaleData;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * A calendar-like view displaying a specified month and the appropriate selectable day numbers
+ * within the specified month.
+ */
+class SimpleMonthView extends View {
+ private static final int DAYS_IN_WEEK = 7;
+ private static final int MAX_WEEKS_IN_MONTH = 6;
+
+ private static final int DEFAULT_SELECTED_DAY = -1;
+ private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
+
+ private static final String MONTH_YEAR_FORMAT = "MMMMy";
+
+ private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
+
+ private final TextPaint mMonthPaint = new TextPaint();
+ private final TextPaint mDayOfWeekPaint = new TextPaint();
+ private final TextPaint mDayPaint = new TextPaint();
+ private final Paint mDaySelectorPaint = new Paint();
+ private final Paint mDayHighlightPaint = new Paint();
+ private final Paint mDayHighlightSelectorPaint = new Paint();
+
+ /** Array of single-character weekday labels ordered by column index. */
+ private final String[] mDayOfWeekLabels = new String[7];
+
+ private final Calendar mCalendar;
+ private final Locale mLocale;
+
+ private final MonthViewTouchHelper mTouchHelper;
+
+ private final NumberFormat mDayFormatter;
+
+ // Desired dimensions.
+ private final int mDesiredMonthHeight;
+ private final int mDesiredDayOfWeekHeight;
+ private final int mDesiredDayHeight;
+ private final int mDesiredCellWidth;
+ private final int mDesiredDaySelectorRadius;
+
+ private String mMonthYearLabel;
+
+ private int mMonth;
+ private int mYear;
+
+ // Dimensions as laid out.
+ private int mMonthHeight;
+ private int mDayOfWeekHeight;
+ private int mDayHeight;
+ private int mCellWidth;
+ private int mDaySelectorRadius;
+
+ private int mPaddedWidth;
+ private int mPaddedHeight;
+
+ /** The day of month for the selected day, or -1 if no day is selected. */
+ private int mActivatedDay = -1;
+
+ /**
+ * The day of month for today, or -1 if the today is not in the current
+ * month.
+ */
+ private int mToday = DEFAULT_SELECTED_DAY;
+
+ /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
+ private int mWeekStart = DEFAULT_WEEK_START;
+
+ /** The number of days (ex. 28) in the current month. */
+ private int mDaysInMonth;
+
+ /**
+ * The day of week (ex. Calendar.SUNDAY) for the first day of the current
+ * month.
+ */
+ private int mDayOfWeekStart;
+
+ /** The day of month for the first (inclusive) enabled day. */
+ private int mEnabledDayStart = 1;
+
+ /** The day of month for the last (inclusive) enabled day. */
+ private int mEnabledDayEnd = 31;
+
+ /** Optional listener for handling day click actions. */
+ private OnDayClickListener mOnDayClickListener;
+
+ private ColorStateList mDayTextColor;
+
+ private int mHighlightedDay = -1;
+ private int mPreviouslyHighlightedDay = -1;
+ private boolean mIsTouchHighlighted = false;
+
+ public SimpleMonthView(Context context) {
+ this(context, null);
+ }
+
+ public SimpleMonthView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.datePickerStyle);
+ }
+
+ public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final Resources res = context.getResources();
+ mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
+ mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
+ mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
+ mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
+ mDesiredDaySelectorRadius = res.getDimensionPixelSize(
+ R.dimen.date_picker_day_selector_radius);
+
+ // Set up accessibility components.
+ mTouchHelper = new MonthViewTouchHelper(this);
+ setAccessibilityDelegate(mTouchHelper);
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+
+ mLocale = res.getConfiguration().locale;
+ mCalendar = Calendar.getInstance(mLocale);
+
+ mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
+
+ updateMonthYearLabel();
+ updateDayOfWeekLabels();
+
+ initPaints(res);
+ }
+
+ private void updateMonthYearLabel() {
+ final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
+ final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
+ formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+ mMonthYearLabel = formatter.format(mCalendar.getTime());
+ }
+
+ private void updateDayOfWeekLabels() {
+ // Use tiny (e.g. single-character) weekday names from ICU. The indices
+ // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
+ final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
+ for (int i = 0; i < DAYS_IN_WEEK; i++) {
+ mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
+ }
+ }
+
+ /**
+ * Applies the specified text appearance resource to a paint, returning the
+ * text color if one is set in the text appearance.
+ *
+ * @param p the paint to modify
+ * @param resId the resource ID of the text appearance
+ * @return the text color, if available
+ */
+ private ColorStateList applyTextAppearance(Paint p, int resId) {
+ final TypedArray ta = mContext.obtainStyledAttributes(null,
+ R.styleable.TextAppearance, 0, resId);
+
+ final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
+ if (fontFamily != null) {
+ p.setTypeface(Typeface.create(fontFamily, 0));
+ }
+
+ p.setTextSize(ta.getDimensionPixelSize(
+ R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
+
+ final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
+ if (textColor != null) {
+ final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
+ p.setColor(enabledColor);
+ }
+
+ ta.recycle();
+
+ return textColor;
+ }
+
+ public int getMonthHeight() {
+ return mMonthHeight;
+ }
+
+ public int getCellWidth() {
+ return mCellWidth;
+ }
+
+ public void setMonthTextAppearance(int resId) {
+ applyTextAppearance(mMonthPaint, resId);
+
+ invalidate();
+ }
+
+ public void setDayOfWeekTextAppearance(int resId) {
+ applyTextAppearance(mDayOfWeekPaint, resId);
+ invalidate();
+ }
+
+ public void setDayTextAppearance(int resId) {
+ final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
+ if (textColor != null) {
+ mDayTextColor = textColor;
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Sets up the text and style properties for painting.
+ */
+ private void initPaints(Resources res) {
+ final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
+ final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
+ final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
+
+ final int monthTextSize = res.getDimensionPixelSize(
+ R.dimen.date_picker_month_text_size);
+ final int dayOfWeekTextSize = res.getDimensionPixelSize(
+ R.dimen.date_picker_day_of_week_text_size);
+ final int dayTextSize = res.getDimensionPixelSize(
+ R.dimen.date_picker_day_text_size);
+
+ mMonthPaint.setAntiAlias(true);
+ mMonthPaint.setTextSize(monthTextSize);
+ mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
+ mMonthPaint.setTextAlign(Align.CENTER);
+ mMonthPaint.setStyle(Style.FILL);
+
+ mDayOfWeekPaint.setAntiAlias(true);
+ mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
+ mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
+ mDayOfWeekPaint.setTextAlign(Align.CENTER);
+ mDayOfWeekPaint.setStyle(Style.FILL);
+
+ mDaySelectorPaint.setAntiAlias(true);
+ mDaySelectorPaint.setStyle(Style.FILL);
+
+ mDayHighlightPaint.setAntiAlias(true);
+ mDayHighlightPaint.setStyle(Style.FILL);
+
+ mDayHighlightSelectorPaint.setAntiAlias(true);
+ mDayHighlightSelectorPaint.setStyle(Style.FILL);
+
+ mDayPaint.setAntiAlias(true);
+ mDayPaint.setTextSize(dayTextSize);
+ mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
+ mDayPaint.setTextAlign(Align.CENTER);
+ mDayPaint.setStyle(Style.FILL);
+ }
+
+ void setMonthTextColor(ColorStateList monthTextColor) {
+ final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
+ mMonthPaint.setColor(enabledColor);
+ invalidate();
+ }
+
+ void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
+ final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
+ mDayOfWeekPaint.setColor(enabledColor);
+ invalidate();
+ }
+
+ void setDayTextColor(ColorStateList dayTextColor) {
+ mDayTextColor = dayTextColor;
+ invalidate();
+ }
+
+ void setDaySelectorColor(ColorStateList dayBackgroundColor) {
+ final int activatedColor = dayBackgroundColor.getColorForState(
+ StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
+ mDaySelectorPaint.setColor(activatedColor);
+ mDayHighlightSelectorPaint.setColor(activatedColor);
+ mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
+ invalidate();
+ }
+
+ void setDayHighlightColor(ColorStateList dayHighlightColor) {
+ final int pressedColor = dayHighlightColor.getColorForState(
+ StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
+ mDayHighlightPaint.setColor(pressedColor);
+ invalidate();
+ }
+
+ public void setOnDayClickListener(OnDayClickListener listener) {
+ mOnDayClickListener = listener;
+ }
+
+ @Override
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ // First right-of-refusal goes the touch exploration helper.
+ return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int x = (int) (event.getX() + 0.5f);
+ final int y = (int) (event.getY() + 0.5f);
+
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ final int touchedItem = getDayAtLocation(x, y);
+ mIsTouchHighlighted = true;
+ if (mHighlightedDay != touchedItem) {
+ mHighlightedDay = touchedItem;
+ mPreviouslyHighlightedDay = touchedItem;
+ invalidate();
+ }
+ if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
+ // Touch something that's not an item, reject event.
+ return false;
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ final int clickedDay = getDayAtLocation(x, y);
+ onDayClicked(clickedDay);
+ // Fall through.
+ case MotionEvent.ACTION_CANCEL:
+ // Reset touched day on stream end.
+ mHighlightedDay = -1;
+ mIsTouchHighlighted = false;
+ invalidate();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // We need to handle focus change within the SimpleMonthView because we are simulating
+ // multiple Views. The arrow keys will move between days until there is no space (no
+ // day to the left, top, right, or bottom). Focus forward and back jumps out of the
+ // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
+ // to the next focusable View in the hierarchy.
+ boolean focusChanged = false;
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (event.hasNoModifiers()) {
+ focusChanged = moveOneDay(isLayoutRtl());
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (event.hasNoModifiers()) {
+ focusChanged = moveOneDay(!isLayoutRtl());
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (event.hasNoModifiers()) {
+ ensureFocusedDay();
+ if (mHighlightedDay > 7) {
+ mHighlightedDay -= 7;
+ focusChanged = true;
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (event.hasNoModifiers()) {
+ ensureFocusedDay();
+ if (mHighlightedDay <= mDaysInMonth - 7) {
+ mHighlightedDay += 7;
+ focusChanged = true;
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (mHighlightedDay != -1) {
+ onDayClicked(mHighlightedDay);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_TAB: {
+ int focusChangeDirection = 0;
+ if (event.hasNoModifiers()) {
+ focusChangeDirection = View.FOCUS_FORWARD;
+ } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ focusChangeDirection = View.FOCUS_BACKWARD;
+ }
+ if (focusChangeDirection != 0) {
+ final ViewParent parent = getParent();
+ // move out of the ViewPager next/previous
+ View nextFocus = this;
+ do {
+ nextFocus = nextFocus.focusSearch(focusChangeDirection);
+ } while (nextFocus != null && nextFocus != this &&
+ nextFocus.getParent() == parent);
+ if (nextFocus != null) {
+ nextFocus.requestFocus();
+ return true;
+ }
+ }
+ break;
+ }
+ }
+ if (focusChanged) {
+ invalidate();
+ return true;
+ } else {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ private boolean moveOneDay(boolean positive) {
+ ensureFocusedDay();
+ boolean focusChanged = false;
+ if (positive) {
+ if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
+ mHighlightedDay++;
+ focusChanged = true;
+ }
+ } else {
+ if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
+ mHighlightedDay--;
+ focusChanged = true;
+ }
+ }
+ return focusChanged;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
+ @Nullable Rect previouslyFocusedRect) {
+ if (gainFocus) {
+ // If we've gained focus through arrow keys, we should find the day closest
+ // to the focus rect. If we've gained focus through forward/back, we should
+ // focus on the selected day if there is one.
+ final int offset = findDayOffset();
+ switch(direction) {
+ case View.FOCUS_RIGHT: {
+ int row = findClosestRow(previouslyFocusedRect);
+ mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
+ break;
+ }
+ case View.FOCUS_LEFT: {
+ int row = findClosestRow(previouslyFocusedRect) + 1;
+ mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
+ break;
+ }
+ case View.FOCUS_DOWN: {
+ final int col = findClosestColumn(previouslyFocusedRect);
+ final int day = col - offset + 1;
+ mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
+ break;
+ }
+ case View.FOCUS_UP: {
+ final int col = findClosestColumn(previouslyFocusedRect);
+ final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
+ final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
+ mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
+ break;
+ }
+ }
+ ensureFocusedDay();
+ invalidate();
+ }
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ }
+
+ /**
+ * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
+ */
+ private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
+ if (previouslyFocusedRect == null) {
+ return 3;
+ } else if (mDayHeight == 0) {
+ return 0; // There hasn't been a layout, so just choose the first row
+ } else {
+ int centerY = previouslyFocusedRect.centerY();
+
+ final TextPaint p = mDayPaint;
+ final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+ final int rowHeight = mDayHeight;
+
+ // Text is vertically centered within the row height.
+ final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+ final int rowCenter = headerHeight + rowHeight / 2;
+
+ centerY -= rowCenter - halfLineHeight;
+ int row = Math.round(centerY / (float) rowHeight);
+ final int maxDay = findDayOffset() + mDaysInMonth;
+ final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
+
+ row = MathUtils.constrain(row, 0, maxRows);
+ return row;
+ }
+ }
+
+ /**
+ * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
+ * The 0 index is related to the first day of the week.
+ */
+ private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
+ if (previouslyFocusedRect == null) {
+ return DAYS_IN_WEEK / 2;
+ } else if (mCellWidth == 0) {
+ return 0; // There hasn't been a layout, so we can just choose the first column
+ } else {
+ int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
+ final int columnFromLeft =
+ MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
+ return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
+ }
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ if (mHighlightedDay > 0) {
+ getBoundsForDay(mHighlightedDay, r);
+ } else {
+ super.getFocusedRect(r);
+ }
+ }
+
+ @Override
+ protected void onFocusLost() {
+ if (!mIsTouchHighlighted) {
+ // Unhighlight a day.
+ mPreviouslyHighlightedDay = mHighlightedDay;
+ mHighlightedDay = -1;
+ invalidate();
+ }
+ super.onFocusLost();
+ }
+
+ /**
+ * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
+ * if possible, or the first day of the month if not.
+ */
+ private void ensureFocusedDay() {
+ if (mHighlightedDay != -1) {
+ return;
+ }
+ if (mPreviouslyHighlightedDay != -1) {
+ mHighlightedDay = mPreviouslyHighlightedDay;
+ return;
+ }
+ if (mActivatedDay != -1) {
+ mHighlightedDay = mActivatedDay;
+ return;
+ }
+ mHighlightedDay = 1;
+ }
+
+ private boolean isFirstDayOfWeek(int day) {
+ final int offset = findDayOffset();
+ return (offset + day - 1) % DAYS_IN_WEEK == 0;
+ }
+
+ private boolean isLastDayOfWeek(int day) {
+ final int offset = findDayOffset();
+ return (offset + day) % DAYS_IN_WEEK == 0;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingTop = getPaddingTop();
+ canvas.translate(paddingLeft, paddingTop);
+
+ drawMonth(canvas);
+ drawDaysOfWeek(canvas);
+ drawDays(canvas);
+
+ canvas.translate(-paddingLeft, -paddingTop);
+ }
+
+ private void drawMonth(Canvas canvas) {
+ final float x = mPaddedWidth / 2f;
+
+ // Vertically centered within the month header height.
+ final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
+ final float y = (mMonthHeight - lineHeight) / 2f;
+
+ canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
+ }
+
+ public String getMonthYearLabel() {
+ return mMonthYearLabel;
+ }
+
+ private void drawDaysOfWeek(Canvas canvas) {
+ final TextPaint p = mDayOfWeekPaint;
+ final int headerHeight = mMonthHeight;
+ final int rowHeight = mDayOfWeekHeight;
+ final int colWidth = mCellWidth;
+
+ // Text is vertically centered within the day of week height.
+ final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+ final int rowCenter = headerHeight + rowHeight / 2;
+
+ for (int col = 0; col < DAYS_IN_WEEK; col++) {
+ final int colCenter = colWidth * col + colWidth / 2;
+ final int colCenterRtl;
+ if (isLayoutRtl()) {
+ colCenterRtl = mPaddedWidth - colCenter;
+ } else {
+ colCenterRtl = colCenter;
+ }
+
+ final String label = mDayOfWeekLabels[col];
+ canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
+ }
+ }
+
+ /**
+ * Draws the month days.
+ */
+ private void drawDays(Canvas canvas) {
+ final TextPaint p = mDayPaint;
+ final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+ final int rowHeight = mDayHeight;
+ final int colWidth = mCellWidth;
+
+ // Text is vertically centered within the row height.
+ final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+ int rowCenter = headerHeight + rowHeight / 2;
+
+ for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
+ final int colCenter = colWidth * col + colWidth / 2;
+ final int colCenterRtl;
+ if (isLayoutRtl()) {
+ colCenterRtl = mPaddedWidth - colCenter;
+ } else {
+ colCenterRtl = colCenter;
+ }
+
+ int stateMask = 0;
+
+ final boolean isDayEnabled = isDayEnabled(day);
+ if (isDayEnabled) {
+ stateMask |= StateSet.VIEW_STATE_ENABLED;
+ }
+
+ final boolean isDayActivated = mActivatedDay == day;
+ final boolean isDayHighlighted = mHighlightedDay == day;
+ if (isDayActivated) {
+ stateMask |= StateSet.VIEW_STATE_ACTIVATED;
+
+ // Adjust the circle to be centered on the row.
+ final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
+ mDaySelectorPaint;
+ canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
+ } else if (isDayHighlighted) {
+ stateMask |= StateSet.VIEW_STATE_PRESSED;
+
+ if (isDayEnabled) {
+ // Adjust the circle to be centered on the row.
+ canvas.drawCircle(colCenterRtl, rowCenter,
+ mDaySelectorRadius, mDayHighlightPaint);
+ }
+ }
+
+ final boolean isDayToday = mToday == day;
+ final int dayTextColor;
+ if (isDayToday && !isDayActivated) {
+ dayTextColor = mDaySelectorPaint.getColor();
+ } else {
+ final int[] stateSet = StateSet.get(stateMask);
+ dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
+ }
+ p.setColor(dayTextColor);
+
+ canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
+
+ col++;
+
+ if (col == DAYS_IN_WEEK) {
+ col = 0;
+ rowCenter += rowHeight;
+ }
+ }
+ }
+
+ private boolean isDayEnabled(int day) {
+ return day >= mEnabledDayStart && day <= mEnabledDayEnd;
+ }
+
+ private boolean isValidDayOfMonth(int day) {
+ return day >= 1 && day <= mDaysInMonth;
+ }
+
+ private static boolean isValidDayOfWeek(int day) {
+ return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
+ }
+
+ private static boolean isValidMonth(int month) {
+ return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
+ }
+
+ /**
+ * Sets the selected day.
+ *
+ * @param dayOfMonth the selected day of the month, or {@code -1} to clear
+ * the selection
+ */
+ public void setSelectedDay(int dayOfMonth) {
+ mActivatedDay = dayOfMonth;
+
+ // Invalidate cached accessibility information.
+ mTouchHelper.invalidateRoot();
+ invalidate();
+ }
+
+ /**
+ * Sets the first day of the week.
+ *
+ * @param weekStart which day the week should start on, valid values are
+ * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+ */
+ public void setFirstDayOfWeek(int weekStart) {
+ if (isValidDayOfWeek(weekStart)) {
+ mWeekStart = weekStart;
+ } else {
+ mWeekStart = mCalendar.getFirstDayOfWeek();
+ }
+
+ updateDayOfWeekLabels();
+
+ // Invalidate cached accessibility information.
+ mTouchHelper.invalidateRoot();
+ invalidate();
+ }
+
+ /**
+ * Sets all the parameters for displaying this week.
+ * <p>
+ * Parameters have a default value and will only update if a new value is
+ * included, except for focus month, which will always default to no focus
+ * month if no value is passed in. The only required parameter is the week
+ * start.
+ *
+ * @param selectedDay the selected day of the month, or -1 for no selection
+ * @param month the month
+ * @param year the year
+ * @param weekStart which day the week should start on, valid values are
+ * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+ * @param enabledDayStart the first enabled day
+ * @param enabledDayEnd the last enabled day
+ */
+ void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
+ int enabledDayEnd) {
+ mActivatedDay = selectedDay;
+
+ if (isValidMonth(month)) {
+ mMonth = month;
+ }
+ mYear = year;
+
+ mCalendar.set(Calendar.MONTH, mMonth);
+ mCalendar.set(Calendar.YEAR, mYear);
+ mCalendar.set(Calendar.DAY_OF_MONTH, 1);
+ mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
+
+ if (isValidDayOfWeek(weekStart)) {
+ mWeekStart = weekStart;
+ } else {
+ mWeekStart = mCalendar.getFirstDayOfWeek();
+ }
+
+ // Figure out what day today is.
+ final Calendar today = Calendar.getInstance();
+ mToday = -1;
+ mDaysInMonth = getDaysInMonth(mMonth, mYear);
+ for (int i = 0; i < mDaysInMonth; i++) {
+ final int day = i + 1;
+ if (sameDay(day, today)) {
+ mToday = day;
+ }
+ }
+
+ mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
+ mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
+
+ updateMonthYearLabel();
+ updateDayOfWeekLabels();
+
+ // Invalidate cached accessibility information.
+ mTouchHelper.invalidateRoot();
+ invalidate();
+ }
+
+ private static int getDaysInMonth(int month, int year) {
+ switch (month) {
+ case Calendar.JANUARY:
+ case Calendar.MARCH:
+ case Calendar.MAY:
+ case Calendar.JULY:
+ case Calendar.AUGUST:
+ case Calendar.OCTOBER:
+ case Calendar.DECEMBER:
+ return 31;
+ case Calendar.APRIL:
+ case Calendar.JUNE:
+ case Calendar.SEPTEMBER:
+ case Calendar.NOVEMBER:
+ return 30;
+ case Calendar.FEBRUARY:
+ return (year % 4 == 0) ? 29 : 28;
+ default:
+ throw new IllegalArgumentException("Invalid Month");
+ }
+ }
+
+ private boolean sameDay(int day, Calendar today) {
+ return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
+ && day == today.get(Calendar.DAY_OF_MONTH);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
+ + mDesiredDayOfWeekHeight + mDesiredMonthHeight
+ + getPaddingTop() + getPaddingBottom();
+ final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
+ + getPaddingStart() + getPaddingEnd();
+ final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
+ final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
+ setMeasuredDimension(resolvedWidth, resolvedHeight);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ requestLayout();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) {
+ return;
+ }
+
+ // Let's initialize a completely reasonable number of variables.
+ final int w = right - left;
+ final int h = bottom - top;
+ final int paddingLeft = getPaddingLeft();
+ final int paddingTop = getPaddingTop();
+ final int paddingRight = getPaddingRight();
+ final int paddingBottom = getPaddingBottom();
+ final int paddedRight = w - paddingRight;
+ final int paddedBottom = h - paddingBottom;
+ final int paddedWidth = paddedRight - paddingLeft;
+ final int paddedHeight = paddedBottom - paddingTop;
+ if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
+ return;
+ }
+
+ mPaddedWidth = paddedWidth;
+ mPaddedHeight = paddedHeight;
+
+ // We may have been laid out smaller than our preferred size. If so,
+ // scale all dimensions to fit.
+ final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
+ final float scaleH = paddedHeight / (float) measuredPaddedHeight;
+ final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
+ final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
+ mMonthHeight = monthHeight;
+ mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
+ mDayHeight = (int) (mDesiredDayHeight * scaleH);
+ mCellWidth = cellWidth;
+
+ // Compute the largest day selector radius that's still within the clip
+ // bounds and desired selector radius.
+ final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
+ final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
+ mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
+ Math.min(maxSelectorWidth, maxSelectorHeight));
+
+ // Invalidate cached accessibility information.
+ mTouchHelper.invalidateRoot();
+ }
+
+ private int findDayOffset() {
+ final int offset = mDayOfWeekStart - mWeekStart;
+ if (mDayOfWeekStart < mWeekStart) {
+ return offset + DAYS_IN_WEEK;
+ }
+ return offset;
+ }
+
+ /**
+ * Calculates the day of the month at the specified touch position. Returns
+ * the day of the month or -1 if the position wasn't in a valid day.
+ *
+ * @param x the x position of the touch event
+ * @param y the y position of the touch event
+ * @return the day of the month at (x, y), or -1 if the position wasn't in
+ * a valid day
+ */
+ private int getDayAtLocation(int x, int y) {
+ final int paddedX = x - getPaddingLeft();
+ if (paddedX < 0 || paddedX >= mPaddedWidth) {
+ return -1;
+ }
+
+ final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+ final int paddedY = y - getPaddingTop();
+ if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
+ return -1;
+ }
+
+ // Adjust for RTL after applying padding.
+ final int paddedXRtl;
+ if (isLayoutRtl()) {
+ paddedXRtl = mPaddedWidth - paddedX;
+ } else {
+ paddedXRtl = paddedX;
+ }
+
+ final int row = (paddedY - headerHeight) / mDayHeight;
+ final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
+ final int index = col + row * DAYS_IN_WEEK;
+ final int day = index + 1 - findDayOffset();
+ if (!isValidDayOfMonth(day)) {
+ return -1;
+ }
+
+ return day;
+ }
+
+ /**
+ * Calculates the bounds of the specified day.
+ *
+ * @param id the day of the month
+ * @param outBounds the rect to populate with bounds
+ */
+ public boolean getBoundsForDay(int id, Rect outBounds) {
+ if (!isValidDayOfMonth(id)) {
+ return false;
+ }
+
+ final int index = id - 1 + findDayOffset();
+
+ // Compute left edge, taking into account RTL.
+ final int col = index % DAYS_IN_WEEK;
+ final int colWidth = mCellWidth;
+ final int left;
+ if (isLayoutRtl()) {
+ left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
+ } else {
+ left = getPaddingLeft() + col * colWidth;
+ }
+
+ // Compute top edge.
+ final int row = index / DAYS_IN_WEEK;
+ final int rowHeight = mDayHeight;
+ final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+ final int top = getPaddingTop() + headerHeight + row * rowHeight;
+
+ outBounds.set(left, top, left + colWidth, top + rowHeight);
+
+ return true;
+ }
+
+ /**
+ * Called when the user clicks on a day. Handles callbacks to the
+ * {@link OnDayClickListener} if one is set.
+ *
+ * @param day the day that was clicked
+ */
+ private boolean onDayClicked(int day) {
+ if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
+ return false;
+ }
+
+ if (mOnDayClickListener != null) {
+ final Calendar date = Calendar.getInstance();
+ date.set(mYear, mMonth, day);
+ mOnDayClickListener.onDayClick(this, date);
+ }
+
+ // This is a no-op if accessibility is turned off.
+ mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
+ return true;
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (!isEnabled()) {
+ return null;
+ }
+ // Add 0.5f to event coordinates to match the logic in onTouchEvent.
+ final int x = (int) (event.getX() + 0.5f);
+ final int y = (int) (event.getY() + 0.5f);
+ final int dayUnderPointer = getDayAtLocation(x, y);
+ if (dayUnderPointer >= 0) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ /**
+ * Provides a virtual view hierarchy for interfacing with an accessibility
+ * service.
+ */
+ private class MonthViewTouchHelper extends ExploreByTouchHelper {
+ private static final String DATE_FORMAT = "dd MMMM yyyy";
+
+ private final Rect mTempRect = new Rect();
+ private final Calendar mTempCalendar = Calendar.getInstance();
+
+ public MonthViewTouchHelper(View host) {
+ super(host);
+ }
+
+ @Override
+ protected int getVirtualViewAt(float x, float y) {
+ final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
+ if (day != -1) {
+ return day;
+ }
+ return ExploreByTouchHelper.INVALID_ID;
+ }
+
+ @Override
+ protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+ for (int day = 1; day <= mDaysInMonth; day++) {
+ virtualViewIds.add(day);
+ }
+ }
+
+ @Override
+ protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+ event.setContentDescription(getDayDescription(virtualViewId));
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+ final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
+
+ if (!hasBounds) {
+ // The day is invalid, kill the node.
+ mTempRect.setEmpty();
+ node.setContentDescription("");
+ node.setBoundsInParent(mTempRect);
+ node.setVisibleToUser(false);
+ return;
+ }
+
+ node.setText(getDayText(virtualViewId));
+ node.setContentDescription(getDayDescription(virtualViewId));
+ node.setBoundsInParent(mTempRect);
+
+ final boolean isDayEnabled = isDayEnabled(virtualViewId);
+ if (isDayEnabled) {
+ node.addAction(AccessibilityAction.ACTION_CLICK);
+ }
+
+ node.setEnabled(isDayEnabled);
+
+ if (virtualViewId == mActivatedDay) {
+ // TODO: This should use activated once that's supported.
+ node.setChecked(true);
+ }
+
+ }
+
+ @Override
+ protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+ Bundle arguments) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK:
+ return onDayClicked(virtualViewId);
+ }
+
+ return false;
+ }
+
+ /**
+ * Generates a description for a given virtual view.
+ *
+ * @param id the day to generate a description for
+ * @return a description of the virtual view
+ */
+ private CharSequence getDayDescription(int id) {
+ if (isValidDayOfMonth(id)) {
+ mTempCalendar.set(mYear, mMonth, id);
+ return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
+ }
+
+ return "";
+ }
+
+ /**
+ * Generates displayed text for a given virtual view.
+ *
+ * @param id the day to generate text for
+ * @return the visible text of the virtual view
+ */
+ private CharSequence getDayText(int id) {
+ if (isValidDayOfMonth(id)) {
+ return mDayFormatter.format(id);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Handles callbacks when the user clicks on a time object.
+ */
+ public interface OnDayClickListener {
+ void onDayClick(SimpleMonthView view, Calendar day);
+ }
+}
diff --git a/android/widget/SlidingDrawer.java b/android/widget/SlidingDrawer.java
new file mode 100644
index 0000000..9f48397
--- /dev/null
+++ b/android/widget/SlidingDrawer.java
@@ -0,0 +1,979 @@
+/*
+ * 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.R;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * SlidingDrawer hides content out of the screen and allows the user to drag a handle
+ * to bring the content on screen. SlidingDrawer can be used vertically or horizontally.
+ *
+ * A special widget composed of two children views: the handle, that the users drags,
+ * and the content, attached to the handle and dragged with it.
+ *
+ * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer
+ * should only be used inside of a FrameLayout or a RelativeLayout for instance. The
+ * size of the SlidingDrawer defines how much space the content will occupy once slid
+ * out so SlidingDrawer should usually use match_parent for both its dimensions.
+ *
+ * Inside an XML layout, SlidingDrawer must define the id of the handle and of the
+ * content:
+ *
+ * <pre class="prettyprint">
+ * <SlidingDrawer
+ * android:id="@+id/drawer"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent"
+ *
+ * android:handle="@+id/handle"
+ * android:content="@+id/content">
+ *
+ * <ImageView
+ * android:id="@id/handle"
+ * android:layout_width="88dip"
+ * android:layout_height="44dip" />
+ *
+ * <GridView
+ * android:id="@id/content"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent" />
+ *
+ * </SlidingDrawer>
+ * </pre>
+ *
+ * @attr ref android.R.styleable#SlidingDrawer_content
+ * @attr ref android.R.styleable#SlidingDrawer_handle
+ * @attr ref android.R.styleable#SlidingDrawer_topOffset
+ * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
+ * @attr ref android.R.styleable#SlidingDrawer_orientation
+ * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
+ * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
+ *
+ * @deprecated This class is not supported anymore. It is recommended you
+ * base your own implementation on the source code for the Android Open
+ * Source Project if you must use it in your application.
+ */
+@Deprecated
+public class SlidingDrawer extends ViewGroup {
+ public static final int ORIENTATION_HORIZONTAL = 0;
+ public static final int ORIENTATION_VERTICAL = 1;
+
+ private static final int TAP_THRESHOLD = 6;
+ private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
+ private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
+ private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
+ private static final float MAXIMUM_ACCELERATION = 2000.0f;
+ private static final int VELOCITY_UNITS = 1000;
+ private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
+
+ private static final int EXPANDED_FULL_OPEN = -10001;
+ private static final int COLLAPSED_FULL_CLOSED = -10002;
+
+ private final int mHandleId;
+ private final int mContentId;
+
+ private View mHandle;
+ private View mContent;
+
+ private final Rect mFrame = new Rect();
+ private final Rect mInvalidate = new Rect();
+ private boolean mTracking;
+ private boolean mLocked;
+
+ private VelocityTracker mVelocityTracker;
+
+ private boolean mVertical;
+ private boolean mExpanded;
+ private int mBottomOffset;
+ private int mTopOffset;
+ private int mHandleHeight;
+ private int mHandleWidth;
+
+ private OnDrawerOpenListener mOnDrawerOpenListener;
+ private OnDrawerCloseListener mOnDrawerCloseListener;
+ private OnDrawerScrollListener mOnDrawerScrollListener;
+
+ private float mAnimatedAcceleration;
+ private float mAnimatedVelocity;
+ private float mAnimationPosition;
+ private long mAnimationLastTime;
+ private long mCurrentAnimationTime;
+ private int mTouchDelta;
+ private boolean mAnimating;
+ private boolean mAllowSingleTap;
+ private boolean mAnimateOnClick;
+
+ private final int mTapThreshold;
+ private final int mMaximumTapVelocity;
+ private final int mMaximumMinorVelocity;
+ private final int mMaximumMajorVelocity;
+ private final int mMaximumAcceleration;
+ private final int mVelocityUnits;
+
+ /**
+ * Callback invoked when the drawer is opened.
+ */
+ public static interface OnDrawerOpenListener {
+ /**
+ * Invoked when the drawer becomes fully open.
+ */
+ public void onDrawerOpened();
+ }
+
+ /**
+ * Callback invoked when the drawer is closed.
+ */
+ public static interface OnDrawerCloseListener {
+ /**
+ * Invoked when the drawer becomes fully closed.
+ */
+ public void onDrawerClosed();
+ }
+
+ /**
+ * Callback invoked when the drawer is scrolled.
+ */
+ public static interface OnDrawerScrollListener {
+ /**
+ * Invoked when the user starts dragging/flinging the drawer's handle.
+ */
+ public void onScrollStarted();
+
+ /**
+ * Invoked when the user stops dragging/flinging the drawer's handle.
+ */
+ public void onScrollEnded();
+ }
+
+ /**
+ * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+ *
+ * @param context The application's environment.
+ * @param attrs The attributes defined in XML.
+ */
+ public SlidingDrawer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+ *
+ * @param context The application's environment.
+ * @param attrs The attributes defined in XML.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+ *
+ * @param context The application's environment.
+ * @param attrs The attributes defined in XML.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
+ */
+ public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SlidingDrawer, defStyleAttr, defStyleRes);
+
+ int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL);
+ mVertical = orientation == ORIENTATION_VERTICAL;
+ mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f);
+ mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f);
+ mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true);
+ mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true);
+
+ int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0);
+ if (handleId == 0) {
+ throw new IllegalArgumentException("The handle attribute is required and must refer "
+ + "to a valid child.");
+ }
+
+ int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0);
+ if (contentId == 0) {
+ throw new IllegalArgumentException("The content attribute is required and must refer "
+ + "to a valid child.");
+ }
+
+ if (handleId == contentId) {
+ throw new IllegalArgumentException("The content and handle attributes must refer "
+ + "to different children.");
+ }
+
+ mHandleId = handleId;
+ mContentId = contentId;
+
+ final float density = getResources().getDisplayMetrics().density;
+ mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
+ mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
+ mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
+ mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
+ mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
+ mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
+
+ a.recycle();
+
+ setAlwaysDrawnWithCacheEnabled(false);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mHandle = findViewById(mHandleId);
+ if (mHandle == null) {
+ throw new IllegalArgumentException("The handle attribute is must refer to an"
+ + " existing child.");
+ }
+ mHandle.setOnClickListener(new DrawerToggler());
+
+ mContent = findViewById(mContentId);
+ if (mContent == null) {
+ throw new IllegalArgumentException("The content attribute is must refer to an"
+ + " existing child.");
+ }
+ mContent.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
+ }
+
+ final View handle = mHandle;
+ measureChild(handle, widthMeasureSpec, heightMeasureSpec);
+
+ if (mVertical) {
+ int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
+ mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ } else {
+ int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
+ mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
+ }
+
+ setMeasuredDimension(widthSpecSize, heightSpecSize);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ final long drawingTime = getDrawingTime();
+ final View handle = mHandle;
+ final boolean isVertical = mVertical;
+
+ drawChild(canvas, handle, drawingTime);
+
+ if (mTracking || mAnimating) {
+ final Bitmap cache = mContent.getDrawingCache();
+ if (cache != null) {
+ if (isVertical) {
+ canvas.drawBitmap(cache, 0, handle.getBottom(), null);
+ } else {
+ canvas.drawBitmap(cache, handle.getRight(), 0, null);
+ }
+ } else {
+ canvas.save();
+ canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset,
+ isVertical ? handle.getTop() - mTopOffset : 0);
+ drawChild(canvas, mContent, drawingTime);
+ canvas.restore();
+ }
+ } else if (mExpanded) {
+ drawChild(canvas, mContent, drawingTime);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mTracking) {
+ return;
+ }
+
+ final int width = r - l;
+ final int height = b - t;
+
+ final View handle = mHandle;
+
+ int childWidth = handle.getMeasuredWidth();
+ int childHeight = handle.getMeasuredHeight();
+
+ int childLeft;
+ int childTop;
+
+ final View content = mContent;
+
+ if (mVertical) {
+ childLeft = (width - childWidth) / 2;
+ childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
+
+ content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
+ mTopOffset + childHeight + content.getMeasuredHeight());
+ } else {
+ childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
+ childTop = (height - childHeight) / 2;
+
+ content.layout(mTopOffset + childWidth, 0,
+ mTopOffset + childWidth + content.getMeasuredWidth(),
+ content.getMeasuredHeight());
+ }
+
+ handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+ mHandleHeight = handle.getHeight();
+ mHandleWidth = handle.getWidth();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mLocked) {
+ return false;
+ }
+
+ final int action = event.getAction();
+
+ float x = event.getX();
+ float y = event.getY();
+
+ final Rect frame = mFrame;
+ final View handle = mHandle;
+
+ handle.getHitRect(frame);
+ if (!mTracking && !frame.contains((int) x, (int) y)) {
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mTracking = true;
+
+ handle.setPressed(true);
+ // Must be called before prepareTracking()
+ prepareContent();
+
+ // Must be called after prepareContent()
+ if (mOnDrawerScrollListener != null) {
+ mOnDrawerScrollListener.onScrollStarted();
+ }
+
+ if (mVertical) {
+ final int top = mHandle.getTop();
+ mTouchDelta = (int) y - top;
+ prepareTracking(top);
+ } else {
+ final int left = mHandle.getLeft();
+ mTouchDelta = (int) x - left;
+ prepareTracking(left);
+ }
+ mVelocityTracker.addMovement(event);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mLocked) {
+ return true;
+ }
+
+ if (mTracking) {
+ mVelocityTracker.addMovement(event);
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(mVelocityUnits);
+
+ float yVelocity = velocityTracker.getYVelocity();
+ float xVelocity = velocityTracker.getXVelocity();
+ boolean negative;
+
+ final boolean vertical = mVertical;
+ if (vertical) {
+ negative = yVelocity < 0;
+ if (xVelocity < 0) {
+ xVelocity = -xVelocity;
+ }
+ if (xVelocity > mMaximumMinorVelocity) {
+ xVelocity = mMaximumMinorVelocity;
+ }
+ } else {
+ negative = xVelocity < 0;
+ if (yVelocity < 0) {
+ yVelocity = -yVelocity;
+ }
+ if (yVelocity > mMaximumMinorVelocity) {
+ yVelocity = mMaximumMinorVelocity;
+ }
+ }
+
+ float velocity = (float) Math.hypot(xVelocity, yVelocity);
+ if (negative) {
+ velocity = -velocity;
+ }
+
+ final int top = mHandle.getTop();
+ final int left = mHandle.getLeft();
+
+ if (Math.abs(velocity) < mMaximumTapVelocity) {
+ if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) ||
+ (!mExpanded && top > mBottomOffset + mBottom - mTop -
+ mHandleHeight - mTapThreshold) :
+ (mExpanded && left < mTapThreshold + mTopOffset) ||
+ (!mExpanded && left > mBottomOffset + mRight - mLeft -
+ mHandleWidth - mTapThreshold)) {
+
+ if (mAllowSingleTap) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+
+ if (mExpanded) {
+ animateClose(vertical ? top : left, true);
+ } else {
+ animateOpen(vertical ? top : left, true);
+ }
+ } else {
+ performFling(vertical ? top : left, velocity, false, true);
+ }
+
+ } else {
+ performFling(vertical ? top : left, velocity, false, true);
+ }
+ } else {
+ performFling(vertical ? top : left, velocity, false, true);
+ }
+ }
+ break;
+ }
+ }
+
+ return mTracking || mAnimating || super.onTouchEvent(event);
+ }
+
+ private void animateClose(int position, boolean notifyScrollListener) {
+ prepareTracking(position);
+ performFling(position, mMaximumAcceleration, true, notifyScrollListener);
+ }
+
+ private void animateOpen(int position, boolean notifyScrollListener) {
+ prepareTracking(position);
+ performFling(position, -mMaximumAcceleration, true, notifyScrollListener);
+ }
+
+ private void performFling(int position, float velocity, boolean always,
+ boolean notifyScrollListener) {
+ mAnimationPosition = position;
+ mAnimatedVelocity = velocity;
+
+ if (mExpanded) {
+ if (always || (velocity > mMaximumMajorVelocity ||
+ (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) &&
+ velocity > -mMaximumMajorVelocity))) {
+ // We are expanded, but they didn't move sufficiently to cause
+ // us to retract. Animate back to the expanded position.
+ mAnimatedAcceleration = mMaximumAcceleration;
+ if (velocity < 0) {
+ mAnimatedVelocity = 0;
+ }
+ } else {
+ // We are expanded and are now going to animate away.
+ mAnimatedAcceleration = -mMaximumAcceleration;
+ if (velocity > 0) {
+ mAnimatedVelocity = 0;
+ }
+ }
+ } else {
+ if (!always && (velocity > mMaximumMajorVelocity ||
+ (position > (mVertical ? getHeight() : getWidth()) / 2 &&
+ velocity > -mMaximumMajorVelocity))) {
+ // We are collapsed, and they moved enough to allow us to expand.
+ mAnimatedAcceleration = mMaximumAcceleration;
+ if (velocity < 0) {
+ mAnimatedVelocity = 0;
+ }
+ } else {
+ // We are collapsed, but they didn't move sufficiently to cause
+ // us to retract. Animate back to the collapsed position.
+ mAnimatedAcceleration = -mMaximumAcceleration;
+ if (velocity > 0) {
+ mAnimatedVelocity = 0;
+ }
+ }
+ }
+
+ long now = SystemClock.uptimeMillis();
+ mAnimationLastTime = now;
+ mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
+ mAnimating = true;
+ removeCallbacks(mSlidingRunnable);
+ postDelayed(mSlidingRunnable, ANIMATION_FRAME_DURATION);
+ stopTracking(notifyScrollListener);
+ }
+
+ private void prepareTracking(int position) {
+ mTracking = true;
+ mVelocityTracker = VelocityTracker.obtain();
+ boolean opening = !mExpanded;
+ if (opening) {
+ mAnimatedAcceleration = mMaximumAcceleration;
+ mAnimatedVelocity = mMaximumMajorVelocity;
+ mAnimationPosition = mBottomOffset +
+ (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
+ moveHandle((int) mAnimationPosition);
+ mAnimating = true;
+ removeCallbacks(mSlidingRunnable);
+ long now = SystemClock.uptimeMillis();
+ mAnimationLastTime = now;
+ mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
+ mAnimating = true;
+ } else {
+ if (mAnimating) {
+ mAnimating = false;
+ removeCallbacks(mSlidingRunnable);
+ }
+ moveHandle(position);
+ }
+ }
+
+ private void moveHandle(int position) {
+ final View handle = mHandle;
+
+ if (mVertical) {
+ if (position == EXPANDED_FULL_OPEN) {
+ handle.offsetTopAndBottom(mTopOffset - handle.getTop());
+ invalidate();
+ } else if (position == COLLAPSED_FULL_CLOSED) {
+ handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop -
+ mHandleHeight - handle.getTop());
+ invalidate();
+ } else {
+ final int top = handle.getTop();
+ int deltaY = position - top;
+ if (position < mTopOffset) {
+ deltaY = mTopOffset - top;
+ } else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) {
+ deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top;
+ }
+ handle.offsetTopAndBottom(deltaY);
+
+ final Rect frame = mFrame;
+ final Rect region = mInvalidate;
+
+ handle.getHitRect(frame);
+ region.set(frame);
+
+ region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
+ region.union(0, frame.bottom - deltaY, getWidth(),
+ frame.bottom - deltaY + mContent.getHeight());
+
+ invalidate(region);
+ }
+ } else {
+ if (position == EXPANDED_FULL_OPEN) {
+ handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
+ invalidate();
+ } else if (position == COLLAPSED_FULL_CLOSED) {
+ handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft -
+ mHandleWidth - handle.getLeft());
+ invalidate();
+ } else {
+ final int left = handle.getLeft();
+ int deltaX = position - left;
+ if (position < mTopOffset) {
+ deltaX = mTopOffset - left;
+ } else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) {
+ deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left;
+ }
+ handle.offsetLeftAndRight(deltaX);
+
+ final Rect frame = mFrame;
+ final Rect region = mInvalidate;
+
+ handle.getHitRect(frame);
+ region.set(frame);
+
+ region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
+ region.union(frame.right - deltaX, 0,
+ frame.right - deltaX + mContent.getWidth(), getHeight());
+
+ invalidate(region);
+ }
+ }
+ }
+
+ private void prepareContent() {
+ if (mAnimating) {
+ return;
+ }
+
+ // Something changed in the content, we need to honor the layout request
+ // before creating the cached bitmap
+ final View content = mContent;
+ if (content.isLayoutRequested()) {
+ if (mVertical) {
+ final int childHeight = mHandleHeight;
+ int height = mBottom - mTop - childHeight - mTopOffset;
+ content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
+ mTopOffset + childHeight + content.getMeasuredHeight());
+ } else {
+ final int childWidth = mHandle.getWidth();
+ int width = mRight - mLeft - childWidth - mTopOffset;
+ content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY));
+ content.layout(childWidth + mTopOffset, 0,
+ mTopOffset + childWidth + content.getMeasuredWidth(),
+ content.getMeasuredHeight());
+ }
+ }
+ // Try only once... we should really loop but it's not a big deal
+ // if the draw was cancelled, it will only be temporary anyway
+ content.getViewTreeObserver().dispatchOnPreDraw();
+ if (!content.isHardwareAccelerated()) content.buildDrawingCache();
+
+ content.setVisibility(View.GONE);
+ }
+
+ private void stopTracking(boolean notifyScrollListener) {
+ mHandle.setPressed(false);
+ mTracking = false;
+
+ if (notifyScrollListener && mOnDrawerScrollListener != null) {
+ mOnDrawerScrollListener.onScrollEnded();
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void doAnimation() {
+ if (mAnimating) {
+ incrementAnimation();
+ if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
+ mAnimating = false;
+ closeDrawer();
+ } else if (mAnimationPosition < mTopOffset) {
+ mAnimating = false;
+ openDrawer();
+ } else {
+ moveHandle((int) mAnimationPosition);
+ mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
+ postDelayed(mSlidingRunnable, ANIMATION_FRAME_DURATION);
+ }
+ }
+ }
+
+ private void incrementAnimation() {
+ long now = SystemClock.uptimeMillis();
+ float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
+ final float position = mAnimationPosition;
+ final float v = mAnimatedVelocity; // px/s
+ final float a = mAnimatedAcceleration; // px/s/s
+ mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
+ mAnimatedVelocity = v + (a * t); // px/s
+ mAnimationLastTime = now; // ms
+ }
+
+ /**
+ * Toggles the drawer open and close. Takes effect immediately.
+ *
+ * @see #open()
+ * @see #close()
+ * @see #animateClose()
+ * @see #animateOpen()
+ * @see #animateToggle()
+ */
+ public void toggle() {
+ if (!mExpanded) {
+ openDrawer();
+ } else {
+ closeDrawer();
+ }
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Toggles the drawer open and close with an animation.
+ *
+ * @see #open()
+ * @see #close()
+ * @see #animateClose()
+ * @see #animateOpen()
+ * @see #toggle()
+ */
+ public void animateToggle() {
+ if (!mExpanded) {
+ animateOpen();
+ } else {
+ animateClose();
+ }
+ }
+
+ /**
+ * Opens the drawer immediately.
+ *
+ * @see #toggle()
+ * @see #close()
+ * @see #animateOpen()
+ */
+ public void open() {
+ openDrawer();
+ invalidate();
+ requestLayout();
+
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ /**
+ * Closes the drawer immediately.
+ *
+ * @see #toggle()
+ * @see #open()
+ * @see #animateClose()
+ */
+ public void close() {
+ closeDrawer();
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Closes the drawer with an animation.
+ *
+ * @see #close()
+ * @see #open()
+ * @see #animateOpen()
+ * @see #animateToggle()
+ * @see #toggle()
+ */
+ public void animateClose() {
+ prepareContent();
+ final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
+ if (scrollListener != null) {
+ scrollListener.onScrollStarted();
+ }
+ animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft(), false);
+
+ if (scrollListener != null) {
+ scrollListener.onScrollEnded();
+ }
+ }
+
+ /**
+ * Opens the drawer with an animation.
+ *
+ * @see #close()
+ * @see #open()
+ * @see #animateClose()
+ * @see #animateToggle()
+ * @see #toggle()
+ */
+ public void animateOpen() {
+ prepareContent();
+ final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
+ if (scrollListener != null) {
+ scrollListener.onScrollStarted();
+ }
+ animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft(), false);
+
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+
+ if (scrollListener != null) {
+ scrollListener.onScrollEnded();
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SlidingDrawer.class.getName();
+ }
+
+ private void closeDrawer() {
+ moveHandle(COLLAPSED_FULL_CLOSED);
+ mContent.setVisibility(View.GONE);
+ mContent.destroyDrawingCache();
+
+ if (!mExpanded) {
+ return;
+ }
+
+ mExpanded = false;
+ if (mOnDrawerCloseListener != null) {
+ mOnDrawerCloseListener.onDrawerClosed();
+ }
+ }
+
+ private void openDrawer() {
+ moveHandle(EXPANDED_FULL_OPEN);
+ mContent.setVisibility(View.VISIBLE);
+
+ if (mExpanded) {
+ return;
+ }
+
+ mExpanded = true;
+
+ if (mOnDrawerOpenListener != null) {
+ mOnDrawerOpenListener.onDrawerOpened();
+ }
+ }
+
+ /**
+ * Sets the listener that receives a notification when the drawer becomes open.
+ *
+ * @param onDrawerOpenListener The listener to be notified when the drawer is opened.
+ */
+ public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
+ mOnDrawerOpenListener = onDrawerOpenListener;
+ }
+
+ /**
+ * Sets the listener that receives a notification when the drawer becomes close.
+ *
+ * @param onDrawerCloseListener The listener to be notified when the drawer is closed.
+ */
+ public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
+ mOnDrawerCloseListener = onDrawerCloseListener;
+ }
+
+ /**
+ * Sets the listener that receives a notification when the drawer starts or ends
+ * a scroll. A fling is considered as a scroll. A fling will also trigger a
+ * drawer opened or drawer closed event.
+ *
+ * @param onDrawerScrollListener The listener to be notified when scrolling
+ * starts or stops.
+ */
+ public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
+ mOnDrawerScrollListener = onDrawerScrollListener;
+ }
+
+ /**
+ * Returns the handle of the drawer.
+ *
+ * @return The View reprenseting the handle of the drawer, identified by
+ * the "handle" id in XML.
+ */
+ public View getHandle() {
+ return mHandle;
+ }
+
+ /**
+ * Returns the content of the drawer.
+ *
+ * @return The View reprenseting the content of the drawer, identified by
+ * the "content" id in XML.
+ */
+ public View getContent() {
+ return mContent;
+ }
+
+ /**
+ * Unlocks the SlidingDrawer so that touch events are processed.
+ *
+ * @see #lock()
+ */
+ public void unlock() {
+ mLocked = false;
+ }
+
+ /**
+ * Locks the SlidingDrawer so that touch events are ignores.
+ *
+ * @see #unlock()
+ */
+ public void lock() {
+ mLocked = true;
+ }
+
+ /**
+ * Indicates whether the drawer is currently fully opened.
+ *
+ * @return True if the drawer is opened, false otherwise.
+ */
+ public boolean isOpened() {
+ return mExpanded;
+ }
+
+ /**
+ * Indicates whether the drawer is scrolling or flinging.
+ *
+ * @return True if the drawer is scroller or flinging, false otherwise.
+ */
+ public boolean isMoving() {
+ return mTracking || mAnimating;
+ }
+
+ private class DrawerToggler implements OnClickListener {
+ public void onClick(View v) {
+ if (mLocked) {
+ return;
+ }
+ // mAllowSingleTap isn't relevant here; you're *always*
+ // allowed to open/close the drawer by clicking with the
+ // trackball.
+
+ if (mAnimateOnClick) {
+ animateToggle();
+ } else {
+ toggle();
+ }
+ }
+ }
+
+ private final Runnable mSlidingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ doAnimation();
+ }
+ };
+}
diff --git a/android/widget/SmartSelectSprite.java b/android/widget/SmartSelectSprite.java
new file mode 100644
index 0000000..27b93bc
--- /dev/null
+++ b/android/widget/SmartSelectSprite.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2017 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 static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.Shape;
+import android.util.TypedValue;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A utility class for creating and animating the Smart Select animation.
+ */
+final class SmartSelectSprite {
+
+ private static final int EXPAND_DURATION = 300;
+ private static final int CORNER_DURATION = 150;
+ private static final float STROKE_WIDTH_DP = 1.5F;
+
+ // GBLUE700
+ @ColorInt
+ private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6;
+
+ private final Interpolator mExpandInterpolator;
+ private final Interpolator mCornerInterpolator;
+ private final float mStrokeWidth;
+
+ private Animator mActiveAnimator = null;
+ private final Runnable mInvalidator;
+ @ColorInt
+ private final int mStrokeColor;
+
+ static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
+ .<RectF>comparingDouble(e -> e.bottom)
+ .thenComparingDouble(e -> e.left);
+
+ private Drawable mExistingDrawable = null;
+ private RectangleList mExistingRectangleList = null;
+
+ /**
+ * A rounded rectangle with a configurable corner radius and the ability to expand outside of
+ * its bounding rectangle and clip against it.
+ */
+ private static final class RoundedRectangleShape extends Shape {
+
+ private static final String PROPERTY_ROUND_RATIO = "roundRatio";
+
+ @Retention(SOURCE)
+ @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT})
+ private @interface ExpansionDirection {
+ int LEFT = 0;
+ int CENTER = 1;
+ int RIGHT = 2;
+ }
+
+ @Retention(SOURCE)
+ @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT})
+ private @interface RectangleBorderType {
+ /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */
+ int FIT = 0;
+ /**
+ * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that
+ * its edges no longer appear rounded.
+ */
+ int OVERSHOOT = 1;
+ }
+
+ private final float mStrokeWidth;
+ private final RectF mBoundingRectangle;
+ private float mRoundRatio = 1.0f;
+ private final @ExpansionDirection int mExpansionDirection;
+ private final @RectangleBorderType int mRectangleBorderType;
+
+ private final RectF mDrawRect = new RectF();
+ private final RectF mClipRect = new RectF();
+ private final Path mClipPath = new Path();
+
+ /** How far offset the left edge of the rectangle is from the bounding box. */
+ private float mLeftBoundary = 0;
+ /** How far offset the right edge of the rectangle is from the bounding box. */
+ private float mRightBoundary = 0;
+
+ private RoundedRectangleShape(
+ final RectF boundingRectangle,
+ final @ExpansionDirection int expansionDirection,
+ final @RectangleBorderType int rectangleBorderType,
+ final float strokeWidth) {
+ mBoundingRectangle = new RectF(boundingRectangle);
+ mExpansionDirection = expansionDirection;
+ mRectangleBorderType = rectangleBorderType;
+ mStrokeWidth = strokeWidth;
+
+ if (boundingRectangle.height() > boundingRectangle.width()) {
+ setRoundRatio(0.0f);
+ } else {
+ setRoundRatio(1.0f);
+ }
+ }
+
+ /*
+ * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be
+ * done in two passes. In this context, the wall is the bounding rectangle and in the first
+ * pass we need to draw the rounded rectangle (expanded and with a corner radius as per
+ * object properties) clipped by the bounding box. If the rounded rectangle expands outside
+ * of the bounding box, one more pass needs to be done, as there will now be a hole in the
+ * rounded rectangle where it "flattened" against the bounding box. In order to fill just
+ * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and
+ * this will connect the missing pieces.
+ */
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ final float cornerRadius = getCornerRadius();
+ final float adjustedCornerRadius = getAdjustedCornerRadius();
+
+ mDrawRect.set(mBoundingRectangle);
+ mDrawRect.left = mBoundingRectangle.left + mLeftBoundary;
+ mDrawRect.right = mBoundingRectangle.left + mRightBoundary;
+
+ if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
+ mDrawRect.left -= cornerRadius / 2;
+ mDrawRect.right -= cornerRadius / 2;
+ } else {
+ switch (mExpansionDirection) {
+ case ExpansionDirection.CENTER:
+ break;
+ case ExpansionDirection.LEFT:
+ mDrawRect.right += cornerRadius;
+ break;
+ case ExpansionDirection.RIGHT:
+ mDrawRect.left -= cornerRadius;
+ break;
+ }
+ }
+
+ canvas.save();
+ mClipRect.set(mBoundingRectangle);
+ mClipRect.inset(-mStrokeWidth, -mStrokeWidth);
+ canvas.clipRect(mClipRect);
+ canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint);
+ canvas.restore();
+
+ canvas.save();
+ mClipPath.reset();
+ mClipPath.addRoundRect(
+ mDrawRect,
+ adjustedCornerRadius,
+ adjustedCornerRadius,
+ Path.Direction.CW);
+ canvas.clipPath(mClipPath);
+ canvas.drawRect(mBoundingRectangle, paint);
+ canvas.restore();
+ }
+
+ public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) {
+ mRoundRatio = roundRatio;
+ }
+
+ public float getRoundRatio() {
+ return mRoundRatio;
+ }
+
+ private void setLeftBoundary(final float leftBoundary) {
+ mLeftBoundary = leftBoundary;
+ }
+
+ private void setRightBoundary(final float rightBoundary) {
+ mRightBoundary = rightBoundary;
+ }
+
+ private float getCornerRadius() {
+ return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height());
+ }
+
+ private float getAdjustedCornerRadius() {
+ return (getCornerRadius() * mRoundRatio);
+ }
+
+ private float getBoundingWidth() {
+ if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
+ return (int) (mBoundingRectangle.width() + getCornerRadius());
+ } else {
+ return mBoundingRectangle.width();
+ }
+ }
+
+ }
+
+ /**
+ * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose
+ * collective left and right boundary can be manipulated.
+ */
+ private static final class RectangleList extends Shape {
+
+ @Retention(SOURCE)
+ @IntDef({DisplayType.RECTANGLES, DisplayType.POLYGON})
+ private @interface DisplayType {
+ int RECTANGLES = 0;
+ int POLYGON = 1;
+ }
+
+ private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary";
+ private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary";
+
+ private final List<RoundedRectangleShape> mRectangles;
+ private final List<RoundedRectangleShape> mReversedRectangles;
+
+ private final Path mOutlinePolygonPath;
+ private @DisplayType int mDisplayType = DisplayType.RECTANGLES;
+
+ private RectangleList(final List<RoundedRectangleShape> rectangles) {
+ mRectangles = new LinkedList<>(rectangles);
+ mReversedRectangles = new LinkedList<>(rectangles);
+ Collections.reverse(mReversedRectangles);
+ mOutlinePolygonPath = generateOutlinePolygonPath(rectangles);
+ }
+
+ private void setLeftBoundary(final float leftBoundary) {
+ float boundarySoFar = getTotalWidth();
+ for (RoundedRectangleShape rectangle : mReversedRectangles) {
+ final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth();
+ if (leftBoundary < rectangleLeftBoundary) {
+ rectangle.setLeftBoundary(0);
+ } else if (leftBoundary > boundarySoFar) {
+ rectangle.setLeftBoundary(rectangle.getBoundingWidth());
+ } else {
+ rectangle.setLeftBoundary(
+ rectangle.getBoundingWidth() - boundarySoFar + leftBoundary);
+ }
+
+ boundarySoFar = rectangleLeftBoundary;
+ }
+ }
+
+ private void setRightBoundary(final float rightBoundary) {
+ float boundarySoFar = 0;
+ for (RoundedRectangleShape rectangle : mRectangles) {
+ final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar;
+ if (rectangleRightBoundary < rightBoundary) {
+ rectangle.setRightBoundary(rectangle.getBoundingWidth());
+ } else if (boundarySoFar > rightBoundary) {
+ rectangle.setRightBoundary(0);
+ } else {
+ rectangle.setRightBoundary(rightBoundary - boundarySoFar);
+ }
+
+ boundarySoFar = rectangleRightBoundary;
+ }
+ }
+
+ void setDisplayType(@DisplayType int displayType) {
+ mDisplayType = displayType;
+ }
+
+ private int getTotalWidth() {
+ int sum = 0;
+ for (RoundedRectangleShape rectangle : mRectangles) {
+ sum += rectangle.getBoundingWidth();
+ }
+ return sum;
+ }
+
+ @Override
+ public void draw(Canvas canvas, Paint paint) {
+ if (mDisplayType == DisplayType.POLYGON) {
+ drawPolygon(canvas, paint);
+ } else {
+ drawRectangles(canvas, paint);
+ }
+ }
+
+ private void drawRectangles(final Canvas canvas, final Paint paint) {
+ for (RoundedRectangleShape rectangle : mRectangles) {
+ rectangle.draw(canvas, paint);
+ }
+ }
+
+ private void drawPolygon(final Canvas canvas, final Paint paint) {
+ canvas.drawPath(mOutlinePolygonPath, paint);
+ }
+
+ private static Path generateOutlinePolygonPath(
+ final List<RoundedRectangleShape> rectangles) {
+ final Path path = new Path();
+ for (final RoundedRectangleShape shape : rectangles) {
+ final Path rectanglePath = new Path();
+ rectanglePath.addRect(shape.mBoundingRectangle, Path.Direction.CW);
+ path.op(rectanglePath, Path.Op.UNION);
+ }
+ return path;
+ }
+
+ }
+
+ /**
+ * @param context The {@link Context} in which the animation will run
+ * @param invalidator A {@link Runnable} which will be called every time the animation updates,
+ * indicating that the view drawing the animation should invalidate itself
+ */
+ SmartSelectSprite(final Context context, final Runnable invalidator) {
+ mExpandInterpolator = AnimationUtils.loadInterpolator(
+ context,
+ android.R.interpolator.fast_out_slow_in);
+ mCornerInterpolator = AnimationUtils.loadInterpolator(
+ context,
+ android.R.interpolator.fast_out_linear_in);
+ mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
+ mStrokeColor = getStrokeColor(context);
+ mInvalidator = Preconditions.checkNotNull(invalidator);
+ }
+
+ /**
+ * Performs the Smart Select animation on the view bound to this SmartSelectSprite.
+ *
+ * @param start The point from which the animation will start. Must be inside
+ * destinationRectangles.
+ * @param destinationRectangles The rectangles which the animation will fill out by its
+ * "selection" and finally join them into a single polygon. In
+ * order to get the correct visual behavior, these rectangles
+ * should be sorted according to {@link #RECTANGLE_COMPARATOR}.
+ * @param onAnimationEnd The callback which will be invoked once the whole animation
+ * completes.
+ * @throws IllegalArgumentException if the given start point is not in any of the
+ * destinationRectangles.
+ * @see #cancelAnimation()
+ */
+ public void startAnimation(
+ final PointF start,
+ final List<RectF> destinationRectangles,
+ final Runnable onAnimationEnd) throws IllegalArgumentException {
+ cancelAnimation();
+
+ final ValueAnimator.AnimatorUpdateListener updateListener =
+ valueAnimator -> mInvalidator.run();
+
+ final List<RoundedRectangleShape> shapes = new LinkedList<>();
+ final List<Animator> cornerAnimators = new LinkedList<>();
+
+ final RectF centerRectangle = destinationRectangles
+ .stream()
+ .filter((r) -> contains(r, start))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Center point is not inside any of the rectangles!"));
+
+ int startingOffset = 0;
+ for (RectF rectangle : destinationRectangles) {
+ if (rectangle.equals(centerRectangle)) {
+ break;
+ }
+ startingOffset += rectangle.width();
+ }
+
+ startingOffset += start.x - centerRectangle.left;
+
+ final float centerRectangleHalfHeight = centerRectangle.height() / 2;
+ final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
+ final float startingOffsetRight = startingOffset + centerRectangleHalfHeight;
+
+ final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
+ generateDirections(centerRectangle, destinationRectangles);
+
+ final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes =
+ generateBorderTypes(destinationRectangles);
+
+ int index = 0;
+
+ for (RectF rectangle : destinationRectangles) {
+ final RoundedRectangleShape shape = new RoundedRectangleShape(
+ rectangle,
+ expansionDirections[index],
+ rectangleBorderTypes[index],
+ mStrokeWidth);
+ cornerAnimators.add(createCornerAnimator(shape, updateListener));
+ shapes.add(shape);
+ index++;
+ }
+
+ final RectangleList rectangleList = new RectangleList(shapes);
+ final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);
+
+ final Paint paint = shapeDrawable.getPaint();
+ paint.setColor(mStrokeColor);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(mStrokeWidth);
+
+ mExistingRectangleList = rectangleList;
+ mExistingDrawable = shapeDrawable;
+
+ mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight,
+ cornerAnimators, updateListener,
+ onAnimationEnd);
+ mActiveAnimator.start();
+ }
+
+ private Animator createAnimator(
+ final RectangleList rectangleList,
+ final float startingOffsetLeft,
+ final float startingOffsetRight,
+ final List<Animator> cornerAnimators,
+ final ValueAnimator.AnimatorUpdateListener updateListener,
+ final Runnable onAnimationEnd) {
+ final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat(
+ rectangleList,
+ RectangleList.PROPERTY_RIGHT_BOUNDARY,
+ startingOffsetRight,
+ rectangleList.getTotalWidth());
+
+ final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat(
+ rectangleList,
+ RectangleList.PROPERTY_LEFT_BOUNDARY,
+ startingOffsetLeft,
+ 0);
+
+ rightBoundaryAnimator.setDuration(EXPAND_DURATION);
+ leftBoundaryAnimator.setDuration(EXPAND_DURATION);
+
+ rightBoundaryAnimator.addUpdateListener(updateListener);
+ leftBoundaryAnimator.addUpdateListener(updateListener);
+
+ rightBoundaryAnimator.setInterpolator(mExpandInterpolator);
+ leftBoundaryAnimator.setInterpolator(mExpandInterpolator);
+
+ final AnimatorSet cornerAnimator = new AnimatorSet();
+ cornerAnimator.playTogether(cornerAnimators);
+
+ final AnimatorSet boundaryAnimator = new AnimatorSet();
+ boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playSequentially(boundaryAnimator, cornerAnimator);
+
+ setUpAnimatorListener(animatorSet, onAnimationEnd);
+
+ return animatorSet;
+ }
+
+ private void setUpAnimatorListener(final Animator animator, final Runnable onAnimationEnd) {
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON);
+ mInvalidator.run();
+
+ onAnimationEnd.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+ });
+ }
+
+ private ObjectAnimator createCornerAnimator(
+ final RoundedRectangleShape shape,
+ final ValueAnimator.AnimatorUpdateListener listener) {
+ final ObjectAnimator animator = ObjectAnimator.ofFloat(
+ shape,
+ RoundedRectangleShape.PROPERTY_ROUND_RATIO,
+ shape.getRoundRatio(), 0.0F);
+ animator.setDuration(CORNER_DURATION);
+ animator.addUpdateListener(listener);
+ animator.setInterpolator(mCornerInterpolator);
+ return animator;
+ }
+
+ private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections(
+ final RectF centerRectangle, final List<RectF> rectangles) {
+ final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()];
+
+ final int centerRectangleIndex = rectangles.indexOf(centerRectangle);
+
+ for (int i = 0; i < centerRectangleIndex - 1; ++i) {
+ result[i] = RoundedRectangleShape.ExpansionDirection.LEFT;
+ }
+
+ if (rectangles.size() == 1) {
+ result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER;
+ } else if (centerRectangleIndex == 0) {
+ result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.LEFT;
+ } else if (centerRectangleIndex == rectangles.size() - 1) {
+ result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.RIGHT;
+ } else {
+ result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER;
+ }
+
+ for (int i = centerRectangleIndex + 1; i < result.length; ++i) {
+ result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT;
+ }
+
+ return result;
+ }
+
+ private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes(
+ final List<RectF> rectangles) {
+ final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()];
+
+ for (int i = 1; i < result.length - 1; ++i) {
+ result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT;
+ }
+
+ result[0] = RoundedRectangleShape.RectangleBorderType.FIT;
+ result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT;
+ return result;
+ }
+
+ private static float dpToPixel(final Context context, final float dp) {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp,
+ context.getResources().getDisplayMetrics());
+ }
+
+ @ColorInt
+ private static int getStrokeColor(final Context context) {
+ final TypedValue typedValue = new TypedValue();
+ final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{
+ android.R.attr.colorControlActivated});
+ final int result = array.getColor(0, DEFAULT_STROKE_COLOR);
+ array.recycle();
+ return result;
+ }
+
+ /**
+ * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
+ * the right boundary of the rectangle.
+ *
+ * @param rectangle the rectangle inside which the point should be to be considered "contained"
+ * @param point the point which will be tested
+ * @return whether the point is inside the rectangle (or on it's right boundary)
+ */
+ private static boolean contains(final RectF rectangle, final PointF point) {
+ final float x = point.x;
+ final float y = point.y;
+ return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top
+ && y <= rectangle.bottom;
+ }
+
+ private void removeExistingDrawables() {
+ mExistingDrawable = null;
+ mExistingRectangleList = null;
+ mInvalidator.run();
+ }
+
+ /**
+ * Cancels any active Smart Select animation that might be in progress.
+ */
+ public void cancelAnimation() {
+ if (mActiveAnimator != null) {
+ mActiveAnimator.cancel();
+ mActiveAnimator = null;
+ removeExistingDrawables();
+ }
+ }
+
+ public void draw(Canvas canvas) {
+ if (mExistingDrawable != null) {
+ mExistingDrawable.draw(canvas);
+ }
+ }
+
+}
diff --git a/android/widget/Space.java b/android/widget/Space.java
new file mode 100644
index 0000000..c4eaeb7
--- /dev/null
+++ b/android/widget/Space.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 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.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Space is a lightweight View subclass that may be used to create gaps between components
+ * in general purpose layouts.
+ */
+public final class Space extends View {
+ /**
+ * {@inheritDoc}
+ */
+ public Space(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ if (getVisibility() == VISIBLE) {
+ setVisibility(INVISIBLE);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Space(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Space(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Space(Context context) {
+ //noinspection NullableProblems
+ this(context, null);
+ }
+
+ /**
+ * Draw nothing.
+ *
+ * @param canvas an unused parameter.
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ }
+
+ /**
+ * Compare to: {@link View#getDefaultSize(int, int)}
+ * If mode is AT_MOST, return the child size instead of the parent size
+ * (unless it is too big).
+ */
+ private static int getDefaultSize2(int size, int measureSpec) {
+ int result = size;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Math.min(size, specSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(
+ getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec));
+ }
+}
diff --git a/android/widget/SpellChecker.java b/android/widget/SpellChecker.java
new file mode 100644
index 0000000..3d4f546
--- /dev/null
+++ b/android/widget/SpellChecker.java
@@ -0,0 +1,788 @@
+/*
+ * Copyright (C) 2011 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.text.Editable;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.WordIterator;
+import android.text.style.SpellCheckSpan;
+import android.text.style.SuggestionSpan;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SpellCheckerSession;
+import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+import android.view.textservice.TextServicesManager;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import java.text.BreakIterator;
+import java.util.Locale;
+
+
+/**
+ * Helper class for TextView. Bridge between the TextView and the Dictionary service.
+ *
+ * @hide
+ */
+public class SpellChecker implements SpellCheckerSessionListener {
+ private static final String TAG = SpellChecker.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ // No more than this number of words will be parsed on each iteration to ensure a minimum
+ // lock of the UI thread
+ public static final int MAX_NUMBER_OF_WORDS = 50;
+
+ // Rough estimate, such that the word iterator interval usually does not need to be shifted
+ public static final int AVERAGE_WORD_LENGTH = 7;
+
+ // When parsing, use a character window of that size. Will be shifted if needed
+ public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
+
+ // Pause between each spell check to keep the UI smooth
+ private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
+
+ private static final int MIN_SENTENCE_LENGTH = 50;
+
+ private static final int USE_SPAN_RANGE = -1;
+
+ private final TextView mTextView;
+
+ SpellCheckerSession mSpellCheckerSession;
+ // We assume that the sentence level spell check will always provide better results than words.
+ // Although word SC has a sequential option.
+ private boolean mIsSentenceSpellCheckSupported;
+ final int mCookie;
+
+ // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
+ // SpellCheckSpan has been recycled and can be-reused.
+ // Contains null SpellCheckSpans after index mLength.
+ private int[] mIds;
+ private SpellCheckSpan[] mSpellCheckSpans;
+ // The mLength first elements of the above arrays have been initialized
+ private int mLength;
+
+ // Parsers on chunk of text, cutting text into words that will be checked
+ private SpellParser[] mSpellParsers = new SpellParser[0];
+
+ private int mSpanSequenceCounter = 0;
+
+ private Locale mCurrentLocale;
+
+ // Shared by all SpellParsers. Cannot be shared with TextView since it may be used
+ // concurrently due to the asynchronous nature of onGetSuggestions.
+ private WordIterator mWordIterator;
+
+ private TextServicesManager mTextServicesManager;
+
+ private Runnable mSpellRunnable;
+
+ private static final int SUGGESTION_SPAN_CACHE_SIZE = 10;
+ private final LruCache<Long, SuggestionSpan> mSuggestionSpanCache =
+ new LruCache<Long, SuggestionSpan>(SUGGESTION_SPAN_CACHE_SIZE);
+
+ public SpellChecker(TextView textView) {
+ mTextView = textView;
+
+ // Arbitrary: these arrays will automatically double their sizes on demand
+ final int size = 1;
+ mIds = ArrayUtils.newUnpaddedIntArray(size);
+ mSpellCheckSpans = new SpellCheckSpan[mIds.length];
+
+ setLocale(mTextView.getSpellCheckerLocale());
+
+ mCookie = hashCode();
+ }
+
+ private void resetSession() {
+ closeSession();
+
+ mTextServicesManager = (TextServicesManager) mTextView.getContext().
+ getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
+ if (!mTextServicesManager.isSpellCheckerEnabled()
+ || mCurrentLocale == null
+ || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
+ mSpellCheckerSession = null;
+ } else {
+ mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
+ null /* Bundle not currently used by the textServicesManager */,
+ mCurrentLocale, this,
+ false /* means any available languages from current spell checker */);
+ mIsSentenceSpellCheckSupported = true;
+ }
+
+ // Restore SpellCheckSpans in pool
+ for (int i = 0; i < mLength; i++) {
+ mIds[i] = -1;
+ }
+ mLength = 0;
+
+ // Remove existing misspelled SuggestionSpans
+ mTextView.removeMisspelledSpans((Editable) mTextView.getText());
+ mSuggestionSpanCache.evictAll();
+ }
+
+ private void setLocale(Locale locale) {
+ mCurrentLocale = locale;
+
+ resetSession();
+
+ if (locale != null) {
+ // Change SpellParsers' wordIterator locale
+ mWordIterator = new WordIterator(locale);
+ }
+
+ // This class is the listener for locale change: warn other locale-aware objects
+ mTextView.onLocaleChanged();
+ }
+
+ /**
+ * @return true if a spell checker session has successfully been created. Returns false if not,
+ * for instance when spell checking has been disabled in settings.
+ */
+ private boolean isSessionActive() {
+ return mSpellCheckerSession != null;
+ }
+
+ public void closeSession() {
+ if (mSpellCheckerSession != null) {
+ mSpellCheckerSession.close();
+ }
+
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ mSpellParsers[i].stop();
+ }
+
+ if (mSpellRunnable != null) {
+ mTextView.removeCallbacks(mSpellRunnable);
+ }
+ }
+
+ private int nextSpellCheckSpanIndex() {
+ for (int i = 0; i < mLength; i++) {
+ if (mIds[i] < 0) return i;
+ }
+
+ mIds = GrowingArrayUtils.append(mIds, mLength, 0);
+ mSpellCheckSpans = GrowingArrayUtils.append(
+ mSpellCheckSpans, mLength, new SpellCheckSpan());
+ mLength++;
+ return mLength - 1;
+ }
+
+ private void addSpellCheckSpan(Editable editable, int start, int end) {
+ final int index = nextSpellCheckSpanIndex();
+ SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
+ editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spellCheckSpan.setSpellCheckInProgress(false);
+ mIds[index] = mSpanSequenceCounter++;
+ }
+
+ public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) {
+ // Recycle any removed SpellCheckSpan (from this code or during text edition)
+ for (int i = 0; i < mLength; i++) {
+ if (mSpellCheckSpans[i] == spellCheckSpan) {
+ mIds[i] = -1;
+ return;
+ }
+ }
+ }
+
+ public void onSelectionChanged() {
+ spellCheck();
+ }
+
+ public void spellCheck(int start, int end) {
+ if (DBG) {
+ Log.d(TAG, "Start spell-checking: " + start + ", " + end);
+ }
+ final Locale locale = mTextView.getSpellCheckerLocale();
+ final boolean isSessionActive = isSessionActive();
+ if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
+ setLocale(locale);
+ // Re-check the entire text
+ start = 0;
+ end = mTextView.getText().length();
+ } else {
+ final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
+ if (isSessionActive != spellCheckerActivated) {
+ // Spell checker has been turned of or off since last spellCheck
+ resetSession();
+ }
+ }
+
+ if (!isSessionActive) return;
+
+ // Find first available SpellParser from pool
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ final SpellParser spellParser = mSpellParsers[i];
+ if (spellParser.isFinished()) {
+ spellParser.parse(start, end);
+ return;
+ }
+ }
+
+ if (DBG) {
+ Log.d(TAG, "new spell parser.");
+ }
+ // No available parser found in pool, create a new one
+ SpellParser[] newSpellParsers = new SpellParser[length + 1];
+ System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
+ mSpellParsers = newSpellParsers;
+
+ SpellParser spellParser = new SpellParser();
+ mSpellParsers[length] = spellParser;
+ spellParser.parse(start, end);
+ }
+
+ private void spellCheck() {
+ if (mSpellCheckerSession == null) return;
+
+ Editable editable = (Editable) mTextView.getText();
+ final int selectionStart = Selection.getSelectionStart(editable);
+ final int selectionEnd = Selection.getSelectionEnd(editable);
+
+ TextInfo[] textInfos = new TextInfo[mLength];
+ int textInfosCount = 0;
+
+ for (int i = 0; i < mLength; i++) {
+ final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
+ if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
+
+ final int start = editable.getSpanStart(spellCheckSpan);
+ final int end = editable.getSpanEnd(spellCheckSpan);
+
+ // Do not check this word if the user is currently editing it
+ final boolean isEditing;
+
+ // Defer spell check when typing a word ending with a punctuation like an apostrophe
+ // which could end up being a mid-word punctuation.
+ if (selectionStart == end + 1
+ && WordIterator.isMidWordPunctuation(
+ mCurrentLocale, Character.codePointBefore(editable, end + 1))) {
+ isEditing = false;
+ } else if (mIsSentenceSpellCheckSupported) {
+ // Allow the overlap of the cursor and the first boundary of the spell check span
+ // no to skip the spell check of the following word because the
+ // following word will never be spell-checked even if the user finishes composing
+ isEditing = selectionEnd <= start || selectionStart > end;
+ } else {
+ isEditing = selectionEnd < start || selectionStart > end;
+ }
+ if (start >= 0 && end > start && isEditing) {
+ spellCheckSpan.setSpellCheckInProgress(true);
+ final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]);
+ textInfos[textInfosCount++] = textInfo;
+ if (DBG) {
+ Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = "
+ + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = "
+ + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
+ + selectionEnd + ", start = " + start + ", end = " + end);
+ }
+ }
+ }
+
+ if (textInfosCount > 0) {
+ if (textInfosCount < textInfos.length) {
+ TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
+ System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
+ textInfos = textInfosCopy;
+ }
+
+ if (mIsSentenceSpellCheckSupported) {
+ mSpellCheckerSession.getSentenceSuggestions(
+ textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
+ } else {
+ mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
+ false /* TODO Set sequentialWords to true for initial spell check */);
+ }
+ }
+ }
+
+ private SpellCheckSpan onGetSuggestionsInternal(
+ SuggestionsInfo suggestionsInfo, int offset, int length) {
+ if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
+ return null;
+ }
+ final Editable editable = (Editable) mTextView.getText();
+ final int sequenceNumber = suggestionsInfo.getSequence();
+ for (int k = 0; k < mLength; ++k) {
+ if (sequenceNumber == mIds[k]) {
+ final int attributes = suggestionsInfo.getSuggestionsAttributes();
+ final boolean isInDictionary =
+ ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
+ final boolean looksLikeTypo =
+ ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
+
+ final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
+ //TODO: we need to change that rule for results from a sentence-level spell
+ // checker that will probably be in dictionary.
+ if (!isInDictionary && looksLikeTypo) {
+ createMisspelledSuggestionSpan(
+ editable, suggestionsInfo, spellCheckSpan, offset, length);
+ } else {
+ // Valid word -- isInDictionary || !looksLikeTypo
+ if (mIsSentenceSpellCheckSupported) {
+ // Allow the spell checker to remove existing misspelled span by
+ // overwriting the span over the same place
+ final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
+ final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
+ final int start;
+ final int end;
+ if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
+ start = spellCheckSpanStart + offset;
+ end = start + length;
+ } else {
+ start = spellCheckSpanStart;
+ end = spellCheckSpanEnd;
+ }
+ if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart
+ && end > start) {
+ final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
+ final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
+ if (tempSuggestionSpan != null) {
+ if (DBG) {
+ Log.i(TAG, "Remove existing misspelled span. "
+ + editable.subSequence(start, end));
+ }
+ editable.removeSpan(tempSuggestionSpan);
+ mSuggestionSpanCache.remove(key);
+ }
+ }
+ }
+ }
+ return spellCheckSpan;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onGetSuggestions(SuggestionsInfo[] results) {
+ final Editable editable = (Editable) mTextView.getText();
+ for (int i = 0; i < results.length; ++i) {
+ final SpellCheckSpan spellCheckSpan =
+ onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
+ if (spellCheckSpan != null) {
+ // onSpellCheckSpanRemoved will recycle this span in the pool
+ editable.removeSpan(spellCheckSpan);
+ }
+ }
+ scheduleNewSpellCheck();
+ }
+
+ @Override
+ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
+ final Editable editable = (Editable) mTextView.getText();
+
+ for (int i = 0; i < results.length; ++i) {
+ final SentenceSuggestionsInfo ssi = results[i];
+ if (ssi == null) {
+ continue;
+ }
+ SpellCheckSpan spellCheckSpan = null;
+ for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
+ final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
+ if (suggestionsInfo == null) {
+ continue;
+ }
+ final int offset = ssi.getOffsetAt(j);
+ final int length = ssi.getLengthAt(j);
+ final SpellCheckSpan scs = onGetSuggestionsInternal(
+ suggestionsInfo, offset, length);
+ if (spellCheckSpan == null && scs != null) {
+ // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
+ // SentenceSuggestionsInfo. Removal is deferred after this loop.
+ spellCheckSpan = scs;
+ }
+ }
+ if (spellCheckSpan != null) {
+ // onSpellCheckSpanRemoved will recycle this span in the pool
+ editable.removeSpan(spellCheckSpan);
+ }
+ }
+ scheduleNewSpellCheck();
+ }
+
+ private void scheduleNewSpellCheck() {
+ if (DBG) {
+ Log.i(TAG, "schedule new spell check.");
+ }
+ if (mSpellRunnable == null) {
+ mSpellRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ final SpellParser spellParser = mSpellParsers[i];
+ if (!spellParser.isFinished()) {
+ spellParser.parse();
+ break; // run one spell parser at a time to bound running time
+ }
+ }
+ }
+ };
+ } else {
+ mTextView.removeCallbacks(mSpellRunnable);
+ }
+
+ mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
+ }
+
+ private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
+ SpellCheckSpan spellCheckSpan, int offset, int length) {
+ final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
+ final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
+ if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
+ return; // span was removed in the meantime
+
+ final int start;
+ final int end;
+ if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
+ start = spellCheckSpanStart + offset;
+ end = start + length;
+ } else {
+ start = spellCheckSpanStart;
+ end = spellCheckSpanEnd;
+ }
+
+ final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
+ String[] suggestions;
+ if (suggestionsCount > 0) {
+ suggestions = new String[suggestionsCount];
+ for (int i = 0; i < suggestionsCount; i++) {
+ suggestions[i] = suggestionsInfo.getSuggestionAt(i);
+ }
+ } else {
+ suggestions = ArrayUtils.emptyArray(String.class);
+ }
+
+ SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
+ SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
+ // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
+ // to share the logic of word level spell checker and sentence level spell checker
+ if (mIsSentenceSpellCheckSupported) {
+ final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
+ final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
+ if (tempSuggestionSpan != null) {
+ if (DBG) {
+ Log.i(TAG, "Cached span on the same position is cleard. "
+ + editable.subSequence(start, end));
+ }
+ editable.removeSpan(tempSuggestionSpan);
+ }
+ mSuggestionSpanCache.put(key, suggestionSpan);
+ }
+ editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mTextView.invalidateRegion(start, end, false /* No cursor involved */);
+ }
+
+ private class SpellParser {
+ private Object mRange = new Object();
+
+ public void parse(int start, int end) {
+ final int max = mTextView.length();
+ final int parseEnd;
+ if (end > max) {
+ Log.w(TAG, "Parse invalid region, from " + start + " to " + end);
+ parseEnd = max;
+ } else {
+ parseEnd = end;
+ }
+ if (parseEnd > start) {
+ setRangeSpan((Editable) mTextView.getText(), start, parseEnd);
+ parse();
+ }
+ }
+
+ public boolean isFinished() {
+ return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
+ }
+
+ public void stop() {
+ removeRangeSpan((Editable) mTextView.getText());
+ }
+
+ private void setRangeSpan(Editable editable, int start, int end) {
+ if (DBG) {
+ Log.d(TAG, "set next range span: " + start + ", " + end);
+ }
+ editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private void removeRangeSpan(Editable editable) {
+ if (DBG) {
+ Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
+ + editable.getSpanEnd(editable));
+ }
+ editable.removeSpan(mRange);
+ }
+
+ public void parse() {
+ Editable editable = (Editable) mTextView.getText();
+ // Iterate over the newly added text and schedule new SpellCheckSpans
+ final int start;
+ if (mIsSentenceSpellCheckSupported) {
+ // TODO: Find the start position of the sentence.
+ // Set span with the context
+ start = Math.max(
+ 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
+ } else {
+ start = editable.getSpanStart(mRange);
+ }
+
+ final int end = editable.getSpanEnd(mRange);
+
+ int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
+ mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
+
+ // Move back to the beginning of the current word, if any
+ int wordStart = mWordIterator.preceding(start);
+ int wordEnd;
+ if (wordStart == BreakIterator.DONE) {
+ wordEnd = mWordIterator.following(start);
+ if (wordEnd != BreakIterator.DONE) {
+ wordStart = mWordIterator.getBeginning(wordEnd);
+ }
+ } else {
+ wordEnd = mWordIterator.getEnd(wordStart);
+ }
+ if (wordEnd == BreakIterator.DONE) {
+ if (DBG) {
+ Log.i(TAG, "No more spell check.");
+ }
+ removeRangeSpan(editable);
+ return;
+ }
+
+ // We need to expand by one character because we want to include the spans that
+ // end/start at position start/end respectively.
+ SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
+ SpellCheckSpan.class);
+ SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
+ SuggestionSpan.class);
+
+ int wordCount = 0;
+ boolean scheduleOtherSpellCheck = false;
+
+ if (mIsSentenceSpellCheckSupported) {
+ if (wordIteratorWindowEnd < end) {
+ if (DBG) {
+ Log.i(TAG, "schedule other spell check.");
+ }
+ // Several batches needed on that region. Cut after last previous word
+ scheduleOtherSpellCheck = true;
+ }
+ int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
+ boolean correct = spellCheckEnd != BreakIterator.DONE;
+ if (correct) {
+ spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
+ correct = spellCheckEnd != BreakIterator.DONE;
+ }
+ if (!correct) {
+ if (DBG) {
+ Log.i(TAG, "Incorrect range span.");
+ }
+ removeRangeSpan(editable);
+ return;
+ }
+ do {
+ // TODO: Find the start position of the sentence.
+ int spellCheckStart = wordStart;
+ boolean createSpellCheckSpan = true;
+ // Cancel or merge overlapped spell check spans
+ for (int i = 0; i < mLength; ++i) {
+ final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
+ if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
+ continue;
+ }
+ final int spanStart = editable.getSpanStart(spellCheckSpan);
+ final int spanEnd = editable.getSpanEnd(spellCheckSpan);
+ if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
+ // No need to merge
+ continue;
+ }
+ if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
+ // There is a completely overlapped spell check span
+ // skip this span
+ createSpellCheckSpan = false;
+ if (DBG) {
+ Log.i(TAG, "The range is overrapped. Skip spell check.");
+ }
+ break;
+ }
+ // This spellCheckSpan is replaced by the one we are creating
+ editable.removeSpan(spellCheckSpan);
+ spellCheckStart = Math.min(spanStart, spellCheckStart);
+ spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
+ }
+
+ if (DBG) {
+ Log.d(TAG, "addSpellCheckSpan: "
+ + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
+ + ", next = " + scheduleOtherSpellCheck + "\n"
+ + editable.subSequence(spellCheckStart, spellCheckEnd));
+ }
+
+ // Stop spell checking when there are no characters in the range.
+ if (spellCheckEnd < start) {
+ break;
+ }
+ if (spellCheckEnd <= spellCheckStart) {
+ Log.w(TAG, "Trying to spellcheck invalid region, from "
+ + start + " to " + end);
+ break;
+ }
+ if (createSpellCheckSpan) {
+ addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
+ }
+ } while (false);
+ wordStart = spellCheckEnd;
+ } else {
+ while (wordStart <= end) {
+ if (wordEnd >= start && wordEnd > wordStart) {
+ if (wordCount >= MAX_NUMBER_OF_WORDS) {
+ scheduleOtherSpellCheck = true;
+ break;
+ }
+ // A new word has been created across the interval boundaries with this
+ // edit. The previous spans (that ended on start / started on end) are
+ // not valid anymore and must be removed.
+ if (wordStart < start && wordEnd > start) {
+ removeSpansAt(editable, start, spellCheckSpans);
+ removeSpansAt(editable, start, suggestionSpans);
+ }
+
+ if (wordStart < end && wordEnd > end) {
+ removeSpansAt(editable, end, spellCheckSpans);
+ removeSpansAt(editable, end, suggestionSpans);
+ }
+
+ // Do not create new boundary spans if they already exist
+ boolean createSpellCheckSpan = true;
+ if (wordEnd == start) {
+ for (int i = 0; i < spellCheckSpans.length; i++) {
+ final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
+ if (spanEnd == start) {
+ createSpellCheckSpan = false;
+ break;
+ }
+ }
+ }
+
+ if (wordStart == end) {
+ for (int i = 0; i < spellCheckSpans.length; i++) {
+ final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
+ if (spanStart == end) {
+ createSpellCheckSpan = false;
+ break;
+ }
+ }
+ }
+
+ if (createSpellCheckSpan) {
+ addSpellCheckSpan(editable, wordStart, wordEnd);
+ }
+ wordCount++;
+ }
+
+ // iterate word by word
+ int originalWordEnd = wordEnd;
+ wordEnd = mWordIterator.following(wordEnd);
+ if ((wordIteratorWindowEnd < end) &&
+ (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
+ wordIteratorWindowEnd =
+ Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
+ mWordIterator.setCharSequence(
+ editable, originalWordEnd, wordIteratorWindowEnd);
+ wordEnd = mWordIterator.following(originalWordEnd);
+ }
+ if (wordEnd == BreakIterator.DONE) break;
+ wordStart = mWordIterator.getBeginning(wordEnd);
+ if (wordStart == BreakIterator.DONE) {
+ break;
+ }
+ }
+ }
+
+ if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) {
+ // Update range span: start new spell check from last wordStart
+ setRangeSpan(editable, wordStart, end);
+ } else {
+ removeRangeSpan(editable);
+ }
+
+ spellCheck();
+ }
+
+ private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
+ final int length = spans.length;
+ for (int i = 0; i < length; i++) {
+ final T span = spans[i];
+ final int start = editable.getSpanStart(span);
+ if (start > offset) continue;
+ final int end = editable.getSpanEnd(span);
+ if (end < offset) continue;
+ editable.removeSpan(span);
+ }
+ }
+ }
+
+ public static boolean haveWordBoundariesChanged(final Editable editable, final int start,
+ final int end, final int spanStart, final int spanEnd) {
+ final boolean haveWordBoundariesChanged;
+ if (spanEnd != start && spanStart != end) {
+ haveWordBoundariesChanged = true;
+ if (DBG) {
+ Log.d(TAG, "(1) Text inside the span has been modified. Remove.");
+ }
+ } else if (spanEnd == start && start < editable.length()) {
+ final int codePoint = Character.codePointAt(editable, start);
+ haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
+ if (DBG) {
+ Log.d(TAG, "(2) Characters have been appended to the spanned text. "
+ + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint)
+ + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
+ + start);
+ }
+ } else if (spanStart == end && end > 0) {
+ final int codePoint = Character.codePointBefore(editable, end);
+ haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
+ if (DBG) {
+ Log.d(TAG, "(3) Characters have been prepended to the spanned text. "
+ + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint)
+ + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
+ + end);
+ }
+ } else {
+ if (DBG) {
+ Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep.");
+ }
+ haveWordBoundariesChanged = false;
+ }
+ return haveWordBoundariesChanged;
+ }
+}
diff --git a/android/widget/Spinner.java b/android/widget/Spinner.java
new file mode 100644
index 0000000..ddf0e74
--- /dev/null
+++ b/android/widget/Spinner.java
@@ -0,0 +1,1299 @@
+/*
+ * 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.DrawableRes;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+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.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.PopupWindow.OnDismissListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * 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.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.</p>
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownSelector
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ * @attr ref android.R.styleable#Spinner_gravity
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ * @attr ref android.R.styleable#Spinner_prompt
+ * @attr ref android.R.styleable#Spinner_spinnerMode
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+@Widget
+public class Spinner extends AbsSpinner implements OnClickListener {
+ private static final String TAG = "Spinner";
+
+ // Only measure this many items to get a decent max width.
+ private static final int MAX_ITEMS_MEASURED = 15;
+
+ /**
+ * Use a dialog window for selecting spinner options.
+ */
+ public static final int MODE_DIALOG = 0;
+
+ /**
+ * Use a dropdown anchored to the Spinner for selecting spinner options.
+ */
+ public static final int MODE_DROPDOWN = 1;
+
+ /**
+ * Use the theme-supplied value to select the dropdown mode.
+ */
+ private static final int MODE_THEME = -1;
+
+ private final Rect mTempRect = new Rect();
+
+ /** Context used to inflate the popup window or dialog. */
+ private final Context mPopupContext;
+
+ /** Forwarding listener used to implement drag-to-open. */
+ private ForwardingListener mForwardingListener;
+
+ /** Temporary holder for setAdapter() calls from the super constructor. */
+ private SpinnerAdapter mTempAdapter;
+
+ private SpinnerPopup mPopup;
+ int mDropDownWidth;
+
+ private int mGravity;
+ private boolean mDisableChildrenWhenDisabled;
+
+ /**
+ * Constructs a new spinner with the given context's theme.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ */
+ public Spinner(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructs a new spinner with the given context's theme and the supplied
+ * mode of displaying choices. <code>mode</code> may be one of
+ * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param mode Constant describing how the user will select choices from
+ * the spinner.
+ *
+ * @see #MODE_DIALOG
+ * @see #MODE_DROPDOWN
+ */
+ public Spinner(Context context, int mode) {
+ this(context, null, com.android.internal.R.attr.spinnerStyle, mode);
+ }
+
+ /**
+ * Constructs a new spinner with the given context's theme and the supplied
+ * attribute set.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public Spinner(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.spinnerStyle);
+ }
+
+ /**
+ * Constructs a new spinner with the given context's theme, the supplied
+ * attribute set, and default style attribute.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ */
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0, MODE_THEME);
+ }
+
+ /**
+ * Constructs a new spinner with the given context's theme, the supplied
+ * attribute set, and default style attribute. <code>mode</code> may be one
+ * of {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
+ * user will select choices from the spinner.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for defaults.
+ * @param mode Constant describing how the user will select choices from the
+ * spinner.
+ *
+ * @see #MODE_DIALOG
+ * @see #MODE_DROPDOWN
+ */
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
+ this(context, attrs, defStyleAttr, 0, mode);
+ }
+
+ /**
+ * Constructs a new spinner with the given context's theme, the supplied
+ * attribute set, and default styles. <code>mode</code> may be one of
+ * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
+ * user will select choices from the spinner.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme.
+ * Can be 0 to not look for defaults.
+ * @param mode Constant describing how the user will select choices from
+ * the spinner.
+ *
+ * @see #MODE_DIALOG
+ * @see #MODE_DROPDOWN
+ */
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
+ int mode) {
+ this(context, attrs, defStyleAttr, defStyleRes, mode, null);
+ }
+
+ /**
+ * Constructs a new spinner with the given context, the supplied attribute
+ * set, default styles, popup mode (one of {@link #MODE_DIALOG} or
+ * {@link #MODE_DROPDOWN}), and the theme against which the popup should be
+ * inflated.
+ *
+ * @param context The context against which the view is inflated, which
+ * provides access to the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default
+ * values for the view. Can be 0 to not look for
+ * defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme.
+ * Can be 0 to not look for defaults.
+ * @param mode Constant describing how the user will select choices from
+ * the spinner.
+ * @param popupTheme The theme against which the dialog or dropdown popup
+ * should be inflated. May be {@code null} to use the
+ * view theme. If set, this will override any value
+ * specified by
+ * {@link android.R.styleable#Spinner_popupTheme}.
+ *
+ * @see #MODE_DIALOG
+ * @see #MODE_DROPDOWN
+ */
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode,
+ Theme popupTheme) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
+
+ if (popupTheme != null) {
+ mPopupContext = new ContextThemeWrapper(context, popupTheme);
+ } else {
+ final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
+ if (popupThemeResId != 0) {
+ mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
+ } else {
+ mPopupContext = context;
+ }
+ }
+
+ if (mode == MODE_THEME) {
+ mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG);
+ }
+
+ switch (mode) {
+ case MODE_DIALOG: {
+ mPopup = new DialogPopup();
+ mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt));
+ break;
+ }
+
+ case MODE_DROPDOWN: {
+ final DropdownPopup popup = new DropdownPopup(
+ mPopupContext, attrs, defStyleAttr, defStyleRes);
+ final TypedArray pa = mPopupContext.obtainStyledAttributes(
+ attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
+ mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_dropDownWidth,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ if (pa.hasValueOrEmpty(R.styleable.Spinner_dropDownSelector)) {
+ popup.setListSelector(pa.getDrawable(
+ R.styleable.Spinner_dropDownSelector));
+ }
+ popup.setBackgroundDrawable(pa.getDrawable(R.styleable.Spinner_popupBackground));
+ popup.setPromptText(a.getString(R.styleable.Spinner_prompt));
+ pa.recycle();
+
+ mPopup = popup;
+ mForwardingListener = new ForwardingListener(this) {
+ @Override
+ public ShowableListMenu getPopup() {
+ return popup;
+ }
+
+ @Override
+ public boolean onForwardingStarted() {
+ if (!mPopup.isShowing()) {
+ mPopup.show(getTextDirection(), getTextAlignment());
+ }
+ return true;
+ }
+ };
+ break;
+ }
+ }
+
+ mGravity = a.getInt(R.styleable.Spinner_gravity, Gravity.CENTER);
+ mDisableChildrenWhenDisabled = a.getBoolean(
+ R.styleable.Spinner_disableChildrenWhenDisabled, false);
+
+ a.recycle();
+
+ // Base constructor can call setAdapter before we initialize mPopup.
+ // Finish setting things up if this happened.
+ if (mTempAdapter != null) {
+ setAdapter(mTempAdapter);
+ mTempAdapter = null;
+ }
+ }
+
+ /**
+ * @return the context used to inflate the Spinner's popup or dialog window
+ */
+ public Context getPopupContext() {
+ return mPopupContext;
+ }
+
+ /**
+ * Set the background drawable for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+ *
+ * @param background Background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public void setPopupBackgroundDrawable(Drawable background) {
+ if (!(mPopup instanceof DropdownPopup)) {
+ Log.e(TAG, "setPopupBackgroundDrawable: incompatible spinner mode; ignoring...");
+ return;
+ }
+ mPopup.setBackgroundDrawable(background);
+ }
+
+ /**
+ * Set the background drawable for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+ *
+ * @param resId Resource ID of a background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public void setPopupBackgroundResource(@DrawableRes int resId) {
+ setPopupBackgroundDrawable(getPopupContext().getDrawable(resId));
+ }
+
+ /**
+ * Get the background drawable for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; other modes will return null.
+ *
+ * @return background Background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public Drawable getPopupBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public boolean isPopupShowing() {
+ return (mPopup != null) && mPopup.isShowing();
+ }
+
+ /**
+ * Set a vertical offset in pixels for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+ *
+ * @param pixels Vertical offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public void setDropDownVerticalOffset(int pixels) {
+ mPopup.setVerticalOffset(pixels);
+ }
+
+ /**
+ * Get the configured vertical offset in pixels for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
+ *
+ * @return Vertical offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public int getDropDownVerticalOffset() {
+ return mPopup.getVerticalOffset();
+ }
+
+ /**
+ * Set a horizontal offset in pixels for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+ *
+ * @param pixels Horizontal offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public void setDropDownHorizontalOffset(int pixels) {
+ mPopup.setHorizontalOffset(pixels);
+ }
+
+ /**
+ * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
+ * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
+ *
+ * @return Horizontal offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public int getDropDownHorizontalOffset() {
+ return mPopup.getHorizontalOffset();
+ }
+
+ /**
+ * Set the width of the spinner's popup window of choices in pixels. This value
+ * may also be set to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+ * to match the width of the Spinner itself, or
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+ * of contained dropdown list items.
+ *
+ * <p>Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.</p>
+ *
+ * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ */
+ public void setDropDownWidth(int pixels) {
+ if (!(mPopup instanceof DropdownPopup)) {
+ Log.e(TAG, "Cannot set dropdown width for MODE_DIALOG, ignoring");
+ return;
+ }
+ mDropDownWidth = pixels;
+ }
+
+ /**
+ * Get the configured width of the spinner's popup window of choices in pixels.
+ * The returned value may also be {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+ * meaning the popup window will match the width of the Spinner itself, or
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+ * of contained dropdown list items.
+ *
+ * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ */
+ public int getDropDownWidth() {
+ return mDropDownWidth;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (mDisableChildrenWhenDisabled) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).setEnabled(enabled);
+ }
+ }
+ }
+
+ /**
+ * Describes how the selected item view is positioned. Currently only the horizontal component
+ * is used. The default is determined by the current theme.
+ *
+ * @param gravity See {@link android.view.Gravity}
+ *
+ * @attr ref android.R.styleable#Spinner_gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.START;
+ }
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Describes how the selected item view is positioned. The default is determined by the
+ * current theme.
+ *
+ * @return A {@link android.view.Gravity Gravity} value
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Sets the {@link SpinnerAdapter} used to provide the data which backs
+ * this Spinner.
+ * <p>
+ * If this Spinner has a popup theme set in XML via the
+ * {@link android.R.styleable#Spinner_popupTheme popupTheme} attribute, the
+ * adapter should inflate drop-down views using the same theme. The easiest
+ * way to achieve this is by using {@link #getPopupContext()} to obtain a
+ * layout inflater for use in
+ * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}.
+ * <p>
+ * Spinner overrides {@link Adapter#getViewTypeCount()} on the
+ * Adapter associated with this view. Calling
+ * {@link Adapter#getItemViewType(int) getItemViewType(int)} on the object
+ * returned from {@link #getAdapter()} will always return 0. Calling
+ * {@link Adapter#getViewTypeCount() getViewTypeCount()} will always return
+ * 1. On API {@link Build.VERSION_CODES#LOLLIPOP} and above, attempting to set an
+ * adapter with more than one view type will throw an
+ * {@link IllegalArgumentException}.
+ *
+ * @param adapter the adapter to set
+ *
+ * @see AbsSpinner#setAdapter(SpinnerAdapter)
+ * @throws IllegalArgumentException if the adapter has more than one view
+ * type
+ */
+ @Override
+ public void setAdapter(SpinnerAdapter adapter) {
+ // The super constructor may call setAdapter before we're prepared.
+ // Postpone doing anything until we've finished construction.
+ if (mPopup == null) {
+ mTempAdapter = adapter;
+ return;
+ }
+
+ super.setAdapter(adapter);
+
+ mRecycler.clear();
+
+ final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP
+ && adapter != null && adapter.getViewTypeCount() != 1) {
+ throw new IllegalArgumentException("Spinner adapter view type count must be 1");
+ }
+
+ final Context popupContext = mPopupContext == null ? mContext : mPopupContext;
+ mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));
+ }
+
+ @Override
+ public int getBaseline() {
+ View child = null;
+
+ if (getChildCount() > 0) {
+ child = getChildAt(0);
+ } else if (mAdapter != null && mAdapter.getCount() > 0) {
+ child = makeView(0, false);
+ mRecycler.put(0, child);
+ }
+
+ if (child != null) {
+ final int childBaseline = child.getBaseline();
+ return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ }
+
+ /**
+ * <p>A spinner does not support item click events. Calling this method
+ * will raise an exception.</p>
+ * <p>Instead use {@link AdapterView#setOnItemSelectedListener}.
+ *
+ * @param l this listener will be ignored
+ */
+ @Override
+ public void setOnItemClickListener(OnItemClickListener l) {
+ throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
+ }
+
+ /**
+ * @hide internal use only
+ */
+ public void setOnItemClickListenerInt(OnItemClickListener l) {
+ super.setOnItemClickListener(l);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
+ return true;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
+ final int measuredWidth = getMeasuredWidth();
+ setMeasuredDimension(Math.min(Math.max(measuredWidth,
+ measureContentWidth(getAdapter(), getBackground())),
+ MeasureSpec.getSize(widthMeasureSpec)),
+ getMeasuredHeight());
+ }
+ }
+
+ /**
+ * @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 means 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 position it
+ mFirstPosition = mSelectedPosition;
+
+ if (mAdapter != null) {
+ View sel = makeView(mSelectedPosition, true);
+ int width = sel.getMeasuredWidth();
+ int selectedOffset = childrenLeft;
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
+ break;
+ case Gravity.RIGHT:
+ selectedOffset = childrenLeft + childrenWidth - width;
+ break;
+ }
+ 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
+ * @param addChild true to add the child to the spinner, false to obtain and configure only.
+ * @return A view for the given position
+ */
+ private View makeView(int position, boolean addChild) {
+ View child;
+
+ if (!mDataChanged) {
+ child = mRecycler.get(position);
+ if (child != null) {
+ // Position the view
+ setUpChild(child, addChild);
+
+ return child;
+ }
+ }
+
+ // Nothing found in the recycler -- ask the adapter for a view
+ child = mAdapter.getView(position, null, this);
+
+ // Position the view
+ setUpChild(child, addChild);
+
+ 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 addChild true if the child should be added to the Spinner during setup
+ */
+ private void setUpChild(View child, boolean addChild) {
+
+ // 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());
+ if (mDisableChildrenWhenDisabled) {
+ child.setEnabled(isEnabled());
+ }
+
+ // 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
+ + ((getMeasuredHeight() - 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);
+
+ if (!addChild) {
+ removeViewInLayout(child);
+ }
+ }
+
+ @Override
+ public boolean performClick() {
+ boolean handled = super.performClick();
+
+ if (!handled) {
+ handled = true;
+
+ if (!mPopup.isShowing()) {
+ mPopup.show(getTextDirection(), getTextAlignment());
+ }
+ }
+
+ return handled;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ setSelection(which);
+ dialog.dismiss();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Spinner.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (mAdapter != null) {
+ info.setCanOpenPopup(true);
+ }
+ }
+
+ /**
+ * Sets the prompt to display when the dialog is shown.
+ * @param prompt the prompt to set
+ */
+ public void setPrompt(CharSequence prompt) {
+ mPopup.setPromptText(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) {
+ setPrompt(getContext().getText(promptId));
+ }
+
+ /**
+ * @return The prompt to display when the dialog is shown
+ */
+ public CharSequence getPrompt() {
+ return mPopup.getHintText();
+ }
+
+ int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
+ if (adapter == null) {
+ return 0;
+ }
+
+ int width = 0;
+ View itemView = null;
+ int itemType = 0;
+ final int widthMeasureSpec =
+ MeasureSpec.makeSafeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
+ final int heightMeasureSpec =
+ MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);
+
+ // Make sure the number of items we'll measure is capped. If it's a huge data set
+ // with wildly varying sizes, oh well.
+ int start = Math.max(0, getSelectedItemPosition());
+ final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
+ final int count = end - start;
+ start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
+ for (int i = start; i < end; i++) {
+ final int positionType = adapter.getItemViewType(i);
+ if (positionType != itemType) {
+ itemType = positionType;
+ itemView = null;
+ }
+ itemView = adapter.getView(i, itemView, this);
+ if (itemView.getLayoutParams() == null) {
+ itemView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ itemView.measure(widthMeasureSpec, heightMeasureSpec);
+ width = Math.max(width, itemView.getMeasuredWidth());
+ }
+
+ // Add background padding to measured width
+ if (background != null) {
+ background.getPadding(mTempRect);
+ width += mTempRect.left + mTempRect.right;
+ }
+
+ return width;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+ ss.showDropdown = mPopup != null && mPopup.isShowing();
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (ss.showDropdown) {
+ ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (!mPopup.isShowing()) {
+ mPopup.show(getTextDirection(), getTextAlignment());
+ }
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ vto.removeOnGlobalLayoutListener(this);
+ }
+ }
+ };
+ vto.addOnGlobalLayoutListener(listener);
+ }
+ }
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ static class SavedState extends AbsSpinner.SavedState {
+ boolean showDropdown;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ showDropdown = in.readByte() != 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeByte((byte) (showDropdown ? 1 : 0));
+ }
+
+ 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];
+ }
+ };
+ }
+
+ /**
+ * <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;
+ private ListAdapter mListAdapter;
+
+ /**
+ * Creates a new ListAdapter wrapper for the specified adapter.
+ *
+ * @param adapter the SpinnerAdapter to transform into a ListAdapter
+ * @param dropDownTheme the theme against which to inflate drop-down
+ * views, may be {@null} to use default theme
+ */
+ public DropDownAdapter(@Nullable SpinnerAdapter adapter,
+ @Nullable Resources.Theme dropDownTheme) {
+ mAdapter = adapter;
+
+ if (adapter instanceof ListAdapter) {
+ mListAdapter = (ListAdapter) adapter;
+ }
+
+ if (dropDownTheme != null && adapter instanceof ThemedSpinnerAdapter) {
+ final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter;
+ if (themedAdapter.getDropDownViewTheme() == null) {
+ themedAdapter.setDropDownViewTheme(dropDownTheme);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
+ * Otherwise, return true.
+ */
+ public boolean areAllItemsEnabled() {
+ final ListAdapter adapter = mListAdapter;
+ if (adapter != null) {
+ return adapter.areAllItemsEnabled();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
+ * Otherwise, return true.
+ */
+ public boolean isEnabled(int position) {
+ final ListAdapter adapter = mListAdapter;
+ if (adapter != null) {
+ return adapter.isEnabled(position);
+ } else {
+ return true;
+ }
+ }
+
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+ }
+
+ /**
+ * Implements some sort of popup selection interface for selecting a spinner option.
+ * Allows for different spinner modes.
+ */
+ private interface SpinnerPopup {
+ public void setAdapter(ListAdapter adapter);
+
+ /**
+ * Show the popup
+ */
+ public void show(int textDirection, int textAlignment);
+
+ /**
+ * Dismiss the popup
+ */
+ public void dismiss();
+
+ /**
+ * @return true if the popup is showing, false otherwise.
+ */
+ public boolean isShowing();
+
+ /**
+ * Set hint text to be displayed to the user. This should provide
+ * a description of the choice being made.
+ * @param hintText Hint text to set.
+ */
+ public void setPromptText(CharSequence hintText);
+ public CharSequence getHintText();
+
+ public void setBackgroundDrawable(Drawable bg);
+ public void setVerticalOffset(int px);
+ public void setHorizontalOffset(int px);
+ public Drawable getBackground();
+ public int getVerticalOffset();
+ public int getHorizontalOffset();
+ }
+
+ private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
+ private AlertDialog mPopup;
+ private ListAdapter mListAdapter;
+ private CharSequence mPrompt;
+
+ public void dismiss() {
+ if (mPopup != null) {
+ mPopup.dismiss();
+ mPopup = null;
+ }
+ }
+
+ public boolean isShowing() {
+ return mPopup != null ? mPopup.isShowing() : false;
+ }
+
+ public void setAdapter(ListAdapter adapter) {
+ mListAdapter = adapter;
+ }
+
+ public void setPromptText(CharSequence hintText) {
+ mPrompt = hintText;
+ }
+
+ public CharSequence getHintText() {
+ return mPrompt;
+ }
+
+ public void show(int textDirection, int textAlignment) {
+ if (mListAdapter == null) {
+ return;
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(getPopupContext());
+ if (mPrompt != null) {
+ builder.setTitle(mPrompt);
+ }
+ mPopup = builder.setSingleChoiceItems(mListAdapter,
+ getSelectedItemPosition(), this).create();
+ final ListView listView = mPopup.getListView();
+ listView.setTextDirection(textDirection);
+ listView.setTextAlignment(textAlignment);
+ mPopup.show();
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ setSelection(which);
+ if (mOnItemClickListener != null) {
+ performItemClick(null, which, mListAdapter.getItemId(which));
+ }
+ dismiss();
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable bg) {
+ Log.e(TAG, "Cannot set popup background for MODE_DIALOG, ignoring");
+ }
+
+ @Override
+ public void setVerticalOffset(int px) {
+ Log.e(TAG, "Cannot set vertical offset for MODE_DIALOG, ignoring");
+ }
+
+ @Override
+ public void setHorizontalOffset(int px) {
+ Log.e(TAG, "Cannot set horizontal offset for MODE_DIALOG, ignoring");
+ }
+
+ @Override
+ public Drawable getBackground() {
+ return null;
+ }
+
+ @Override
+ public int getVerticalOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getHorizontalOffset() {
+ return 0;
+ }
+ }
+
+ private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
+ private CharSequence mHintText;
+ private ListAdapter mAdapter;
+
+ public DropdownPopup(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setAnchorView(Spinner.this);
+ setModal(true);
+ setPromptPosition(POSITION_PROMPT_ABOVE);
+ setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
+ Spinner.this.setSelection(position);
+ if (mOnItemClickListener != null) {
+ Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
+ }
+ dismiss();
+ }
+ });
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ super.setAdapter(adapter);
+ mAdapter = adapter;
+ }
+
+ public CharSequence getHintText() {
+ return mHintText;
+ }
+
+ public void setPromptText(CharSequence hintText) {
+ // Hint text is ignored for dropdowns, but maintain it here.
+ mHintText = hintText;
+ }
+
+ void computeContentWidth() {
+ final Drawable background = getBackground();
+ int hOffset = 0;
+ if (background != null) {
+ background.getPadding(mTempRect);
+ hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
+ } else {
+ mTempRect.left = mTempRect.right = 0;
+ }
+
+ final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
+ final int spinnerPaddingRight = Spinner.this.getPaddingRight();
+ final int spinnerWidth = Spinner.this.getWidth();
+
+ if (mDropDownWidth == WRAP_CONTENT) {
+ int contentWidth = measureContentWidth(
+ (SpinnerAdapter) mAdapter, getBackground());
+ final int contentWidthLimit = mContext.getResources()
+ .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
+ if (contentWidth > contentWidthLimit) {
+ contentWidth = contentWidthLimit;
+ }
+ setContentWidth(Math.max(
+ contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
+ } else if (mDropDownWidth == MATCH_PARENT) {
+ setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
+ } else {
+ setContentWidth(mDropDownWidth);
+ }
+
+ if (isLayoutRtl()) {
+ hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
+ } else {
+ hOffset += spinnerPaddingLeft;
+ }
+ setHorizontalOffset(hOffset);
+ }
+
+ public void show(int textDirection, int textAlignment) {
+ final boolean wasShowing = isShowing();
+
+ computeContentWidth();
+
+ setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+ super.show();
+ final ListView listView = getListView();
+ listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ listView.setTextDirection(textDirection);
+ listView.setTextAlignment(textAlignment);
+ setSelection(Spinner.this.getSelectedItemPosition());
+
+ if (wasShowing) {
+ // Skip setting up the layout/dismiss listener below. If we were previously
+ // showing it will still stick around.
+ return;
+ }
+
+ // Make sure we hide if our anchor goes away.
+ // TODO: This might be appropriate to push all the way down to PopupWindow,
+ // but it may have other side effects to investigate first. (Text editing handles, etc.)
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ final OnGlobalLayoutListener layoutListener = new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (!Spinner.this.isVisibleToUser()) {
+ dismiss();
+ } else {
+ computeContentWidth();
+
+ // Use super.show here to update; we don't want to move the selected
+ // position or adjust other things that would be reset otherwise.
+ DropdownPopup.super.show();
+ }
+ }
+ };
+ vto.addOnGlobalLayoutListener(layoutListener);
+ setOnDismissListener(new OnDismissListener() {
+ @Override public void onDismiss() {
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ vto.removeOnGlobalLayoutListener(layoutListener);
+ }
+ }
+ });
+ }
+ }
+ }
+
+}
diff --git a/android/widget/SpinnerAdapter.java b/android/widget/SpinnerAdapter.java
new file mode 100644
index 0000000..f99f45b
--- /dev/null
+++ b/android/widget/SpinnerAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+public interface SpinnerAdapter extends Adapter {
+ /**
+ * Gets a {@link android.view.View} that displays in the drop down popup
+ * the data at the specified position in the data set.
+ *
+ * @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/android/widget/StackView.java b/android/widget/StackView.java
new file mode 100644
index 0000000..1b9055c
--- /dev/null
+++ b/android/widget/StackView.java
@@ -0,0 +1,1459 @@
+/* Copyright (C) 2010 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.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.TableMaskFilter;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.LinearInterpolator;
+import android.widget.RemoteViews.RemoteView;
+
+import java.lang.ref.WeakReference;
+
+@RemoteView
+/**
+ * A view that displays its children in a stack and allows users to discretely swipe
+ * through the children.
+ */
+public class StackView extends AdapterViewAnimator {
+ private final String TAG = "StackView";
+
+ /**
+ * Default animation parameters
+ */
+ private static final int DEFAULT_ANIMATION_DURATION = 400;
+ private static final int MINIMUM_ANIMATION_DURATION = 50;
+ private static final int STACK_RELAYOUT_DURATION = 100;
+
+ /**
+ * Parameters effecting the perspective visuals
+ */
+ private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
+ private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
+
+ private float mPerspectiveShiftX;
+ private float mPerspectiveShiftY;
+ private float mNewPerspectiveShiftX;
+ private float mNewPerspectiveShiftY;
+
+ @SuppressWarnings({"FieldCanBeLocal"})
+ private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
+
+ /**
+ * Represent the two possible stack modes, one where items slide up, and the other
+ * where items slide down. The perspective is also inverted between these two modes.
+ */
+ private static final int ITEMS_SLIDE_UP = 0;
+ private static final int ITEMS_SLIDE_DOWN = 1;
+
+ /**
+ * These specify the different gesture states
+ */
+ private static final int GESTURE_NONE = 0;
+ private static final int GESTURE_SLIDE_UP = 1;
+ private static final int GESTURE_SLIDE_DOWN = 2;
+
+ /**
+ * Specifies how far you need to swipe (up or down) before it
+ * will be consider a completed gesture when you lift your finger
+ */
+ private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
+
+ /**
+ * Specifies the total distance, relative to the size of the stack,
+ * that views will be slid, either up or down
+ */
+ private static final float SLIDE_UP_RATIO = 0.7f;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
+ */
+ private static final int NUM_ACTIVE_VIEWS = 5;
+
+ private static final int FRAME_PADDING = 4;
+
+ private final Rect mTouchRect = new Rect();
+
+ private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
+
+ private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
+
+ /**
+ * These variables are all related to the current state of touch interaction
+ * with the stack
+ */
+ private float mInitialY;
+ private float mInitialX;
+ private int mActivePointerId;
+ private int mYVelocity = 0;
+ private int mSwipeGestureType = GESTURE_NONE;
+ private int mSlideAmount;
+ private int mSwipeThreshold;
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private VelocityTracker mVelocityTracker;
+ private boolean mTransitionIsSetup = false;
+ private int mResOutColor;
+ private int mClickColor;
+
+ private static HolographicHelper sHolographicHelper;
+ private ImageView mHighlight;
+ private ImageView mClickFeedback;
+ private boolean mClickFeedbackIsValid = false;
+ private StackSlider mStackSlider;
+ private boolean mFirstLayoutHappened = false;
+ private long mLastInteractionTime = 0;
+ private long mLastScrollTime;
+ private int mStackMode;
+ private int mFramePadding;
+ private final Rect stackInvalidateRect = new Rect();
+
+ /**
+ * {@inheritDoc}
+ */
+ public StackView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public StackView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.stackViewStyle);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
+
+ mResOutColor = a.getColor(
+ com.android.internal.R.styleable.StackView_resOutColor, 0);
+ mClickColor = a.getColor(
+ com.android.internal.R.styleable.StackView_clickColor, 0);
+
+ a.recycle();
+ initStackView();
+ }
+
+ private void initStackView() {
+ configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
+ setStaticTransformationsEnabled(true);
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mActivePointerId = INVALID_POINTER;
+
+ mHighlight = new ImageView(getContext());
+ mHighlight.setLayoutParams(new LayoutParams(mHighlight));
+ addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
+
+ mClickFeedback = new ImageView(getContext());
+ mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
+ addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
+ mClickFeedback.setVisibility(INVISIBLE);
+
+ mStackSlider = new StackSlider();
+
+ if (sHolographicHelper == null) {
+ sHolographicHelper = new HolographicHelper(mContext);
+ }
+ setClipChildren(false);
+ setClipToPadding(false);
+
+ // This sets the form of the StackView, which is currently to have the perspective-shifted
+ // views above the active view, and have items slide down when sliding out. The opposite is
+ // available by using ITEMS_SLIDE_UP.
+ mStackMode = ITEMS_SLIDE_DOWN;
+
+ // This is a flag to indicate the the stack is loading for the first time
+ mWhichChild = -1;
+
+ // Adjust the frame padding based on the density, since the highlight changes based
+ // on the density
+ final float density = mContext.getResources().getDisplayMetrics().density;
+ mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
+ }
+
+ /**
+ * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
+ */
+ void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
+ if (!animate) {
+ ((StackFrame) view).cancelSliderAnimator();
+ view.setRotationX(0f);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ lp.setVerticalOffset(0);
+ lp.setHorizontalOffset(0);
+ }
+
+ if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
+ transformViewAtIndex(toIndex, view, false);
+ view.setVisibility(VISIBLE);
+ view.setAlpha(1.0f);
+ } else if (fromIndex == 0 && toIndex == 1) {
+ // Slide item in
+ ((StackFrame) view).cancelSliderAnimator();
+ view.setVisibility(VISIBLE);
+
+ int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
+ StackSlider animationSlider = new StackSlider(mStackSlider);
+ animationSlider.setView(view);
+
+ if (animate) {
+ PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
+ PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+ ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+ slideInX, slideInY);
+ slideIn.setDuration(duration);
+ slideIn.setInterpolator(new LinearInterpolator());
+ ((StackFrame) view).setSliderAnimator(slideIn);
+ slideIn.start();
+ } else {
+ animationSlider.setYProgress(0f);
+ animationSlider.setXProgress(0f);
+ }
+ } else if (fromIndex == 1 && toIndex == 0) {
+ // Slide item out
+ ((StackFrame) view).cancelSliderAnimator();
+ int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
+
+ StackSlider animationSlider = new StackSlider(mStackSlider);
+ animationSlider.setView(view);
+ if (animate) {
+ PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
+ PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+ ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+ slideOutX, slideOutY);
+ slideOut.setDuration(duration);
+ slideOut.setInterpolator(new LinearInterpolator());
+ ((StackFrame) view).setSliderAnimator(slideOut);
+ slideOut.start();
+ } else {
+ animationSlider.setYProgress(1.0f);
+ animationSlider.setXProgress(0f);
+ }
+ } else if (toIndex == 0) {
+ // Make sure this view that is "waiting in the wings" is invisible
+ view.setAlpha(0.0f);
+ view.setVisibility(INVISIBLE);
+ } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
+ view.setVisibility(VISIBLE);
+ view.setAlpha(1.0f);
+ view.setRotationX(0f);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ lp.setVerticalOffset(0);
+ lp.setHorizontalOffset(0);
+ } else if (fromIndex == -1) {
+ view.setAlpha(1.0f);
+ view.setVisibility(VISIBLE);
+ } else if (toIndex == -1) {
+ if (animate) {
+ postDelayed(new Runnable() {
+ public void run() {
+ view.setAlpha(0);
+ }
+ }, STACK_RELAYOUT_DURATION);
+ } else {
+ view.setAlpha(0f);
+ }
+ }
+
+ // Implement the faked perspective
+ if (toIndex != -1) {
+ transformViewAtIndex(toIndex, view, animate);
+ }
+ }
+
+ private void transformViewAtIndex(int index, final View view, boolean animate) {
+ final float maxPerspectiveShiftY = mPerspectiveShiftY;
+ final float maxPerspectiveShiftX = mPerspectiveShiftX;
+
+ if (mStackMode == ITEMS_SLIDE_DOWN) {
+ index = mMaxNumActiveViews - index - 1;
+ if (index == mMaxNumActiveViews - 1) index--;
+ } else {
+ index--;
+ if (index < 0) index++;
+ }
+
+ float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
+
+ final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
+
+ float perspectiveTranslationY = r * maxPerspectiveShiftY;
+ float scaleShiftCorrectionY = (scale - 1) *
+ (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
+ final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
+
+ float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
+ float scaleShiftCorrectionX = (1 - scale) *
+ (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
+ final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
+
+ // If this view is currently being animated for a certain position, we need to cancel
+ // this animation so as not to interfere with the new transformation.
+ if (view instanceof StackFrame) {
+ ((StackFrame) view).cancelTransformAnimator();
+ }
+
+ if (animate) {
+ PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
+ PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
+ PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
+ PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
+
+ ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
+ translationY, translationX);
+ oa.setDuration(STACK_RELAYOUT_DURATION);
+ if (view instanceof StackFrame) {
+ ((StackFrame) view).setTransformAnimator(oa);
+ }
+ oa.start();
+ } else {
+ view.setTranslationX(transX);
+ view.setTranslationY(transY);
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ }
+ }
+
+ private void setupStackSlider(View v, int mode) {
+ mStackSlider.setMode(mode);
+ if (v != null) {
+ mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
+ mHighlight.setRotation(v.getRotation());
+ mHighlight.setTranslationY(v.getTranslationY());
+ mHighlight.setTranslationX(v.getTranslationX());
+ mHighlight.bringToFront();
+ v.bringToFront();
+ mStackSlider.setView(v);
+
+ v.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @android.view.RemotableViewMethod
+ public void showNext() {
+ if (mSwipeGestureType != GESTURE_NONE) return;
+ if (!mTransitionIsSetup) {
+ View v = getViewAtRelativeIndex(1);
+ if (v != null) {
+ setupStackSlider(v, StackSlider.NORMAL_MODE);
+ mStackSlider.setYProgress(0);
+ mStackSlider.setXProgress(0);
+ }
+ }
+ super.showNext();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @android.view.RemotableViewMethod
+ public void showPrevious() {
+ if (mSwipeGestureType != GESTURE_NONE) return;
+ if (!mTransitionIsSetup) {
+ View v = getViewAtRelativeIndex(0);
+ if (v != null) {
+ setupStackSlider(v, StackSlider.NORMAL_MODE);
+ mStackSlider.setYProgress(1);
+ mStackSlider.setXProgress(0);
+ }
+ }
+ super.showPrevious();
+ }
+
+ @Override
+ void showOnly(int childIndex, boolean animate) {
+ super.showOnly(childIndex, animate);
+
+ // Here we need to make sure that the z-order of the children is correct
+ for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
+ int index = modulo(i, getWindowSize());
+ ViewAndMetaData vm = mViewsMap.get(index);
+ if (vm != null) {
+ View v = mViewsMap.get(index).view;
+ if (v != null) v.bringToFront();
+ }
+ }
+ if (mHighlight != null) {
+ mHighlight.bringToFront();
+ }
+ mTransitionIsSetup = false;
+ mClickFeedbackIsValid = false;
+ }
+
+ void updateClickFeedback() {
+ if (!mClickFeedbackIsValid) {
+ View v = getViewAtRelativeIndex(1);
+ if (v != null) {
+ mClickFeedback.setImageBitmap(
+ sHolographicHelper.createClickOutline(v, mClickColor));
+ mClickFeedback.setTranslationX(v.getTranslationX());
+ mClickFeedback.setTranslationY(v.getTranslationY());
+ }
+ mClickFeedbackIsValid = true;
+ }
+ }
+
+ @Override
+ void showTapFeedback(View v) {
+ updateClickFeedback();
+ mClickFeedback.setVisibility(VISIBLE);
+ mClickFeedback.bringToFront();
+ invalidate();
+ }
+
+ @Override
+ void hideTapFeedback(View v) {
+ mClickFeedback.setVisibility(INVISIBLE);
+ invalidate();
+ }
+
+ private void updateChildTransforms() {
+ for (int i = 0; i < getNumActiveViews(); i++) {
+ View v = getViewAtRelativeIndex(i);
+ if (v != null) {
+ transformViewAtIndex(i, v, false);
+ }
+ }
+ }
+
+ private static class StackFrame extends FrameLayout {
+ WeakReference<ObjectAnimator> transformAnimator;
+ WeakReference<ObjectAnimator> sliderAnimator;
+
+ public StackFrame(Context context) {
+ super(context);
+ }
+
+ void setTransformAnimator(ObjectAnimator oa) {
+ transformAnimator = new WeakReference<ObjectAnimator>(oa);
+ }
+
+ void setSliderAnimator(ObjectAnimator oa) {
+ sliderAnimator = new WeakReference<ObjectAnimator>(oa);
+ }
+
+ boolean cancelTransformAnimator() {
+ if (transformAnimator != null) {
+ ObjectAnimator oa = transformAnimator.get();
+ if (oa != null) {
+ oa.cancel();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean cancelSliderAnimator() {
+ if (sliderAnimator != null) {
+ ObjectAnimator oa = sliderAnimator.get();
+ if (oa != null) {
+ oa.cancel();
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ @Override
+ FrameLayout getFrameForChild() {
+ StackFrame fl = new StackFrame(mContext);
+ fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
+ return fl;
+ }
+
+ /**
+ * Apply any necessary tranforms for the child that is being added.
+ */
+ void applyTransformForChildAtIndex(View child, int relativeIndex) {
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ boolean expandClipRegion = false;
+
+ canvas.getClipBounds(stackInvalidateRect);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
+ child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
+ lp.resetInvalidateRect();
+ }
+ Rect childInvalidateRect = lp.getInvalidateRect();
+ if (!childInvalidateRect.isEmpty()) {
+ expandClipRegion = true;
+ stackInvalidateRect.union(childInvalidateRect);
+ }
+ }
+
+ // We only expand the clip bounds if necessary.
+ if (expandClipRegion) {
+ canvas.save(Canvas.CLIP_SAVE_FLAG);
+ canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
+ super.dispatchDraw(canvas);
+ canvas.restore();
+ } else {
+ super.dispatchDraw(canvas);
+ }
+ }
+
+ private void onLayout() {
+ if (!mFirstLayoutHappened) {
+ mFirstLayoutHappened = true;
+ updateChildTransforms();
+ }
+
+ final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
+ if (mSlideAmount != newSlideAmount) {
+ mSlideAmount = newSlideAmount;
+ mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
+ }
+
+ if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
+ Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
+
+ mPerspectiveShiftY = mNewPerspectiveShiftY;
+ mPerspectiveShiftX = mNewPerspectiveShiftX;
+ updateChildTransforms();
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL: {
+ final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ if (vscroll < 0) {
+ pacedScroll(false);
+ return true;
+ } else if (vscroll > 0) {
+ pacedScroll(true);
+ return true;
+ }
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ // This ensures that the frequency of stack flips caused by scrolls is capped
+ private void pacedScroll(boolean up) {
+ long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
+ if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
+ if (up) {
+ showPrevious();
+ } else {
+ showNext();
+ }
+ mLastScrollTime = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ switch(action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mActivePointerId == INVALID_POINTER) {
+ mInitialX = ev.getX();
+ mInitialY = ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER) {
+ // no data for our primary pointer, this shouldn't happen, log it
+ Log.d(TAG, "Error: No data for our primary pointer.");
+ return false;
+ }
+ float newY = ev.getY(pointerIndex);
+ float deltaY = newY - mInitialY;
+
+ beginGestureIfNeeded(deltaY);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ mActivePointerId = INVALID_POINTER;
+ mSwipeGestureType = GESTURE_NONE;
+ }
+ }
+
+ return mSwipeGestureType != GESTURE_NONE;
+ }
+
+ private void beginGestureIfNeeded(float deltaY) {
+ if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
+ final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
+ cancelLongPress();
+ requestDisallowInterceptTouchEvent(true);
+
+ if (mAdapter == null) return;
+ final int adapterCount = getCount();
+
+ int activeIndex;
+ if (mStackMode == ITEMS_SLIDE_UP) {
+ activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
+ } else {
+ activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
+ }
+
+ boolean endOfStack = mLoopViews && adapterCount == 1
+ && ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP)
+ || (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
+ boolean beginningOfStack = mLoopViews && adapterCount == 1
+ && ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP)
+ || (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
+
+ int stackMode;
+ if (mLoopViews && !beginningOfStack && !endOfStack) {
+ stackMode = StackSlider.NORMAL_MODE;
+ } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
+ activeIndex++;
+ stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
+ } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
+ stackMode = StackSlider.END_OF_STACK_MODE;
+ } else {
+ stackMode = StackSlider.NORMAL_MODE;
+ }
+
+ mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
+
+ View v = getViewAtRelativeIndex(activeIndex);
+ if (v == null) return;
+
+ setupStackSlider(v, stackMode);
+
+ // We only register this gesture if we've made it this far without a problem
+ mSwipeGestureType = swipeGestureType;
+ cancelHandleClick();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ int action = ev.getAction();
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER) {
+ // no data for our primary pointer, this shouldn't happen, log it
+ Log.d(TAG, "Error: No data for our primary pointer.");
+ return false;
+ }
+
+ float newY = ev.getY(pointerIndex);
+ float newX = ev.getX(pointerIndex);
+ float deltaY = newY - mInitialY;
+ float deltaX = newX - mInitialX;
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: {
+ beginGestureIfNeeded(deltaY);
+
+ float rx = deltaX / (mSlideAmount * 1.0f);
+ if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
+ float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
+ if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
+ mStackSlider.setYProgress(1 - r);
+ mStackSlider.setXProgress(rx);
+ return true;
+ } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
+ float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
+ if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
+ mStackSlider.setYProgress(r);
+ mStackSlider.setXProgress(rx);
+ return true;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ handlePointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ mActivePointerId = INVALID_POINTER;
+ mSwipeGestureType = GESTURE_NONE;
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int activePointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(activePointerIndex);
+ if (pointerId == mActivePointerId) {
+
+ int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
+
+ View v = getViewAtRelativeIndex(activeViewIndex);
+ if (v == null) return;
+
+ // Our primary pointer has gone up -- let's see if we can find
+ // another pointer on the view. If so, then we should replace
+ // our primary pointer with this new pointer and adjust things
+ // so that the view doesn't jump
+ for (int index = 0; index < ev.getPointerCount(); index++) {
+ if (index != activePointerIndex) {
+
+ float x = ev.getX(index);
+ float y = ev.getY(index);
+
+ mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+ if (mTouchRect.contains(Math.round(x), Math.round(y))) {
+ float oldX = ev.getX(activePointerIndex);
+ float oldY = ev.getY(activePointerIndex);
+
+ // adjust our frame of reference to avoid a jump
+ mInitialY += (y - oldY);
+ mInitialX += (x - oldX);
+
+ mActivePointerId = ev.getPointerId(index);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ // ok, we're good, we found a new pointer which is touching the active view
+ return;
+ }
+ }
+ }
+ // if we made it this far, it means we didn't find a satisfactory new pointer :(,
+ // so end the gesture
+ handlePointerUp(ev);
+ }
+ }
+
+ private void handlePointerUp(MotionEvent ev) {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ float newY = ev.getY(pointerIndex);
+ int deltaY = (int) (newY - mInitialY);
+ mLastInteractionTime = System.currentTimeMillis();
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
+ && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
+ // We reset the gesture variable, because otherwise we will ignore showPrevious() /
+ // showNext();
+ mSwipeGestureType = GESTURE_NONE;
+
+ // Swipe threshold exceeded, swipe down
+ if (mStackMode == ITEMS_SLIDE_UP) {
+ showPrevious();
+ } else {
+ showNext();
+ }
+ mHighlight.bringToFront();
+ } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
+ && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
+ // We reset the gesture variable, because otherwise we will ignore showPrevious() /
+ // showNext();
+ mSwipeGestureType = GESTURE_NONE;
+
+ // Swipe threshold exceeded, swipe up
+ if (mStackMode == ITEMS_SLIDE_UP) {
+ showNext();
+ } else {
+ showPrevious();
+ }
+
+ mHighlight.bringToFront();
+ } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
+ // Didn't swipe up far enough, snap back down
+ int duration;
+ float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
+ if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
+ duration = Math.round(mStackSlider.getDurationForNeutralPosition());
+ } else {
+ duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
+ }
+
+ StackSlider animationSlider = new StackSlider(mStackSlider);
+ PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
+ PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+ ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+ snapBackX, snapBackY);
+ pa.setDuration(duration);
+ pa.setInterpolator(new LinearInterpolator());
+ pa.start();
+ } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
+ // Didn't swipe down far enough, snap back up
+ float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
+ int duration;
+ if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
+ duration = Math.round(mStackSlider.getDurationForNeutralPosition());
+ } else {
+ duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
+ }
+
+ StackSlider animationSlider = new StackSlider(mStackSlider);
+ PropertyValuesHolder snapBackY =
+ PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
+ PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+ ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+ snapBackX, snapBackY);
+ pa.setDuration(duration);
+ pa.start();
+ }
+
+ mActivePointerId = INVALID_POINTER;
+ mSwipeGestureType = GESTURE_NONE;
+ }
+
+ private class StackSlider {
+ View mView;
+ float mYProgress;
+ float mXProgress;
+
+ static final int NORMAL_MODE = 0;
+ static final int BEGINNING_OF_STACK_MODE = 1;
+ static final int END_OF_STACK_MODE = 2;
+
+ int mMode = NORMAL_MODE;
+
+ public StackSlider() {
+ }
+
+ public StackSlider(StackSlider copy) {
+ mView = copy.mView;
+ mYProgress = copy.mYProgress;
+ mXProgress = copy.mXProgress;
+ mMode = copy.mMode;
+ }
+
+ private float cubic(float r) {
+ return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
+ }
+
+ private float highlightAlphaInterpolator(float r) {
+ float pivot = 0.4f;
+ if (r < pivot) {
+ return 0.85f * cubic(r / pivot);
+ } else {
+ return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
+ }
+ }
+
+ private float viewAlphaInterpolator(float r) {
+ float pivot = 0.3f;
+ if (r > pivot) {
+ return (r - pivot) / (1 - pivot);
+ } else {
+ return 0;
+ }
+ }
+
+ private float rotationInterpolator(float r) {
+ float pivot = 0.2f;
+ if (r < pivot) {
+ return 0;
+ } else {
+ return (r - pivot) / (1 - pivot);
+ }
+ }
+
+ void setView(View v) {
+ mView = v;
+ }
+
+ public void setYProgress(float r) {
+ // enforce r between 0 and 1
+ r = Math.min(1.0f, r);
+ r = Math.max(0, r);
+
+ mYProgress = r;
+ if (mView == null) return;
+
+ final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+ final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
+
+ int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
+
+ // We need to prevent any clipping issues which may arise by setting a layer type.
+ // This doesn't come for free however, so we only want to enable it when required.
+ if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
+ if (mView.getLayerType() == LAYER_TYPE_NONE) {
+ mView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+ } else {
+ if (mView.getLayerType() != LAYER_TYPE_NONE) {
+ mView.setLayerType(LAYER_TYPE_NONE, null);
+ }
+ }
+
+ switch (mMode) {
+ case NORMAL_MODE:
+ viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
+ highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
+ mHighlight.setAlpha(highlightAlphaInterpolator(r));
+
+ float alpha = viewAlphaInterpolator(1 - r);
+
+ // We make sure that views which can't be seen (have 0 alpha) are also invisible
+ // so that they don't interfere with click events.
+ if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
+ mView.setVisibility(VISIBLE);
+ } else if (alpha == 0 && mView.getAlpha() != 0
+ && mView.getVisibility() == VISIBLE) {
+ mView.setVisibility(INVISIBLE);
+ }
+
+ mView.setAlpha(alpha);
+ mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
+ mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
+ break;
+ case END_OF_STACK_MODE:
+ r = r * 0.2f;
+ viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
+ highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
+ mHighlight.setAlpha(highlightAlphaInterpolator(r));
+ break;
+ case BEGINNING_OF_STACK_MODE:
+ r = (1-r) * 0.2f;
+ viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
+ highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
+ mHighlight.setAlpha(highlightAlphaInterpolator(r));
+ break;
+ }
+ }
+
+ public void setXProgress(float r) {
+ // enforce r between 0 and 1
+ r = Math.min(2.0f, r);
+ r = Math.max(-2.0f, r);
+
+ mXProgress = r;
+
+ if (mView == null) return;
+ final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+ final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
+
+ r *= 0.2f;
+ viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
+ highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
+ }
+
+ void setMode(int mode) {
+ mMode = mode;
+ }
+
+ float getDurationForNeutralPosition() {
+ return getDuration(false, 0);
+ }
+
+ float getDurationForOffscreenPosition() {
+ return getDuration(true, 0);
+ }
+
+ float getDurationForNeutralPosition(float velocity) {
+ return getDuration(false, velocity);
+ }
+
+ float getDurationForOffscreenPosition(float velocity) {
+ return getDuration(true, velocity);
+ }
+
+ private float getDuration(boolean invert, float velocity) {
+ if (mView != null) {
+ final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+
+ float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset);
+ float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount);
+ if (d > maxd) {
+ // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
+ // if we get onLayout() right before this method is called.
+ d = maxd;
+ }
+
+ if (velocity == 0) {
+ return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
+ } else {
+ float duration = invert ? d / Math.abs(velocity) :
+ (maxd - d) / Math.abs(velocity);
+ if (duration < MINIMUM_ANIMATION_DURATION ||
+ duration > DEFAULT_ANIMATION_DURATION) {
+ return getDuration(invert, 0);
+ } else {
+ return duration;
+ }
+ }
+ }
+ return 0;
+ }
+
+ // Used for animations
+ @SuppressWarnings({"UnusedDeclaration"})
+ public float getYProgress() {
+ return mYProgress;
+ }
+
+ // Used for animations
+ @SuppressWarnings({"UnusedDeclaration"})
+ public float getXProgress() {
+ return mXProgress;
+ }
+ }
+
+ LayoutParams createOrReuseLayoutParams(View v) {
+ final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
+ if (currentLp instanceof LayoutParams) {
+ LayoutParams lp = (LayoutParams) currentLp;
+ lp.setHorizontalOffset(0);
+ lp.setVerticalOffset(0);
+ lp.width = 0;
+ lp.width = 0;
+ return lp;
+ }
+ return new LayoutParams(v);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ checkForAndHandleDataChanged();
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ int childRight = mPaddingLeft + child.getMeasuredWidth();
+ int childBottom = mPaddingTop + child.getMeasuredHeight();
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
+ childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
+
+ }
+ onLayout();
+ }
+
+ @Override
+ public void advance() {
+ long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
+
+ if (mAdapter == null) return;
+ final int adapterCount = getCount();
+ if (adapterCount == 1 && mLoopViews) return;
+
+ if (mSwipeGestureType == GESTURE_NONE &&
+ timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
+ showNext();
+ }
+ }
+
+ private void measureChildren() {
+ final int count = getChildCount();
+
+ final int measuredWidth = getMeasuredWidth();
+ final int measuredHeight = getMeasuredHeight();
+
+ final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
+ - mPaddingLeft - mPaddingRight;
+ final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
+ - mPaddingTop - mPaddingBottom;
+
+ int maxWidth = 0;
+ int maxHeight = 0;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
+
+ if (child != mHighlight && child != mClickFeedback) {
+ final int childMeasuredWidth = child.getMeasuredWidth();
+ final int childMeasuredHeight = child.getMeasuredHeight();
+ if (childMeasuredWidth > maxWidth) {
+ maxWidth = childMeasuredWidth;
+ }
+ if (childMeasuredHeight > maxHeight) {
+ maxHeight = childMeasuredHeight;
+ }
+ }
+ }
+
+ mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
+ mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
+
+ // If we have extra space, we try and spread the items out
+ if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
+ mNewPerspectiveShiftX = measuredWidth - maxWidth;
+ }
+
+ if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
+ mNewPerspectiveShiftY = measuredHeight - maxHeight;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
+
+ // We need to deal with the case where our parent hasn't told us how
+ // big we should be. In this case we should
+ float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
+ if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
+ heightSpecSize = haveChildRefSize ?
+ Math.round(mReferenceChildHeight * (1 + factorY)) +
+ mPaddingTop + mPaddingBottom : 0;
+ } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+ if (haveChildRefSize) {
+ int height = Math.round(mReferenceChildHeight * (1 + factorY))
+ + mPaddingTop + mPaddingBottom;
+ if (height <= heightSpecSize) {
+ heightSpecSize = height;
+ } else {
+ heightSpecSize |= MEASURED_STATE_TOO_SMALL;
+
+ }
+ } else {
+ heightSpecSize = 0;
+ }
+ }
+
+ float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
+ if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
+ widthSpecSize = haveChildRefSize ?
+ Math.round(mReferenceChildWidth * (1 + factorX)) +
+ mPaddingLeft + mPaddingRight : 0;
+ } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+ if (haveChildRefSize) {
+ int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
+ if (width <= widthSpecSize) {
+ widthSpecSize = width;
+ } else {
+ widthSpecSize |= MEASURED_STATE_TOO_SMALL;
+ }
+ } else {
+ widthSpecSize = 0;
+ }
+ }
+ setMeasuredDimension(widthSpecSize, heightSpecSize);
+ measureChildren();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return StackView.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setScrollable(getChildCount() > 1);
+ if (isEnabled()) {
+ if (getDisplayedChild() < getChildCount() - 1) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ }
+ if (getDisplayedChild() > 0) {
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (!isEnabled()) {
+ return false;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+ if (getDisplayedChild() < getChildCount() - 1) {
+ showNext();
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+ if (getDisplayedChild() > 0) {
+ showPrevious();
+ return true;
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ class LayoutParams extends ViewGroup.LayoutParams {
+ int horizontalOffset;
+ int verticalOffset;
+ View mView;
+ private final Rect parentRect = new Rect();
+ private final Rect invalidateRect = new Rect();
+ private final RectF invalidateRectf = new RectF();
+ private final Rect globalInvalidateRect = new Rect();
+
+ LayoutParams(View view) {
+ super(0, 0);
+ width = 0;
+ height = 0;
+ horizontalOffset = 0;
+ verticalOffset = 0;
+ mView = view;
+ }
+
+ LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ horizontalOffset = 0;
+ verticalOffset = 0;
+ width = 0;
+ height = 0;
+ }
+
+ void invalidateGlobalRegion(View v, Rect r) {
+ // We need to make a new rect here, so as not to modify the one passed
+ globalInvalidateRect.set(r);
+ globalInvalidateRect.union(0, 0, getWidth(), getHeight());
+ View p = v;
+ if (!(v.getParent() != null && v.getParent() instanceof View)) return;
+
+ boolean firstPass = true;
+ parentRect.set(0, 0, 0, 0);
+ while (p.getParent() != null && p.getParent() instanceof View
+ && !parentRect.contains(globalInvalidateRect)) {
+ if (!firstPass) {
+ globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
+ - p.getScrollY());
+ }
+ firstPass = false;
+ p = (View) p.getParent();
+ parentRect.set(p.getScrollX(), p.getScrollY(),
+ p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
+ p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
+ globalInvalidateRect.right, globalInvalidateRect.bottom);
+ }
+
+ p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
+ globalInvalidateRect.right, globalInvalidateRect.bottom);
+ }
+
+ Rect getInvalidateRect() {
+ return invalidateRect;
+ }
+
+ void resetInvalidateRect() {
+ invalidateRect.set(0, 0, 0, 0);
+ }
+
+ // This is public so that ObjectAnimator can access it
+ public void setVerticalOffset(int newVerticalOffset) {
+ setOffsets(horizontalOffset, newVerticalOffset);
+ }
+
+ public void setHorizontalOffset(int newHorizontalOffset) {
+ setOffsets(newHorizontalOffset, verticalOffset);
+ }
+
+ public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
+ int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
+ horizontalOffset = newHorizontalOffset;
+ int verticalOffsetDelta = newVerticalOffset - verticalOffset;
+ verticalOffset = newVerticalOffset;
+
+ if (mView != null) {
+ mView.requestLayout();
+ int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
+ int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
+ int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
+ int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
+
+ invalidateRectf.set(left, top, right, bottom);
+
+ float xoffset = -invalidateRectf.left;
+ float yoffset = -invalidateRectf.top;
+ invalidateRectf.offset(xoffset, yoffset);
+ mView.getMatrix().mapRect(invalidateRectf);
+ invalidateRectf.offset(-xoffset, -yoffset);
+
+ invalidateRect.set((int) Math.floor(invalidateRectf.left),
+ (int) Math.floor(invalidateRectf.top),
+ (int) Math.ceil(invalidateRectf.right),
+ (int) Math.ceil(invalidateRectf.bottom));
+
+ invalidateGlobalRegion(mView, invalidateRect);
+ }
+ }
+ }
+
+ private static class HolographicHelper {
+ private final Paint mHolographicPaint = new Paint();
+ private final Paint mErasePaint = new Paint();
+ private final Paint mBlurPaint = new Paint();
+ private static final int RES_OUT = 0;
+ private static final int CLICK_FEEDBACK = 1;
+ private float mDensity;
+ private BlurMaskFilter mSmallBlurMaskFilter;
+ private BlurMaskFilter mLargeBlurMaskFilter;
+ private final Canvas mCanvas = new Canvas();
+ private final Canvas mMaskCanvas = new Canvas();
+ private final int[] mTmpXY = new int[2];
+ private final Matrix mIdentityMatrix = new Matrix();
+
+ HolographicHelper(Context context) {
+ mDensity = context.getResources().getDisplayMetrics().density;
+
+ mHolographicPaint.setFilterBitmap(true);
+ mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
+ mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+ mErasePaint.setFilterBitmap(true);
+
+ mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
+ mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
+ }
+
+ Bitmap createClickOutline(View v, int color) {
+ return createOutline(v, CLICK_FEEDBACK, color);
+ }
+
+ Bitmap createResOutline(View v, int color) {
+ return createOutline(v, RES_OUT, color);
+ }
+
+ Bitmap createOutline(View v, int type, int color) {
+ mHolographicPaint.setColor(color);
+ if (type == RES_OUT) {
+ mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
+ } else if (type == CLICK_FEEDBACK) {
+ mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
+ }
+
+ if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
+ return null;
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
+ v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+ mCanvas.setBitmap(bitmap);
+
+ float rotationX = v.getRotationX();
+ float rotation = v.getRotation();
+ float translationY = v.getTranslationY();
+ float translationX = v.getTranslationX();
+ v.setRotationX(0);
+ v.setRotation(0);
+ v.setTranslationY(0);
+ v.setTranslationX(0);
+ v.draw(mCanvas);
+ v.setRotationX(rotationX);
+ v.setRotation(rotation);
+ v.setTranslationY(translationY);
+ v.setTranslationX(translationX);
+
+ drawOutline(mCanvas, bitmap);
+ mCanvas.setBitmap(null);
+ return bitmap;
+ }
+
+ void drawOutline(Canvas dest, Bitmap src) {
+ final int[] xy = mTmpXY;
+ Bitmap mask = src.extractAlpha(mBlurPaint, xy);
+ mMaskCanvas.setBitmap(mask);
+ mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
+ dest.drawColor(0, PorterDuff.Mode.CLEAR);
+ dest.setMatrix(mIdentityMatrix);
+ dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
+ mMaskCanvas.setBitmap(null);
+ mask.recycle();
+ }
+ }
+}
diff --git a/android/widget/SuggestionsAdapter.java b/android/widget/SuggestionsAdapter.java
new file mode 100644
index 0000000..fbb8993
--- /dev/null
+++ b/android/widget/SuggestionsAdapter.java
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2009 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.SearchDialog;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentResolver.OpenResourceIdResult;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.WeakHashMap;
+
+/**
+ * Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
+ *
+ * @hide
+ */
+class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListener {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SuggestionsAdapter";
+ private static final int QUERY_LIMIT = 50;
+
+ static final int REFINE_NONE = 0;
+ static final int REFINE_BY_ENTRY = 1;
+ static final int REFINE_ALL = 2;
+
+ private final SearchManager mSearchManager;
+ private final SearchView mSearchView;
+ private final SearchableInfo mSearchable;
+ private final Context mProviderContext;
+ private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
+ private final int mCommitIconResId;
+
+ private boolean mClosed = false;
+ private int mQueryRefinement = REFINE_BY_ENTRY;
+
+ // URL color
+ private ColorStateList mUrlColor;
+
+ static final int INVALID_INDEX = -1;
+
+ // Cached column indexes, updated when the cursor changes.
+ private int mText1Col = INVALID_INDEX;
+ private int mText2Col = INVALID_INDEX;
+ private int mText2UrlCol = INVALID_INDEX;
+ private int mIconName1Col = INVALID_INDEX;
+ private int mIconName2Col = INVALID_INDEX;
+ private int mFlagsCol = INVALID_INDEX;
+
+ // private final Runnable mStartSpinnerRunnable;
+ // private final Runnable mStopSpinnerRunnable;
+
+ /**
+ * The amount of time we delay in the filter when the user presses the delete key.
+ * @see Filter#setDelayer(android.widget.Filter.Delayer).
+ */
+ private static final long DELETE_KEY_POST_DELAY = 500L;
+
+ public SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable,
+ WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
+ super(context, searchView.getSuggestionRowLayout(), null /* no initial cursor */,
+ true /* auto-requery */);
+
+ mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+ mSearchView = searchView;
+ mSearchable = searchable;
+ mCommitIconResId = searchView.getSuggestionCommitIconResId();
+
+ // set up provider resources (gives us icons, etc.)
+ final Context activityContext = mSearchable.getActivityContext(mContext);
+ mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
+
+ mOutsideDrawablesCache = outsideDrawablesCache;
+
+ // mStartSpinnerRunnable = new Runnable() {
+ // public void run() {
+ // // mSearchView.setWorking(true); // TODO:
+ // }
+ // };
+ //
+ // mStopSpinnerRunnable = new Runnable() {
+ // public void run() {
+ // // mSearchView.setWorking(false); // TODO:
+ // }
+ // };
+
+ // delay 500ms when deleting
+ getFilter().setDelayer(new Filter.Delayer() {
+
+ private int mPreviousLength = 0;
+
+ public long getPostingDelay(CharSequence constraint) {
+ if (constraint == null) return 0;
+
+ long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0;
+ mPreviousLength = constraint.length();
+ return delay;
+ }
+ });
+ }
+
+ /**
+ * Enables query refinement for all suggestions. This means that an additional icon
+ * will be shown for each entry. When clicked, the suggested text on that line will be
+ * copied to the query text field.
+ * <p>
+ *
+ * @param refineWhat which queries to refine. Possible values are
+ * {@link #REFINE_NONE}, {@link #REFINE_BY_ENTRY}, and
+ * {@link #REFINE_ALL}.
+ */
+ public void setQueryRefinement(int refineWhat) {
+ mQueryRefinement = refineWhat;
+ }
+
+ /**
+ * Returns the current query refinement preference.
+ * @return value of query refinement preference
+ */
+ public int getQueryRefinement() {
+ return mQueryRefinement;
+ }
+
+ /**
+ * Overridden to always return <code>false</code>, since we cannot be sure that
+ * suggestion sources return stable IDs.
+ */
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ /**
+ * Use the search suggestions provider to obtain a live cursor. This will be called
+ * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
+ * The results will be processed in the UI thread and changeCursor() will be called.
+ */
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
+ String query = (constraint == null) ? "" : constraint.toString();
+ /**
+ * for in app search we show the progress spinner until the cursor is returned with
+ * the results.
+ */
+ Cursor cursor = null;
+ if (mSearchView.getVisibility() != View.VISIBLE
+ || mSearchView.getWindowVisibility() != View.VISIBLE) {
+ return null;
+ }
+ //mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+ try {
+ cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT);
+ // trigger fill window so the spinner stays up until the results are copied over and
+ // closer to being ready
+ if (cursor != null) {
+ cursor.getCount();
+ return cursor;
+ }
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
+ }
+ // If cursor is null or an exception was thrown, stop the spinner and return null.
+ // changeCursor doesn't get called if cursor is null
+ // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+ return null;
+ }
+
+ public void close() {
+ if (DBG) Log.d(LOG_TAG, "close()");
+ changeCursor(null);
+ mClosed = true;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
+ super.notifyDataSetChanged();
+
+ // mSearchView.onDataSetChanged(); // TODO:
+
+ updateSpinnerState(getCursor());
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
+ super.notifyDataSetInvalidated();
+
+ updateSpinnerState(getCursor());
+ }
+
+ private void updateSpinnerState(Cursor cursor) {
+ Bundle extras = cursor != null ? cursor.getExtras() : null;
+ if (DBG) {
+ Log.d(LOG_TAG, "updateSpinnerState - extra = "
+ + (extras != null
+ ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
+ : null));
+ }
+ // Check if the Cursor indicates that the query is not complete and show the spinner
+ if (extras != null
+ && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
+ // mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+ return;
+ }
+ // If cursor is null or is done, stop the spinner
+ // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+ }
+
+ /**
+ * Cache columns.
+ */
+ @Override
+ public void changeCursor(Cursor c) {
+ if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
+
+ if (mClosed) {
+ Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
+ if (c != null) c.close();
+ return;
+ }
+
+ try {
+ super.changeCursor(c);
+
+ if (c != null) {
+ mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+ mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+ mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+ mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+ mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS);
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "error changing cursor and caching columns", e);
+ }
+ }
+
+ /**
+ * Tags the view with cached child view look-ups.
+ */
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ final View v = super.newView(context, cursor, parent);
+ v.setTag(new ChildViewCache(v));
+
+ // Set up icon.
+ final ImageView iconRefine = v.findViewById(R.id.edit_query);
+ iconRefine.setImageResource(mCommitIconResId);
+
+ return v;
+ }
+
+ /**
+ * Cache of the child views of drop-drown list items, to avoid looking up the children
+ * each time the contents of a list item are changed.
+ */
+ private final static class ChildViewCache {
+ public final TextView mText1;
+ public final TextView mText2;
+ public final ImageView mIcon1;
+ public final ImageView mIcon2;
+ public final ImageView mIconRefine;
+
+ public ChildViewCache(View v) {
+ mText1 = v.findViewById(com.android.internal.R.id.text1);
+ mText2 = v.findViewById(com.android.internal.R.id.text2);
+ mIcon1 = v.findViewById(com.android.internal.R.id.icon1);
+ mIcon2 = v.findViewById(com.android.internal.R.id.icon2);
+ mIconRefine = v.findViewById(com.android.internal.R.id.edit_query);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ChildViewCache views = (ChildViewCache) view.getTag();
+
+ int flags = 0;
+ if (mFlagsCol != INVALID_INDEX) {
+ flags = cursor.getInt(mFlagsCol);
+ }
+ if (views.mText1 != null) {
+ String text1 = getStringOrNull(cursor, mText1Col);
+ setViewText(views.mText1, text1);
+ }
+ if (views.mText2 != null) {
+ // First check TEXT_2_URL
+ CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
+ if (text2 != null) {
+ text2 = formatUrl(context, text2);
+ } else {
+ text2 = getStringOrNull(cursor, mText2Col);
+ }
+
+ // If no second line of text is indicated, allow the first line of text
+ // to be up to two lines if it wants to be.
+ if (TextUtils.isEmpty(text2)) {
+ if (views.mText1 != null) {
+ views.mText1.setSingleLine(false);
+ views.mText1.setMaxLines(2);
+ }
+ } else {
+ if (views.mText1 != null) {
+ views.mText1.setSingleLine(true);
+ views.mText1.setMaxLines(1);
+ }
+ }
+ setViewText(views.mText2, text2);
+ }
+
+ if (views.mIcon1 != null) {
+ setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE);
+ }
+ if (views.mIcon2 != null) {
+ setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE);
+ }
+ if (mQueryRefinement == REFINE_ALL
+ || (mQueryRefinement == REFINE_BY_ENTRY
+ && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) {
+ views.mIconRefine.setVisibility(View.VISIBLE);
+ views.mIconRefine.setTag(views.mText1.getText());
+ views.mIconRefine.setOnClickListener(this);
+ } else {
+ views.mIconRefine.setVisibility(View.GONE);
+ }
+ }
+
+ public void onClick(View v) {
+ Object tag = v.getTag();
+ if (tag instanceof CharSequence) {
+ mSearchView.onQueryRefine((CharSequence) tag);
+ }
+ }
+
+ private CharSequence formatUrl(Context context, CharSequence url) {
+ if (mUrlColor == null) {
+ // Lazily get the URL color from the current theme.
+ TypedValue colorValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true);
+ mUrlColor = context.getColorStateList(colorValue.resourceId);
+ }
+
+ SpannableString text = new SpannableString(url);
+ text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null),
+ 0, url.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return text;
+ }
+
+ private void setViewText(TextView v, CharSequence text) {
+ // Set the text even if it's null, since we need to clear any previous text.
+ v.setText(text);
+
+ if (TextUtils.isEmpty(text)) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private Drawable getIcon1(Cursor cursor) {
+ if (mIconName1Col == INVALID_INDEX) {
+ return null;
+ }
+ String value = cursor.getString(mIconName1Col);
+ Drawable drawable = getDrawableFromResourceValue(value);
+ if (drawable != null) {
+ return drawable;
+ }
+ return getDefaultIcon1(cursor);
+ }
+
+ private Drawable getIcon2(Cursor cursor) {
+ if (mIconName2Col == INVALID_INDEX) {
+ return null;
+ }
+ String value = cursor.getString(mIconName2Col);
+ return getDrawableFromResourceValue(value);
+ }
+
+ /**
+ * Sets the drawable in an image view, makes sure the view is only visible if there
+ * is a drawable.
+ */
+ private void setViewDrawable(ImageView v, Drawable drawable, int nullVisibility) {
+ // Set the icon even if the drawable is null, since we need to clear any
+ // previous icon.
+ v.setImageDrawable(drawable);
+
+ if (drawable == null) {
+ v.setVisibility(nullVisibility);
+ } else {
+ v.setVisibility(View.VISIBLE);
+
+ // This is a hack to get any animated drawables (like a 'working' spinner)
+ // to animate. You have to setVisible true on an AnimationDrawable to get
+ // it to start animating, but it must first have been false or else the
+ // call to setVisible will be ineffective. We need to clear up the story
+ // about animated drawables in the future, see http://b/1878430.
+ drawable.setVisible(false, false);
+ drawable.setVisible(true, false);
+ }
+ }
+
+ /**
+ * Gets the text to show in the query field when a suggestion is selected.
+ *
+ * @param cursor The Cursor to read the suggestion data from. The Cursor should already
+ * be moved to the suggestion that is to be read from.
+ * @return The text to show, or <code>null</code> if the query should not be
+ * changed when selecting this suggestion.
+ */
+ @Override
+ public CharSequence convertToString(Cursor cursor) {
+ if (cursor == null) {
+ return null;
+ }
+
+ String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
+ if (query != null) {
+ return query;
+ }
+
+ if (mSearchable.shouldRewriteQueryFromData()) {
+ String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+ if (data != null) {
+ return data;
+ }
+ }
+
+ if (mSearchable.shouldRewriteQueryFromText()) {
+ String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
+ if (text1 != null) {
+ return text1;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This method is overridden purely to provide a bit of protection against
+ * flaky content providers.
+ *
+ * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ try {
+ return super.getView(position, convertView, parent);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
+ // Put exception string in item title
+ View v = newView(mContext, mCursor, parent);
+ if (v != null) {
+ ChildViewCache views = (ChildViewCache) v.getTag();
+ TextView tv = views.mText1;
+ tv.setText(e.toString());
+ }
+ return v;
+ }
+ }
+
+ /**
+ * This method is overridden purely to provide a bit of protection against
+ * flaky content providers.
+ *
+ * @see android.widget.CursorAdapter#getDropDownView(int, View, ViewGroup)
+ */
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ try {
+ return super.getDropDownView(position, convertView, parent);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
+ // Put exception string in item title
+ final Context context = mDropDownContext == null ? mContext : mDropDownContext;
+ final View v = newDropDownView(context, mCursor, parent);
+ if (v != null) {
+ final ChildViewCache views = (ChildViewCache) v.getTag();
+ final TextView tv = views.mText1;
+ tv.setText(e.toString());
+ }
+ return v;
+ }
+ }
+
+ /**
+ * Gets a drawable given a value provided by a suggestion provider.
+ *
+ * This value could be just the string value of a resource id
+ * (e.g., "2130837524"), in which case we will try to retrieve a drawable from
+ * the provider's resources. If the value is not an integer, it is
+ * treated as a Uri and opened with
+ * {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
+ *
+ * All resources and URIs are read using the suggestion provider's context.
+ *
+ * If the string is not formatted as expected, or no drawable can be found for
+ * the provided value, this method returns null.
+ *
+ * @param drawableId a string like "2130837524",
+ * "android.resource://com.android.alarmclock/2130837524",
+ * or "content://contacts/photos/253".
+ * @return a Drawable, or null if none found
+ */
+ private Drawable getDrawableFromResourceValue(String drawableId) {
+ if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
+ return null;
+ }
+ try {
+ // First, see if it's just an integer
+ int resourceId = Integer.parseInt(drawableId);
+ // It's an int, look for it in the cache
+ String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
+ + "://" + mProviderContext.getPackageName() + "/" + resourceId;
+ // Must use URI as cache key, since ints are app-specific
+ Drawable drawable = checkIconCache(drawableUri);
+ if (drawable != null) {
+ return drawable;
+ }
+ // Not cached, find it by resource ID
+ drawable = mProviderContext.getDrawable(resourceId);
+ // Stick it in the cache, using the URI as key
+ storeInIconCache(drawableUri, drawable);
+ return drawable;
+ } catch (NumberFormatException nfe) {
+ // It's not an integer, use it as a URI
+ Drawable drawable = checkIconCache(drawableId);
+ if (drawable != null) {
+ return drawable;
+ }
+ Uri uri = Uri.parse(drawableId);
+ drawable = getDrawable(uri);
+ storeInIconCache(drawableId, drawable);
+ return drawable;
+ } catch (Resources.NotFoundException nfe) {
+ // It was an integer, but it couldn't be found, bail out
+ Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
+ return null;
+ }
+ }
+
+ /**
+ * Gets a drawable by URI, without using the cache.
+ *
+ * @return A drawable, or {@code null} if the drawable could not be loaded.
+ */
+ private Drawable getDrawable(Uri uri) {
+ try {
+ String scheme = uri.getScheme();
+ if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ // Load drawables through Resources, to get the source density information
+ OpenResourceIdResult r =
+ mProviderContext.getContentResolver().getResourceId(uri);
+ try {
+ return r.r.getDrawable(r.id, mProviderContext.getTheme());
+ } catch (Resources.NotFoundException ex) {
+ throw new FileNotFoundException("Resource does not exist: " + uri);
+ }
+ } else {
+ // Let the ContentResolver handle content and file URIs.
+ InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
+ if (stream == null) {
+ throw new FileNotFoundException("Failed to open " + uri);
+ }
+ try {
+ return Drawable.createFromStream(stream, null);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException ex) {
+ Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
+ }
+ }
+ }
+ } catch (FileNotFoundException fnfe) {
+ Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
+ return null;
+ }
+ }
+
+ private Drawable checkIconCache(String resourceUri) {
+ Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
+ if (cached == null) {
+ return null;
+ }
+ if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
+ return cached.newDrawable();
+ }
+
+ private void storeInIconCache(String resourceUri, Drawable drawable) {
+ if (drawable != null) {
+ mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
+ }
+ }
+
+ /**
+ * Gets the left-hand side icon that will be used for the current suggestion
+ * if the suggestion contains an icon column but no icon or a broken icon.
+ *
+ * @param cursor A cursor positioned at the current suggestion.
+ * @return A non-null drawable.
+ */
+ private Drawable getDefaultIcon1(Cursor cursor) {
+ // Check the component that gave us the suggestion
+ Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
+ if (drawable != null) {
+ return drawable;
+ }
+
+ // Fall back to a default icon
+ return mContext.getPackageManager().getDefaultActivityIcon();
+ }
+
+ /**
+ * Gets the activity or application icon for an activity.
+ * Uses the local icon cache for fast repeated lookups.
+ *
+ * @param component Name of an activity.
+ * @return A drawable, or {@code null} if neither the activity nor the application
+ * has an icon set.
+ */
+ private Drawable getActivityIconWithCache(ComponentName component) {
+ // First check the icon cache
+ String componentIconKey = component.flattenToShortString();
+ // Using containsKey() since we also store null values.
+ if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
+ Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
+ return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
+ }
+ // Then try the activity or application icon
+ Drawable drawable = getActivityIcon(component);
+ // Stick it in the cache so we don't do this lookup again.
+ Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
+ mOutsideDrawablesCache.put(componentIconKey, toCache);
+ return drawable;
+ }
+
+ /**
+ * Gets the activity or application icon for an activity.
+ *
+ * @param component Name of an activity.
+ * @return A drawable, or {@code null} if neither the acitivy or the application
+ * have an icon set.
+ */
+ private Drawable getActivityIcon(ComponentName component) {
+ PackageManager pm = mContext.getPackageManager();
+ final ActivityInfo activityInfo;
+ try {
+ activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
+ } catch (NameNotFoundException ex) {
+ Log.w(LOG_TAG, ex.toString());
+ return null;
+ }
+ int iconId = activityInfo.getIconResource();
+ if (iconId == 0) return null;
+ String pkg = component.getPackageName();
+ Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
+ if (drawable == null) {
+ Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
+ + component.flattenToShortString());
+ return null;
+ }
+ return drawable;
+ }
+
+ /**
+ * Gets the value of a string column by name.
+ *
+ * @param cursor Cursor to read the value from.
+ * @param columnName The name of the column to read.
+ * @return The value of the given column, or <code>null</null>
+ * if the cursor does not contain the given column.
+ */
+ public static String getColumnString(Cursor cursor, String columnName) {
+ int col = cursor.getColumnIndex(columnName);
+ return getStringOrNull(cursor, col);
+ }
+
+ private static String getStringOrNull(Cursor cursor, int col) {
+ if (col == INVALID_INDEX) {
+ return null;
+ }
+ try {
+ return cursor.getString(col);
+ } catch (Exception e) {
+ Log.e(LOG_TAG,
+ "unexpected error retrieving valid column from cursor, "
+ + "did the remote process die?", e);
+ return null;
+ }
+ }
+}
diff --git a/android/widget/Switch.java b/android/widget/Switch.java
new file mode 100644
index 0000000..2e1e963
--- /dev/null
+++ b/android/widget/Switch.java
@@ -0,0 +1,1472 @@
+/*
+ * Copyright (C) 2010 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.animation.ObjectAnimator;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+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.Insets;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.method.AllCapsTransformationMethod;
+import android.text.method.TransformationMethod2;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * A Switch is a two-state toggle switch widget that can select between two
+ * options. The user may drag the "thumb" back and forth to choose the selected option,
+ * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
+ * property controls the text displayed in the label for the switch, whereas the
+ * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
+ * controls the text on the thumb. Similarly, the
+ * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
+ * setTypeface() methods control the typeface and style of label text, whereas the
+ * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
+ * the related setSwitchTypeface() methods control that of the thumb.
+ *
+ * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
+ * the Switch widget which runs on devices back to API 7.</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
+ * guide.</p>
+ *
+ * @attr ref android.R.styleable#Switch_textOn
+ * @attr ref android.R.styleable#Switch_textOff
+ * @attr ref android.R.styleable#Switch_switchMinWidth
+ * @attr ref android.R.styleable#Switch_switchPadding
+ * @attr ref android.R.styleable#Switch_switchTextAppearance
+ * @attr ref android.R.styleable#Switch_thumb
+ * @attr ref android.R.styleable#Switch_thumbTextPadding
+ * @attr ref android.R.styleable#Switch_track
+ */
+public class Switch extends CompoundButton {
+ private static final int THUMB_ANIMATION_DURATION = 250;
+
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_DOWN = 1;
+ private static final int TOUCH_MODE_DRAGGING = 2;
+
+ // Enum for the "typeface" XML parameter.
+ private static final int SANS = 1;
+ private static final int SERIF = 2;
+ private static final int MONOSPACE = 3;
+
+ private Drawable mThumbDrawable;
+ private ColorStateList mThumbTintList = null;
+ private PorterDuff.Mode mThumbTintMode = null;
+ private boolean mHasThumbTint = false;
+ private boolean mHasThumbTintMode = false;
+
+ private Drawable mTrackDrawable;
+ private ColorStateList mTrackTintList = null;
+ private PorterDuff.Mode mTrackTintMode = null;
+ private boolean mHasTrackTint = false;
+ private boolean mHasTrackTintMode = false;
+
+ private int mThumbTextPadding;
+ private int mSwitchMinWidth;
+ private int mSwitchPadding;
+ private boolean mSplitTrack;
+ private CharSequence mTextOn;
+ private CharSequence mTextOff;
+ private boolean mShowText;
+ private boolean mUseFallbackLineSpacing;
+
+ private int mTouchMode;
+ private int mTouchSlop;
+ private float mTouchX;
+ private float mTouchY;
+ private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private int mMinFlingVelocity;
+
+ private float mThumbPosition;
+
+ /**
+ * Width required to draw the switch track and thumb. Includes padding and
+ * optical bounds for both the track and thumb.
+ */
+ private int mSwitchWidth;
+
+ /**
+ * Height required to draw the switch track and thumb. Includes padding and
+ * optical bounds for both the track and thumb.
+ */
+ private int mSwitchHeight;
+
+ /**
+ * Width of the thumb's content region. Does not include padding or
+ * optical bounds.
+ */
+ private int mThumbWidth;
+
+ /** Left bound for drawing the switch track and thumb. */
+ private int mSwitchLeft;
+
+ /** Top bound for drawing the switch track and thumb. */
+ private int mSwitchTop;
+
+ /** Right bound for drawing the switch track and thumb. */
+ private int mSwitchRight;
+
+ /** Bottom bound for drawing the switch track and thumb. */
+ private int mSwitchBottom;
+
+ private TextPaint mTextPaint;
+ private ColorStateList mTextColors;
+ private Layout mOnLayout;
+ private Layout mOffLayout;
+ private TransformationMethod2 mSwitchTransformationMethod;
+ private ObjectAnimator mPositionAnimator;
+
+ @SuppressWarnings("hiding")
+ private final Rect mTempRect = new Rect();
+
+ private static final int[] CHECKED_STATE_SET = {
+ R.attr.state_checked
+ };
+
+ /**
+ * Construct a new Switch with default styling.
+ *
+ * @param context The Context that will determine this widget's theming.
+ */
+ public Switch(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Construct a new Switch with default styling, overriding specific style
+ * attributes as requested.
+ *
+ * @param context The Context that will determine this widget's theming.
+ * @param attrs Specification of attributes that should deviate from default styling.
+ */
+ public Switch(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.switchStyle);
+ }
+
+ /**
+ * Construct a new Switch with a default style determined by the given theme attribute,
+ * overriding specific style attributes as requested.
+ *
+ * @param context The Context that will determine this widget's theming.
+ * @param attrs Specification of attributes that should deviate from the default styling.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+
+ /**
+ * Construct a new Switch with a default style determined by the given theme
+ * attribute or style resource, overriding specific style attributes as
+ * requested.
+ *
+ * @param context The Context that will determine this widget's theming.
+ * @param attrs Specification of attributes that should deviate from the
+ * default styling.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
+ */
+ public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+
+ final Resources res = getResources();
+ mTextPaint.density = res.getDisplayMetrics().density;
+ mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
+ mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
+ if (mThumbDrawable != null) {
+ mThumbDrawable.setCallback(this);
+ }
+ mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
+ if (mTrackDrawable != null) {
+ mTrackDrawable.setCallback(this);
+ }
+ mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
+ mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
+ mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
+ mThumbTextPadding = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
+ mSwitchMinWidth = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.Switch_switchMinWidth, 0);
+ mSwitchPadding = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.Switch_switchPadding, 0);
+ mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
+
+ // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES.
+ // STOPSHIP if the above TODO is not done.
+ mUseFallbackLineSpacing =
+ context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT;
+
+ ColorStateList thumbTintList = a.getColorStateList(
+ com.android.internal.R.styleable.Switch_thumbTint);
+ if (thumbTintList != null) {
+ mThumbTintList = thumbTintList;
+ mHasThumbTint = true;
+ }
+ PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
+ a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
+ if (mThumbTintMode != thumbTintMode) {
+ mThumbTintMode = thumbTintMode;
+ mHasThumbTintMode = true;
+ }
+ if (mHasThumbTint || mHasThumbTintMode) {
+ applyThumbTint();
+ }
+
+ ColorStateList trackTintList = a.getColorStateList(
+ com.android.internal.R.styleable.Switch_trackTint);
+ if (trackTintList != null) {
+ mTrackTintList = trackTintList;
+ mHasTrackTint = true;
+ }
+ PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
+ a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
+ if (mTrackTintMode != trackTintMode) {
+ mTrackTintMode = trackTintMode;
+ mHasTrackTintMode = true;
+ }
+ if (mHasTrackTint || mHasTrackTintMode) {
+ applyTrackTint();
+ }
+
+ final int appearance = a.getResourceId(
+ com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
+ if (appearance != 0) {
+ setSwitchTextAppearance(context, appearance);
+ }
+ a.recycle();
+
+ final ViewConfiguration config = ViewConfiguration.get(context);
+ mTouchSlop = config.getScaledTouchSlop();
+ mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
+
+ // Refresh display with current params
+ refreshDrawableState();
+ setChecked(isChecked());
+ }
+
+ /**
+ * Sets the switch text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ *
+ * @attr ref android.R.styleable#Switch_switchTextAppearance
+ */
+ public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
+ TypedArray appearance =
+ context.obtainStyledAttributes(resid,
+ com.android.internal.R.styleable.TextAppearance);
+
+ ColorStateList colors;
+ int ts;
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ if (colors != null) {
+ mTextColors = colors;
+ } else {
+ // If no color set in TextAppearance, default to the view's textColor
+ mTextColors = getTextColors();
+ }
+
+ ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, 0);
+ if (ts != 0) {
+ if (ts != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(ts);
+ requestLayout();
+ }
+ }
+
+ 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);
+
+ setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
+
+ boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
+ TextAppearance_textAllCaps, false);
+ if (allCaps) {
+ mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
+ mSwitchTransformationMethod.setLengthChangesAllowed(true);
+ } else {
+ mSwitchTransformationMethod = null;
+ }
+
+ appearance.recycle();
+ }
+
+ private void setSwitchTypefaceByIndex(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;
+ }
+
+ setSwitchTypeface(tf, styleIndex);
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed on the
+ * switch, 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.
+ */
+ public void setSwitchTypeface(Typeface tf, int style) {
+ if (style > 0) {
+ if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ setSwitchTypeface(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);
+ setSwitchTypeface(tf);
+ }
+ }
+
+ /**
+ * Sets the typeface in which the text should be displayed on the switch.
+ * Note that not all Typeface families actually have bold and italic
+ * variants, so you may need to use
+ * {@link #setSwitchTypeface(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 setSwitchTypeface(Typeface tf) {
+ if (mTextPaint.getTypeface() != tf) {
+ mTextPaint.setTypeface(tf);
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Set the amount of horizontal padding between the switch and the associated text.
+ *
+ * @param pixels Amount of padding in pixels
+ *
+ * @attr ref android.R.styleable#Switch_switchPadding
+ */
+ public void setSwitchPadding(int pixels) {
+ mSwitchPadding = pixels;
+ requestLayout();
+ }
+
+ /**
+ * Get the amount of horizontal padding between the switch and the associated text.
+ *
+ * @return Amount of padding in pixels
+ *
+ * @attr ref android.R.styleable#Switch_switchPadding
+ */
+ public int getSwitchPadding() {
+ return mSwitchPadding;
+ }
+
+ /**
+ * Set the minimum width of the switch in pixels. The switch's width will be the maximum
+ * of this value and its measured width as determined by the switch drawables and text used.
+ *
+ * @param pixels Minimum width of the switch in pixels
+ *
+ * @attr ref android.R.styleable#Switch_switchMinWidth
+ */
+ public void setSwitchMinWidth(int pixels) {
+ mSwitchMinWidth = pixels;
+ requestLayout();
+ }
+
+ /**
+ * Get the minimum width of the switch in pixels. The switch's width will be the maximum
+ * of this value and its measured width as determined by the switch drawables and text used.
+ *
+ * @return Minimum width of the switch in pixels
+ *
+ * @attr ref android.R.styleable#Switch_switchMinWidth
+ */
+ public int getSwitchMinWidth() {
+ return mSwitchMinWidth;
+ }
+
+ /**
+ * Set the horizontal padding around the text drawn on the switch itself.
+ *
+ * @param pixels Horizontal padding for switch thumb text in pixels
+ *
+ * @attr ref android.R.styleable#Switch_thumbTextPadding
+ */
+ public void setThumbTextPadding(int pixels) {
+ mThumbTextPadding = pixels;
+ requestLayout();
+ }
+
+ /**
+ * Get the horizontal padding around the text drawn on the switch itself.
+ *
+ * @return Horizontal padding for switch thumb text in pixels
+ *
+ * @attr ref android.R.styleable#Switch_thumbTextPadding
+ */
+ public int getThumbTextPadding() {
+ return mThumbTextPadding;
+ }
+
+ /**
+ * Set the drawable used for the track that the switch slides within.
+ *
+ * @param track Track drawable
+ *
+ * @attr ref android.R.styleable#Switch_track
+ */
+ public void setTrackDrawable(Drawable track) {
+ if (mTrackDrawable != null) {
+ mTrackDrawable.setCallback(null);
+ }
+ mTrackDrawable = track;
+ if (track != null) {
+ track.setCallback(this);
+ }
+ requestLayout();
+ }
+
+ /**
+ * Set the drawable used for the track that the switch slides within.
+ *
+ * @param resId Resource ID of a track drawable
+ *
+ * @attr ref android.R.styleable#Switch_track
+ */
+ public void setTrackResource(@DrawableRes int resId) {
+ setTrackDrawable(getContext().getDrawable(resId));
+ }
+
+ /**
+ * Get the drawable used for the track that the switch slides within.
+ *
+ * @return Track drawable
+ *
+ * @attr ref android.R.styleable#Switch_track
+ */
+ public Drawable getTrackDrawable() {
+ return mTrackDrawable;
+ }
+
+ /**
+ * Applies a tint to the track drawable. Does not modify the current
+ * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and tint
+ * mode using {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#Switch_trackTint
+ * @see #getTrackTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setTrackTintList(@Nullable ColorStateList tint) {
+ mTrackTintList = tint;
+ mHasTrackTint = true;
+
+ applyTrackTint();
+ }
+
+ /**
+ * @return the tint applied to the track drawable
+ * @attr ref android.R.styleable#Switch_trackTint
+ * @see #setTrackTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getTrackTintList() {
+ return mTrackTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
+ * The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#Switch_trackTintMode
+ * @see #getTrackTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mTrackTintMode = tintMode;
+ mHasTrackTintMode = true;
+
+ applyTrackTint();
+ }
+
+ /**
+ * @return the blending mode used to apply the tint to the track
+ * drawable
+ * @attr ref android.R.styleable#Switch_trackTintMode
+ * @see #setTrackTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getTrackTintMode() {
+ return mTrackTintMode;
+ }
+
+ private void applyTrackTint() {
+ if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
+ mTrackDrawable = mTrackDrawable.mutate();
+
+ if (mHasTrackTint) {
+ mTrackDrawable.setTintList(mTrackTintList);
+ }
+
+ if (mHasTrackTintMode) {
+ mTrackDrawable.setTintMode(mTrackTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mTrackDrawable.isStateful()) {
+ mTrackDrawable.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * Set the drawable used for the switch "thumb" - the piece that the user
+ * can physically touch and drag along the track.
+ *
+ * @param thumb Thumb drawable
+ *
+ * @attr ref android.R.styleable#Switch_thumb
+ */
+ public void setThumbDrawable(Drawable thumb) {
+ if (mThumbDrawable != null) {
+ mThumbDrawable.setCallback(null);
+ }
+ mThumbDrawable = thumb;
+ if (thumb != null) {
+ thumb.setCallback(this);
+ }
+ requestLayout();
+ }
+
+ /**
+ * Set the drawable used for the switch "thumb" - the piece that the user
+ * can physically touch and drag along the track.
+ *
+ * @param resId Resource ID of a thumb drawable
+ *
+ * @attr ref android.R.styleable#Switch_thumb
+ */
+ public void setThumbResource(@DrawableRes int resId) {
+ setThumbDrawable(getContext().getDrawable(resId));
+ }
+
+ /**
+ * Get the drawable used for the switch "thumb" - the piece that the user
+ * can physically touch and drag along the track.
+ *
+ * @return Thumb drawable
+ *
+ * @attr ref android.R.styleable#Switch_thumb
+ */
+ public Drawable getThumbDrawable() {
+ return mThumbDrawable;
+ }
+
+ /**
+ * Applies a tint to the thumb drawable. Does not modify the current
+ * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
+ * automatically mutate the drawable and apply the specified tint and tint
+ * mode using {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#Switch_thumbTint
+ * @see #getThumbTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setThumbTintList(@Nullable ColorStateList tint) {
+ mThumbTintList = tint;
+ mHasThumbTint = true;
+
+ applyThumbTint();
+ }
+
+ /**
+ * @return the tint applied to the thumb drawable
+ * @attr ref android.R.styleable#Switch_thumbTint
+ * @see #setThumbTintList(ColorStateList)
+ */
+ @Nullable
+ public ColorStateList getThumbTintList() {
+ return mThumbTintList;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
+ * The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#Switch_thumbTintMode
+ * @see #getThumbTintMode()
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
+ mThumbTintMode = tintMode;
+ mHasThumbTintMode = true;
+
+ applyThumbTint();
+ }
+
+ /**
+ * @return the blending mode used to apply the tint to the thumb
+ * drawable
+ * @attr ref android.R.styleable#Switch_thumbTintMode
+ * @see #setThumbTintMode(PorterDuff.Mode)
+ */
+ @Nullable
+ public PorterDuff.Mode getThumbTintMode() {
+ return mThumbTintMode;
+ }
+
+ private void applyThumbTint() {
+ if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
+ mThumbDrawable = mThumbDrawable.mutate();
+
+ if (mHasThumbTint) {
+ mThumbDrawable.setTintList(mThumbTintList);
+ }
+
+ if (mHasThumbTintMode) {
+ mThumbDrawable.setTintMode(mThumbTintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (mThumbDrawable.isStateful()) {
+ mThumbDrawable.setState(getDrawableState());
+ }
+ }
+ }
+
+ /**
+ * Specifies whether the track should be split by the thumb. When true,
+ * the thumb's optical bounds will be clipped out of the track drawable,
+ * then the thumb will be drawn into the resulting gap.
+ *
+ * @param splitTrack Whether the track should be split by the thumb
+ *
+ * @attr ref android.R.styleable#Switch_splitTrack
+ */
+ public void setSplitTrack(boolean splitTrack) {
+ mSplitTrack = splitTrack;
+ invalidate();
+ }
+
+ /**
+ * Returns whether the track should be split by the thumb.
+ *
+ * @attr ref android.R.styleable#Switch_splitTrack
+ */
+ public boolean getSplitTrack() {
+ return mSplitTrack;
+ }
+
+ /**
+ * Returns the text displayed when the button is in the checked state.
+ *
+ * @attr ref android.R.styleable#Switch_textOn
+ */
+ public CharSequence getTextOn() {
+ return mTextOn;
+ }
+
+ /**
+ * Sets the text displayed when the button is in the checked state.
+ *
+ * @attr ref android.R.styleable#Switch_textOn
+ */
+ public void setTextOn(CharSequence textOn) {
+ mTextOn = textOn;
+ requestLayout();
+ }
+
+ /**
+ * Returns the text displayed when the button is not in the checked state.
+ *
+ * @attr ref android.R.styleable#Switch_textOff
+ */
+ public CharSequence getTextOff() {
+ return mTextOff;
+ }
+
+ /**
+ * Sets the text displayed when the button is not in the checked state.
+ *
+ * @attr ref android.R.styleable#Switch_textOff
+ */
+ public void setTextOff(CharSequence textOff) {
+ mTextOff = textOff;
+ requestLayout();
+ }
+
+ /**
+ * Sets whether the on/off text should be displayed.
+ *
+ * @param showText {@code true} to display on/off text
+ * @attr ref android.R.styleable#Switch_showText
+ */
+ public void setShowText(boolean showText) {
+ if (mShowText != showText) {
+ mShowText = showText;
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return whether the on/off text should be displayed
+ * @attr ref android.R.styleable#Switch_showText
+ */
+ public boolean getShowText() {
+ return mShowText;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mShowText) {
+ if (mOnLayout == null) {
+ mOnLayout = makeLayout(mTextOn);
+ }
+
+ if (mOffLayout == null) {
+ mOffLayout = makeLayout(mTextOff);
+ }
+ }
+
+ final Rect padding = mTempRect;
+ final int thumbWidth;
+ final int thumbHeight;
+ if (mThumbDrawable != null) {
+ // Cached thumb width does not include padding.
+ mThumbDrawable.getPadding(padding);
+ thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
+ thumbHeight = mThumbDrawable.getIntrinsicHeight();
+ } else {
+ thumbWidth = 0;
+ thumbHeight = 0;
+ }
+
+ final int maxTextWidth;
+ if (mShowText) {
+ maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
+ + mThumbTextPadding * 2;
+ } else {
+ maxTextWidth = 0;
+ }
+
+ mThumbWidth = Math.max(maxTextWidth, thumbWidth);
+
+ final int trackHeight;
+ if (mTrackDrawable != null) {
+ mTrackDrawable.getPadding(padding);
+ trackHeight = mTrackDrawable.getIntrinsicHeight();
+ } else {
+ padding.setEmpty();
+ trackHeight = 0;
+ }
+
+ // Adjust left and right padding to ensure there's enough room for the
+ // thumb's padding (when present).
+ int paddingLeft = padding.left;
+ int paddingRight = padding.right;
+ if (mThumbDrawable != null) {
+ final Insets inset = mThumbDrawable.getOpticalInsets();
+ paddingLeft = Math.max(paddingLeft, inset.left);
+ paddingRight = Math.max(paddingRight, inset.right);
+ }
+
+ final int switchWidth = Math.max(mSwitchMinWidth,
+ 2 * mThumbWidth + paddingLeft + paddingRight);
+ final int switchHeight = Math.max(trackHeight, thumbHeight);
+ mSwitchWidth = switchWidth;
+ mSwitchHeight = switchHeight;
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final int measuredHeight = getMeasuredHeight();
+ if (measuredHeight < switchHeight) {
+ setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEventInternal(event);
+
+ final CharSequence text = isChecked() ? mTextOn : mTextOff;
+ if (text != null) {
+ event.getText().add(text);
+ }
+ }
+
+ private Layout makeLayout(CharSequence text) {
+ final CharSequence transformed = (mSwitchTransformationMethod != null)
+ ? mSwitchTransformationMethod.getTransformation(text, this)
+ : text;
+
+ int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
+ transformed.length(), mTextPaint, getTextDirectionHeuristic()));
+ return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
+ .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+ .build();
+ }
+
+ /**
+ * @return true if (x, y) is within the target area of the switch thumb
+ */
+ private boolean hitThumb(float x, float y) {
+ if (mThumbDrawable == null) {
+ return false;
+ }
+
+ // Relies on mTempRect, MUST be called first!
+ final int thumbOffset = getThumbOffset();
+
+ mThumbDrawable.getPadding(mTempRect);
+ final int thumbTop = mSwitchTop - mTouchSlop;
+ final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
+ final int thumbRight = thumbLeft + mThumbWidth +
+ mTempRect.left + mTempRect.right + mTouchSlop;
+ final int thumbBottom = mSwitchBottom + mTouchSlop;
+ return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ if (isEnabled() && hitThumb(x, y)) {
+ mTouchMode = TOUCH_MODE_DOWN;
+ mTouchX = x;
+ mTouchY = y;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_IDLE:
+ // Didn't target the thumb, treat normally.
+ break;
+
+ case TOUCH_MODE_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ if (Math.abs(x - mTouchX) > mTouchSlop ||
+ Math.abs(y - mTouchY) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mTouchX = x;
+ mTouchY = y;
+ return true;
+ }
+ break;
+ }
+
+ case TOUCH_MODE_DRAGGING: {
+ final float x = ev.getX();
+ final int thumbScrollRange = getThumbScrollRange();
+ final float thumbScrollOffset = x - mTouchX;
+ float dPos;
+ if (thumbScrollRange != 0) {
+ dPos = thumbScrollOffset / thumbScrollRange;
+ } else {
+ // If the thumb scroll range is empty, just use the
+ // movement direction to snap on or off.
+ dPos = thumbScrollOffset > 0 ? 1 : -1;
+ }
+ if (isLayoutRtl()) {
+ dPos = -dPos;
+ }
+ final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
+ if (newPos != mThumbPosition) {
+ mTouchX = x;
+ setThumbPosition(newPos);
+ }
+ return true;
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ if (mTouchMode == TOUCH_MODE_DRAGGING) {
+ stopDrag(ev);
+ // Allow super class to handle pressed state, etc.
+ super.onTouchEvent(ev);
+ return true;
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ mVelocityTracker.clear();
+ break;
+ }
+ }
+
+ return super.onTouchEvent(ev);
+ }
+
+ private void cancelSuperTouch(MotionEvent ev) {
+ MotionEvent cancel = MotionEvent.obtain(ev);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ super.onTouchEvent(cancel);
+ cancel.recycle();
+ }
+
+ /**
+ * Called from onTouchEvent to end a drag operation.
+ *
+ * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
+ */
+ private void stopDrag(MotionEvent ev) {
+ mTouchMode = TOUCH_MODE_IDLE;
+
+ // Commit the change if the event is up and not canceled and the switch
+ // has not been disabled during the drag.
+ final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
+ final boolean oldState = isChecked();
+ final boolean newState;
+ if (commitChange) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float xvel = mVelocityTracker.getXVelocity();
+ if (Math.abs(xvel) > mMinFlingVelocity) {
+ newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
+ } else {
+ newState = getTargetCheckedState();
+ }
+ } else {
+ newState = oldState;
+ }
+
+ if (newState != oldState) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ // Always call setChecked so that the thumb is moved back to the correct edge
+ setChecked(newState);
+ cancelSuperTouch(ev);
+ }
+
+ private void animateThumbToCheckedState(boolean newCheckedState) {
+ final float targetPosition = newCheckedState ? 1 : 0;
+ mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
+ mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
+ mPositionAnimator.setAutoCancel(true);
+ mPositionAnimator.start();
+ }
+
+ private void cancelPositionAnimator() {
+ if (mPositionAnimator != null) {
+ mPositionAnimator.cancel();
+ }
+ }
+
+ private boolean getTargetCheckedState() {
+ return mThumbPosition > 0.5f;
+ }
+
+ /**
+ * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
+ *
+ * @param position new position between [0,1]
+ */
+ private void setThumbPosition(float position) {
+ mThumbPosition = position;
+ invalidate();
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!isChecked());
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setChecked(checked);
+
+ // Calling the super method may result in setChecked() getting called
+ // recursively with a different value, so load the REAL value...
+ checked = isChecked();
+
+ if (isAttachedToWindow() && isLaidOut()) {
+ animateThumbToCheckedState(checked);
+ } else {
+ // Immediately move the thumb to the new position.
+ cancelPositionAnimator();
+ setThumbPosition(checked ? 1 : 0);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ int opticalInsetLeft = 0;
+ int opticalInsetRight = 0;
+ if (mThumbDrawable != null) {
+ final Rect trackPadding = mTempRect;
+ if (mTrackDrawable != null) {
+ mTrackDrawable.getPadding(trackPadding);
+ } else {
+ trackPadding.setEmpty();
+ }
+
+ final Insets insets = mThumbDrawable.getOpticalInsets();
+ opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
+ opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
+ }
+
+ final int switchRight;
+ final int switchLeft;
+ if (isLayoutRtl()) {
+ switchLeft = getPaddingLeft() + opticalInsetLeft;
+ switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
+ } else {
+ switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
+ switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
+ }
+
+ final int switchTop;
+ final int switchBottom;
+ switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
+ default:
+ case Gravity.TOP:
+ switchTop = getPaddingTop();
+ switchBottom = switchTop + mSwitchHeight;
+ break;
+
+ case Gravity.CENTER_VERTICAL:
+ switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
+ mSwitchHeight / 2;
+ switchBottom = switchTop + mSwitchHeight;
+ break;
+
+ case Gravity.BOTTOM:
+ switchBottom = getHeight() - getPaddingBottom();
+ switchTop = switchBottom - mSwitchHeight;
+ break;
+ }
+
+ mSwitchLeft = switchLeft;
+ mSwitchTop = switchTop;
+ mSwitchBottom = switchBottom;
+ mSwitchRight = switchRight;
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ final Rect padding = mTempRect;
+ final int switchLeft = mSwitchLeft;
+ final int switchTop = mSwitchTop;
+ final int switchRight = mSwitchRight;
+ final int switchBottom = mSwitchBottom;
+
+ int thumbInitialLeft = switchLeft + getThumbOffset();
+
+ final Insets thumbInsets;
+ if (mThumbDrawable != null) {
+ thumbInsets = mThumbDrawable.getOpticalInsets();
+ } else {
+ thumbInsets = Insets.NONE;
+ }
+
+ // Layout the track.
+ if (mTrackDrawable != null) {
+ mTrackDrawable.getPadding(padding);
+
+ // Adjust thumb position for track padding.
+ thumbInitialLeft += padding.left;
+
+ // If necessary, offset by the optical insets of the thumb asset.
+ int trackLeft = switchLeft;
+ int trackTop = switchTop;
+ int trackRight = switchRight;
+ int trackBottom = switchBottom;
+ if (thumbInsets != Insets.NONE) {
+ if (thumbInsets.left > padding.left) {
+ trackLeft += thumbInsets.left - padding.left;
+ }
+ if (thumbInsets.top > padding.top) {
+ trackTop += thumbInsets.top - padding.top;
+ }
+ if (thumbInsets.right > padding.right) {
+ trackRight -= thumbInsets.right - padding.right;
+ }
+ if (thumbInsets.bottom > padding.bottom) {
+ trackBottom -= thumbInsets.bottom - padding.bottom;
+ }
+ }
+ mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
+ }
+
+ // Layout the thumb.
+ if (mThumbDrawable != null) {
+ mThumbDrawable.getPadding(padding);
+
+ final int thumbLeft = thumbInitialLeft - padding.left;
+ final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
+ mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+
+ final Drawable background = getBackground();
+ if (background != null) {
+ background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+ }
+ }
+
+ // Draw the background.
+ super.draw(c);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final Rect padding = mTempRect;
+ final Drawable trackDrawable = mTrackDrawable;
+ if (trackDrawable != null) {
+ trackDrawable.getPadding(padding);
+ } else {
+ padding.setEmpty();
+ }
+
+ final int switchTop = mSwitchTop;
+ final int switchBottom = mSwitchBottom;
+ final int switchInnerTop = switchTop + padding.top;
+ final int switchInnerBottom = switchBottom - padding.bottom;
+
+ final Drawable thumbDrawable = mThumbDrawable;
+ if (trackDrawable != null) {
+ if (mSplitTrack && thumbDrawable != null) {
+ final Insets insets = thumbDrawable.getOpticalInsets();
+ thumbDrawable.copyBounds(padding);
+ padding.left += insets.left;
+ padding.right -= insets.right;
+
+ final int saveCount = canvas.save();
+ canvas.clipRect(padding, Op.DIFFERENCE);
+ trackDrawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ } else {
+ trackDrawable.draw(canvas);
+ }
+ }
+
+ final int saveCount = canvas.save();
+
+ if (thumbDrawable != null) {
+ thumbDrawable.draw(canvas);
+ }
+
+ final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
+ if (switchText != null) {
+ final int drawableState[] = getDrawableState();
+ if (mTextColors != null) {
+ mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
+ }
+ mTextPaint.drawableState = drawableState;
+
+ final int cX;
+ if (thumbDrawable != null) {
+ final Rect bounds = thumbDrawable.getBounds();
+ cX = bounds.left + bounds.right;
+ } else {
+ cX = getWidth();
+ }
+
+ final int left = cX / 2 - switchText.getWidth() / 2;
+ final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
+ canvas.translate(left, top);
+ switchText.draw(canvas);
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ public int getCompoundPaddingLeft() {
+ if (!isLayoutRtl()) {
+ return super.getCompoundPaddingLeft();
+ }
+ int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
+ if (!TextUtils.isEmpty(getText())) {
+ padding += mSwitchPadding;
+ }
+ return padding;
+ }
+
+ @Override
+ public int getCompoundPaddingRight() {
+ if (isLayoutRtl()) {
+ return super.getCompoundPaddingRight();
+ }
+ int padding = super.getCompoundPaddingRight() + mSwitchWidth;
+ if (!TextUtils.isEmpty(getText())) {
+ padding += mSwitchPadding;
+ }
+ return padding;
+ }
+
+ /**
+ * Translates thumb position to offset according to current RTL setting and
+ * thumb scroll range. Accounts for both track and thumb padding.
+ *
+ * @return thumb offset
+ */
+ private int getThumbOffset() {
+ final float thumbPosition;
+ if (isLayoutRtl()) {
+ thumbPosition = 1 - mThumbPosition;
+ } else {
+ thumbPosition = mThumbPosition;
+ }
+ return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
+ }
+
+ private int getThumbScrollRange() {
+ if (mTrackDrawable != null) {
+ final Rect padding = mTempRect;
+ mTrackDrawable.getPadding(padding);
+
+ final Insets insets;
+ if (mThumbDrawable != null) {
+ insets = mThumbDrawable.getOpticalInsets();
+ } else {
+ insets = Insets.NONE;
+ }
+
+ return mSwitchWidth - mThumbWidth - padding.left - padding.right
+ - insets.left - insets.right;
+ } else {
+ return 0;
+ }
+ }
+
+ @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();
+
+ final int[] state = getDrawableState();
+ boolean changed = false;
+
+ final Drawable thumbDrawable = mThumbDrawable;
+ if (thumbDrawable != null && thumbDrawable.isStateful()) {
+ changed |= thumbDrawable.setState(state);
+ }
+
+ final Drawable trackDrawable = mTrackDrawable;
+ if (trackDrawable != null && trackDrawable.isStateful()) {
+ changed |= trackDrawable.setState(state);
+ }
+
+ if (changed) {
+ invalidate();
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mThumbDrawable != null) {
+ mThumbDrawable.setHotspot(x, y);
+ }
+
+ if (mTrackDrawable != null) {
+ mTrackDrawable.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+
+ if (mThumbDrawable != null) {
+ mThumbDrawable.jumpToCurrentState();
+ }
+
+ if (mTrackDrawable != null) {
+ mTrackDrawable.jumpToCurrentState();
+ }
+
+ if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
+ mPositionAnimator.end();
+ mPositionAnimator = null;
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Switch.class.getName();
+ }
+
+ @Override
+ public void onProvideStructure(ViewStructure structure) {
+ super.onProvideStructure(structure);
+ onProvideAutoFillStructureForAssistOrAutofill(structure);
+ }
+
+ @Override
+ public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+ super.onProvideAutofillStructure(structure, flags);
+ onProvideAutoFillStructureForAssistOrAutofill(structure);
+ }
+
+ // NOTE: currently there is no difference for Assist or AutoFill, so it doesn't take flags
+ private void onProvideAutoFillStructureForAssistOrAutofill(ViewStructure structure) {
+ CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+ if (!TextUtils.isEmpty(switchText)) {
+ CharSequence oldText = structure.getText();
+ if (TextUtils.isEmpty(oldText)) {
+ structure.setText(switchText);
+ } else {
+ StringBuilder newText = new StringBuilder();
+ newText.append(oldText).append(' ').append(switchText);
+ structure.setText(newText);
+ }
+ // The style of the label text is provided via the base TextView class. This is more
+ // relevant than the style of the (optional) on/off text on the switch button itself,
+ // so ignore the size/color/style stored this.mTextPaint.
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+ if (!TextUtils.isEmpty(switchText)) {
+ CharSequence oldText = info.getText();
+ if (TextUtils.isEmpty(oldText)) {
+ info.setText(switchText);
+ } else {
+ StringBuilder newText = new StringBuilder();
+ newText.append(oldText).append(' ').append(switchText);
+ info.setText(newText);
+ }
+ }
+ }
+
+ private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
+ @Override
+ public Float get(Switch object) {
+ return object.mThumbPosition;
+ }
+
+ @Override
+ public void setValue(Switch object, float value) {
+ object.setThumbPosition(value);
+ }
+ };
+}
diff --git a/android/widget/TabHost.java b/android/widget/TabHost.java
new file mode 100644
index 0000000..8696d0d
--- /dev/null
+++ b/android/widget/TabHost.java
@@ -0,0 +1,806 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.LocalActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
+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 static final int TABWIDGET_LOCATION_LEFT = 0;
+ private static final int TABWIDGET_LOCATION_TOP = 1;
+ private static final int TABWIDGET_LOCATION_RIGHT = 2;
+ private static final int TABWIDGET_LOCATION_BOTTOM = 3;
+ 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;
+
+ private int mTabLayoutId;
+
+ public TabHost(Context context) {
+ super(context);
+ initTabHost();
+ }
+
+ public TabHost(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
+ }
+
+ public TabHost(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TabHost(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes);
+
+ mTabLayoutId = a.getResourceId(R.styleable.TabWidget_tabLayout, 0);
+ a.recycle();
+
+ if (mTabLayoutId == 0) {
+ // In case the tabWidgetStyle does not inherit from Widget.TabWidget and tabLayout is
+ // not defined.
+ mTabLayoutId = R.layout.tab_indicator_holo;
+ }
+
+ initTabHost();
+ }
+
+ private void initTabHost() {
+ setFocusableInTouchMode(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+
+ mCurrentTab = -1;
+ mCurrentView = null;
+ }
+
+ /**
+ * Creates a new {@link TabSpec} associated with this tab host.
+ *
+ * @param tag tag for the tab specification, must be non-null
+ * @throws IllegalArgumentException If the passed tag is null
+ */
+ @NonNull
+ public TabSpec newTabSpec(@NonNull String tag) {
+ if (tag == null) {
+ throw new IllegalArgumentException("tag must be non-null");
+ }
+ 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 = 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) {
+ if (KeyEvent.isModifierKey(keyCode)) {
+ return false;
+ }
+ 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_TAB:
+ case KeyEvent.KEYCODE_SPACE:
+ 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 = 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'");
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void sendAccessibilityEventInternal(int eventType) {
+ /* avoid super class behavior - TabWidget sends the right events */
+ }
+
+ /**
+ * 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
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ // No longer used, but kept to maintain API compatibility.
+ }
+
+ /**
+ * Add a tab.
+ * @param tabSpec Specifies how to create the indicator and content.
+ * @throws IllegalArgumentException If the passed tab spec has null indicator strategy and / or
+ * null content strategy.
+ */
+ 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);
+
+ // If this is a custom view, then do not draw the bottom strips for
+ // the tab indicators.
+ if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) {
+ mTabWidget.setStripEnabled(false);
+ }
+
+ 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;
+ }
+
+ /**
+ * Returns the current tab.
+ *
+ * @return the current tab, may be {@code null} if no tab is set as current
+ */
+ @Nullable
+ public int getCurrentTab() {
+ return mCurrentTab;
+ }
+
+ /**
+ * Returns the tag for the current tab.
+ *
+ * @return the tag for the current tab, may be {@code null} if no tab is
+ * set as current
+ */
+ @Nullable
+ public String getCurrentTabTag() {
+ if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+ return mTabSpecs.get(mCurrentTab).getTag();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the view for the current tab.
+ *
+ * @return the view for the current tab, may be {@code null} if no tab is
+ * set as current
+ */
+ @Nullable
+ public View getCurrentTabView() {
+ if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+ return mTabWidget.getChildTabViewAt(mCurrentTab);
+ }
+ return null;
+ }
+
+ public View getCurrentView() {
+ return mCurrentView;
+ }
+
+ /**
+ * Sets the current tab based on its tag.
+ *
+ * @param tag the tag for the tab to set as current
+ */
+ public void setCurrentTabByTag(String tag) {
+ for (int i = 0, count = mTabSpecs.size(); i < count; i++) {
+ if (mTabSpecs.get(i).getTag().equals(tag)) {
+ setCurrentTab(i);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get the FrameLayout which holds tab content
+ */
+ public FrameLayout getTabContentView() {
+ return mTabContent;
+ }
+
+ /**
+ * Get the location of the TabWidget.
+ *
+ * @return The TabWidget location.
+ */
+ private int getTabWidgetLocation() {
+ int location = TABWIDGET_LOCATION_TOP;
+
+ switch (mTabWidget.getOrientation()) {
+ case LinearLayout.VERTICAL:
+ location = (mTabContent.getLeft() < mTabWidget.getLeft()) ? TABWIDGET_LOCATION_RIGHT
+ : TABWIDGET_LOCATION_LEFT;
+ break;
+ case LinearLayout.HORIZONTAL:
+ default:
+ location = (mTabContent.getTop() < mTabWidget.getTop()) ? TABWIDGET_LOCATION_BOTTOM
+ : TABWIDGET_LOCATION_TOP;
+ break;
+ }
+ return location;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ final boolean handled = super.dispatchKeyEvent(event);
+
+ // unhandled key events 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)
+ && (mCurrentView != null)
+ && (mCurrentView.isRootNamespace())
+ && (mCurrentView.hasFocus())) {
+ int keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_UP;
+ int directionShouldChangeFocus = View.FOCUS_UP;
+ int soundEffect = SoundEffectConstants.NAVIGATION_UP;
+
+ switch (getTabWidgetLocation()) {
+ case TABWIDGET_LOCATION_LEFT:
+ keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_LEFT;
+ directionShouldChangeFocus = View.FOCUS_LEFT;
+ soundEffect = SoundEffectConstants.NAVIGATION_LEFT;
+ break;
+ case TABWIDGET_LOCATION_RIGHT:
+ keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_RIGHT;
+ directionShouldChangeFocus = View.FOCUS_RIGHT;
+ soundEffect = SoundEffectConstants.NAVIGATION_RIGHT;
+ break;
+ case TABWIDGET_LOCATION_BOTTOM:
+ keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_DOWN;
+ directionShouldChangeFocus = View.FOCUS_DOWN;
+ soundEffect = SoundEffectConstants.NAVIGATION_DOWN;
+ break;
+ case TABWIDGET_LOCATION_TOP:
+ default:
+ keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_UP;
+ directionShouldChangeFocus = View.FOCUS_UP;
+ soundEffect = SoundEffectConstants.NAVIGATION_UP;
+ break;
+ }
+ if (event.getKeyCode() == keyCodeShouldChangeFocus
+ && mCurrentView.findFocus().focusSearch(directionShouldChangeFocus) == null) {
+ mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus();
+ playSoundEffect(soundEffect);
+ return true;
+ }
+ }
+ return handled;
+ }
+
+
+ @Override
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ if (mCurrentView != null){
+ mCurrentView.dispatchWindowFocusChanged(hasFocus);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TabHost.class.getName();
+ }
+
+ 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.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_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 display the contents of the selected tab.
+ */
+ View createTabContent(String tag);
+ }
+
+
+ /**
+ * A tab has a tab indicator, 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 final @NonNull String mTag;
+
+ private IndicatorStrategy mIndicatorStrategy;
+ private ContentStrategy mContentStrategy;
+
+ /**
+ * Constructs a new tab specification with the specified tag.
+ *
+ * @param tag the tag for the tag specification, must be non-null
+ */
+ private TabSpec(@NonNull 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 LabelAndIconIndicatorStrategy(label, icon);
+ return this;
+ }
+
+ /**
+ * Specify a view as the tab indicator.
+ */
+ public TabSpec setIndicator(View view) {
+ mIndicatorStrategy = new ViewIndicatorStrategy(view);
+ 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;
+ }
+
+ /**
+ * Returns the tag for this tab specification.
+ *
+ * @return the tag for this tab specification
+ */
+ @NonNull
+ public 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() {
+ final Context context = getContext();
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View tabIndicator = inflater.inflate(mTabLayoutId,
+ mTabWidget, // tab widget is the parent
+ false); // no inflate params
+
+ final TextView tv = tabIndicator.findViewById(R.id.title);
+ tv.setText(mLabel);
+
+ if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
+ // Donut apps get old color scheme
+ tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
+ tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4));
+ }
+
+ return tabIndicator;
+ }
+ }
+
+ /**
+ * How we create a tab indicator that has a label and an icon
+ */
+ private class LabelAndIconIndicatorStrategy implements IndicatorStrategy {
+
+ private final CharSequence mLabel;
+ private final Drawable mIcon;
+
+ private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) {
+ mLabel = label;
+ mIcon = icon;
+ }
+
+ public View createIndicatorView() {
+ final Context context = getContext();
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View tabIndicator = inflater.inflate(mTabLayoutId,
+ mTabWidget, // tab widget is the parent
+ false); // no inflate params
+
+ final TextView tv = tabIndicator.findViewById(R.id.title);
+ final ImageView iconView = tabIndicator.findViewById(R.id.icon);
+
+ // when icon is gone by default, we're in exclusive mode
+ final boolean exclusive = iconView.getVisibility() == View.GONE;
+ final boolean bindIcon = !exclusive || TextUtils.isEmpty(mLabel);
+
+ tv.setText(mLabel);
+
+ if (bindIcon && mIcon != null) {
+ iconView.setImageDrawable(mIcon);
+ iconView.setVisibility(VISIBLE);
+ }
+
+ if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
+ // Donut apps get old color scheme
+ tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
+ tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4));
+ }
+
+ return tabIndicator;
+ }
+ }
+
+ /**
+ * How to create a tab indicator by specifying a view.
+ */
+ private class ViewIndicatorStrategy implements IndicatorStrategy {
+
+ private final View mView;
+
+ private ViewIndicatorStrategy(View view) {
+ mView = view;
+ }
+
+ public View createIndicatorView() {
+ return mView;
+ }
+ }
+
+ /**
+ * 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.GONE);
+ }
+ }
+
+ /**
+ * 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 activities 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/android/widget/TabWidget.java b/android/widget/TabWidget.java
new file mode 100644
index 0000000..05f7c0a
--- /dev/null
+++ b/android/widget/TabWidget.java
@@ -0,0 +1,566 @@
+/*
+ * 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.annotation.DrawableRes;
+import android.annotation.Nullable;
+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.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.R;
+
+/**
+ *
+ * Displays a list of tab labels representing each page in the parent's tab
+ * collection.
+ * <p>
+ * 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.
+ *
+ * @attr ref android.R.styleable#TabWidget_divider
+ * @attr ref android.R.styleable#TabWidget_tabStripEnabled
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+public class TabWidget extends LinearLayout implements OnFocusChangeListener {
+ private final Rect mBounds = new Rect();
+
+ private OnTabSelectionChanged mSelectionChangedListener;
+
+ // This value will be set to 0 as soon as the first tab is added to TabHost.
+ private int mSelectedTab = -1;
+
+ private Drawable mLeftStrip;
+ private Drawable mRightStrip;
+
+ private boolean mDrawBottomStrips = true;
+ private boolean mStripMoved;
+
+ // When positive, the widths and heights of tabs will be imposed so that
+ // they fit in parent.
+ private int mImposedTabsHeight = -1;
+ private int[] mImposedTabWidths;
+
+ 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.TabWidget, defStyleAttr, defStyleRes);
+
+ mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, mDrawBottomStrips);
+
+ // Tests the target SDK version, as set in the Manifest. Could not be
+ // set using styles.xml in a values-v? directory which targets the
+ // current platform SDK version instead.
+ final boolean isTargetSdkDonutOrLower =
+ context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT;
+
+ final boolean hasExplicitLeft = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripLeft);
+ if (hasExplicitLeft) {
+ mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
+ } else if (isTargetSdkDonutOrLower) {
+ mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left_v4);
+ } else {
+ mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left);
+ }
+
+ final boolean hasExplicitRight = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripRight);
+ if (hasExplicitRight) {
+ mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
+ } else if (isTargetSdkDonutOrLower) {
+ mRightStrip = context.getDrawable(R.drawable.tab_bottom_right_v4);
+ } else {
+ mRightStrip = context.getDrawable(R.drawable.tab_bottom_right);
+ }
+
+ a.recycle();
+
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mStripMoved = true;
+
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ if (mSelectedTab == -1) {
+ return i;
+ } else {
+ // Always draw the selected tab last, so that drop shadows are drawn
+ // in the correct z-order.
+ if (i == childCount - 1) {
+ return mSelectedTab;
+ } else if (i >= mSelectedTab) {
+ return i + 1;
+ } else {
+ return i;
+ }
+ }
+ }
+
+ @Override
+ void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth,
+ int heightMeasureSpec, int totalHeight) {
+ if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
+ MeasureSpec.EXACTLY);
+ }
+
+ super.measureChildBeforeLayout(child, childIndex,
+ widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
+ }
+
+ @Override
+ void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+ super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+
+ // First, measure with no constraint
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int unspecifiedWidth = MeasureSpec.makeSafeMeasureSpec(width,
+ MeasureSpec.UNSPECIFIED);
+ mImposedTabsHeight = -1;
+ super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
+
+ int extraWidth = getMeasuredWidth() - width;
+ if (extraWidth > 0) {
+ final int count = getChildCount();
+
+ int childCount = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ childCount++;
+ }
+
+ if (childCount > 0) {
+ if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
+ mImposedTabWidths = new int[count];
+ }
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ final int childWidth = child.getMeasuredWidth();
+ final int delta = extraWidth / childCount;
+ final int newWidth = Math.max(0, childWidth - delta);
+ mImposedTabWidths[i] = newWidth;
+ // Make sure the extra width is evenly distributed, no int division remainder
+ extraWidth -= childWidth - newWidth; // delta may have been clamped
+ childCount--;
+ mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
+ }
+ }
+ }
+
+ // Measure again, this time with imposed tab widths and respecting
+ // initial spec request.
+ super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Returns the tab indicator view at the given index.
+ *
+ * @param index the zero-based index of the tab indicator view to return
+ * @return the tab indicator view at the given index
+ */
+ public View getChildTabViewAt(int index) {
+ return getChildAt(index);
+ }
+
+ /**
+ * Returns the number of tab indicator views.
+ *
+ * @return the number of tab indicator views
+ */
+ public int getTabCount() {
+ return getChildCount();
+ }
+
+ /**
+ * Sets the drawable to use as a divider between the tab indicators.
+ *
+ * @param drawable the divider drawable
+ * @attr ref android.R.styleable#TabWidget_divider
+ */
+ @Override
+ public void setDividerDrawable(@Nullable Drawable drawable) {
+ super.setDividerDrawable(drawable);
+ }
+
+ /**
+ * Sets the drawable to use as a divider between the tab indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as a divider
+ * @attr ref android.R.styleable#TabWidget_divider
+ */
+ public void setDividerDrawable(@DrawableRes int resId) {
+ setDividerDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * Sets the drawable to use as the left part of the strip below the tab
+ * indicators.
+ *
+ * @param drawable the left strip drawable
+ * @see #getLeftStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ public void setLeftStripDrawable(@Nullable Drawable drawable) {
+ mLeftStrip = drawable;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the drawable to use as the left part of the strip below the tab
+ * indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as the left
+ * strip drawable
+ * @see #getLeftStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ public void setLeftStripDrawable(@DrawableRes int resId) {
+ setLeftStripDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * @return the drawable used as the left part of the strip below the tab
+ * indicators, may be {@code null}
+ * @see #setLeftStripDrawable(int)
+ * @see #setLeftStripDrawable(Drawable)
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ @Nullable
+ public Drawable getLeftStripDrawable() {
+ return mLeftStrip;
+ }
+
+ /**
+ * Sets the drawable to use as the right part of the strip below the tab
+ * indicators.
+ *
+ * @param drawable the right strip drawable
+ * @see #getRightStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ public void setRightStripDrawable(@Nullable Drawable drawable) {
+ mRightStrip = drawable;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the drawable to use as the right part of the strip below the tab
+ * indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as the right
+ * strip drawable
+ * @see #getRightStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ public void setRightStripDrawable(@DrawableRes int resId) {
+ setRightStripDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * @return the drawable used as the right part of the strip below the tab
+ * indicators, may be {@code null}
+ * @see #setRightStripDrawable(int)
+ * @see #setRightStripDrawable(Drawable)
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ @Nullable
+ public Drawable getRightStripDrawable() {
+ return mRightStrip;
+ }
+
+ /**
+ * Controls whether the bottom strips on the tab indicators are drawn or
+ * not. The default is to draw them. If the user specifies a custom
+ * view for the tab indicators, then the TabHost class calls this method
+ * to disable drawing of the bottom strips.
+ * @param stripEnabled true if the bottom strips should be drawn.
+ */
+ public void setStripEnabled(boolean stripEnabled) {
+ mDrawBottomStrips = stripEnabled;
+ invalidate();
+ }
+
+ /**
+ * Indicates whether the bottom strips on the tab indicators are drawn
+ * or not.
+ */
+ public boolean isStripEnabled() {
+ return mDrawBottomStrips;
+ }
+
+ @Override
+ public void childDrawableStateChanged(View child) {
+ if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
+ // To make sure that the bottom strip is redrawn
+ invalidate();
+ }
+ super.childDrawableStateChanged(child);
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ // Do nothing if there are no tabs.
+ if (getTabCount() == 0) return;
+
+ // If the user specified a custom view for the tab indicators, then
+ // do not draw the bottom strips.
+ if (!mDrawBottomStrips) {
+ // Skip drawing the bottom strips.
+ return;
+ }
+
+ final View selectedChild = getChildTabViewAt(mSelectedTab);
+
+ final Drawable leftStrip = mLeftStrip;
+ final Drawable rightStrip = mRightStrip;
+
+ leftStrip.setState(selectedChild.getDrawableState());
+ rightStrip.setState(selectedChild.getDrawableState());
+
+ if (mStripMoved) {
+ final Rect bounds = mBounds;
+ bounds.left = selectedChild.getLeft();
+ bounds.right = selectedChild.getRight();
+ final int myHeight = getHeight();
+ leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
+ myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
+ rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
+ Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
+ mStripMoved = false;
+ }
+
+ leftStrip.draw(canvas);
+ rightStrip.draw(canvas);
+ }
+
+ /**
+ * Sets the current tab.
+ * <p>
+ * 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.
+ * <p>
+ * Note, this is separate from the traditional "focus" that is
+ * employed from the view logic.
+ * <p>
+ * 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).
+ * <p>
+ * 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 index of 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 >= getTabCount() || index == mSelectedTab) {
+ return;
+ }
+
+ if (mSelectedTab != -1) {
+ getChildTabViewAt(mSelectedTab).setSelected(false);
+ }
+ mSelectedTab = index;
+ getChildTabViewAt(mSelectedTab).setSelected(true);
+ mStripMoved = true;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TabWidget.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setItemCount(getTabCount());
+ event.setCurrentItemIndex(mSelectedTab);
+ }
+
+ /**
+ * 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) {
+ getChildTabViewAt(index).requestFocus();
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+
+ final int count = getTabCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildTabViewAt(i);
+ child.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void addView(View child) {
+ if (child.getLayoutParams() == null) {
+ final LinearLayout.LayoutParams lp = new LayoutParams(
+ 0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
+ 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);
+
+ if (child.getPointerIcon() == null) {
+ child.setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND));
+ }
+
+ 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(getTabCount() - 1));
+ }
+
+ @Override
+ public void removeAllViews() {
+ super.removeAllViews();
+ mSelectedTab = -1;
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (!isEnabled()) {
+ return null;
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ /**
+ * Provides a way for {@link TabHost} to be notified that the user clicked
+ * on a tab indicator.
+ */
+ void setTabSelectionListener(OnTabSelectionChanged listener) {
+ mSelectionChangedListener = listener;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ // No-op. Tab selection is separate from keyboard focus.
+ }
+
+ // 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);
+ }
+ }
+
+ /**
+ * Lets {@link TabHost} know that the user clicked on a tab indicator.
+ */
+ 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.
+ * {@code true} if it was due to a press/click and
+ * {@code false} otherwise.
+ */
+ void onTabSelectionChanged(int tabIndex, boolean clicked);
+ }
+}
diff --git a/android/widget/TableLayout.java b/android/widget/TableLayout.java
new file mode 100644
index 0000000..8bb4d16
--- /dev/null
+++ b/android/widget/TableLayout.java
@@ -0,0 +1,780 @@
+/*
+ * 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.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+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>MATCH_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();
+ }
+
+ // TableLayouts are always in vertical orientation; keep this tracked
+ // for shared LinearLayout code.
+ setOrientation(VERTICAL);
+
+ 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
+ *
+ * @attr ref android.R.styleable#TableLayout_shrinkColumns
+ */
+ 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
+ *
+ * @attr ref android.R.styleable#TableLayout_stretchColumns
+ */
+ 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 || mShrinkableColumns.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(l, t, r, b);
+ }
+
+ /**
+ * {@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, heightMeasureSpec);
+ 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, int heightMeasureSpec) {
+ 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, heightMeasureSpec);
+ 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 width 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;
+
+ // Column's widths are changed: force child table rows to re-measure.
+ // (done by super.measureVertical after shrinkAndStretchColumns.)
+ final int nbChildren = getChildCount();
+ for (int i = 0; i < nbChildren; i++) {
+ View child = getChildAt(i);
+ if (child instanceof TableRow) {
+ child.forceLayout();
+ }
+ }
+
+ 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#MATCH_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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TableLayout.class.getName();
+ }
+
+ /**
+ * <p>This set of layout parameters enforces the width of each child to be
+ * {@link #MATCH_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(MATCH_PARENT, h);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int w, int h, float initWeight) {
+ super(MATCH_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(MATCH_PARENT, WRAP_CONTENT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ width = MATCH_PARENT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ width = MATCH_PARENT;
+ if (source instanceof TableLayout.LayoutParams) {
+ weight = ((TableLayout.LayoutParams) source).weight;
+ }
+ }
+
+ /**
+ * <p>Fixes the row's width to
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_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 = MATCH_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/android/widget/TableRow.java b/android/widget/TableRow.java
new file mode 100644
index 0000000..22931fc
--- /dev/null
+++ b/android/widget/TableRow.java
@@ -0,0 +1,549 @@
+/*
+ * 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.NonNull;
+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.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+
+/**
+ * <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#MATCH_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) {
+ final 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(l, t, r, b);
+ }
+
+ /**
+ * {@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;
+
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+ switch (absoluteGravity & 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, int heightMeasureSpec) {
+ 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.MATCH_PARENT:
+ spec = MeasureSpec.makeSafeMeasureSpec(
+ MeasureSpec.getSize(heightMeasureSpec),
+ 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#MATCH_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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TableRow.class.getName();
+ }
+
+ /**
+ * <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(category = "layout")
+ public int column;
+
+ /**
+ * <p>The number of columns the widgets spans over.</p>
+ */
+ @ViewDebug.ExportedProperty(category = "layout")
+ 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(MATCH_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#MATCH_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 = MATCH_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;
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+ encoder.addProperty("layout:column", column);
+ encoder.addProperty("layout:span", span);
+ }
+ }
+
+ // 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/android/widget/TextClock.java b/android/widget/TextClock.java
new file mode 100644
index 0000000..1279040
--- /dev/null
+++ b/android/widget/TextClock.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2012 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 static android.view.ViewDebug.ExportedProperty;
+import static android.widget.RemoteViews.RemoteView;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.ViewHierarchyEncoder;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+/**
+ * <p><code>TextClock</code> can display the current date and/or time as
+ * a formatted string.</p>
+ *
+ * <p>This view honors the 24-hour format system setting. As such, it is
+ * possible and recommended to provide two different formatting patterns:
+ * one to display the date/time in 24-hour mode and one to display the
+ * date/time in 12-hour mode. Most callers will want to use the defaults,
+ * though, which will be appropriate for the user's locale.</p>
+ *
+ * <p>It is possible to determine whether the system is currently in
+ * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p>
+ *
+ * <p>The rules used by this widget to decide how to format the date and
+ * time are the following:</p>
+ * <ul>
+ * <li>In 24-hour mode:
+ * <ul>
+ * <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li>
+ * <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li>
+ * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li>
+ * </ul>
+ * </li>
+ * <li>In 12-hour mode:
+ * <ul>
+ * <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li>
+ * <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li>
+ * <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * <p>The {@link CharSequence} instances used as formatting patterns when calling either
+ * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can
+ * contain styling information. To do so, use a {@link android.text.Spanned} object.
+ * Note that if you customize these strings, it is your responsibility to supply strings
+ * appropriate for formatting dates and/or times in the user's locale.</p>
+ *
+ * @attr ref android.R.styleable#TextClock_format12Hour
+ * @attr ref android.R.styleable#TextClock_format24Hour
+ * @attr ref android.R.styleable#TextClock_timeZone
+ */
+@RemoteView
+public class TextClock extends TextView {
+ /**
+ * The default formatting pattern in 12-hour mode. This pattern is used
+ * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern
+ * or if no pattern was specified when creating an instance of this class.
+ *
+ * This default pattern shows only the time, hours and minutes, and an am/pm
+ * indicator.
+ *
+ * @see #setFormat12Hour(CharSequence)
+ * @see #getFormat12Hour()
+ *
+ * @deprecated Let the system use locale-appropriate defaults instead.
+ */
+ @Deprecated
+ public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
+
+ /**
+ * The default formatting pattern in 24-hour mode. This pattern is used
+ * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
+ * or if no pattern was specified when creating an instance of this class.
+ *
+ * This default pattern shows only the time, hours and minutes.
+ *
+ * @see #setFormat24Hour(CharSequence)
+ * @see #getFormat24Hour()
+ *
+ * @deprecated Let the system use locale-appropriate defaults instead.
+ */
+ @Deprecated
+ public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
+
+ private CharSequence mFormat12;
+ private CharSequence mFormat24;
+ private CharSequence mDescFormat12;
+ private CharSequence mDescFormat24;
+
+ @ExportedProperty
+ private CharSequence mFormat;
+ @ExportedProperty
+ private boolean mHasSeconds;
+
+ private CharSequence mDescFormat;
+
+ private boolean mRegistered;
+ private boolean mShouldRunTicker;
+
+ private Calendar mTime;
+ private String mTimeZone;
+
+ private boolean mShowCurrentUserTime;
+
+ private ContentObserver mFormatChangeObserver;
+ private class FormatChangeObserver extends ContentObserver {
+
+ public FormatChangeObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ chooseFormat();
+ onTimeChanged();
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ chooseFormat();
+ onTimeChanged();
+ }
+ };
+
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+ final String timeZone = intent.getStringExtra("time-zone");
+ createTime(timeZone);
+ }
+ onTimeChanged();
+ }
+ };
+
+ private final Runnable mTicker = new Runnable() {
+ public void run() {
+ onTimeChanged();
+
+ long now = SystemClock.uptimeMillis();
+ long next = now + (1000 - now % 1000);
+
+ getHandler().postAtTime(mTicker, next);
+ }
+ };
+
+ /**
+ * Creates a new clock using the default patterns for the current locale.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public TextClock(Context context) {
+ super(context);
+ init();
+ }
+
+ /**
+ * Creates a new clock inflated from XML. This object's properties are
+ * intialized from the attributes specified in XML.
+ *
+ * This constructor uses a default style of 0, so the only attribute values
+ * applied are those in the Context's Theme and the given AttributeSet.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public TextClock(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Creates a new clock inflated from XML. This object's properties are
+ * intialized from the attributes specified in XML.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
+ try {
+ mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
+ mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
+ mTimeZone = a.getString(R.styleable.TextClock_timeZone);
+ } finally {
+ a.recycle();
+ }
+
+ init();
+ }
+
+ private void init() {
+ if (mFormat12 == null || mFormat24 == null) {
+ LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
+ if (mFormat12 == null) {
+ mFormat12 = ld.timeFormat_hm;
+ }
+ if (mFormat24 == null) {
+ mFormat24 = ld.timeFormat_Hm;
+ }
+ }
+
+ createTime(mTimeZone);
+ chooseFormat();
+ }
+
+ private void createTime(String timeZone) {
+ if (timeZone != null) {
+ mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
+ } else {
+ mTime = Calendar.getInstance();
+ }
+ }
+
+ /**
+ * Returns the formatting pattern used to display the date and/or time
+ * in 12-hour mode. The formatting pattern syntax is described in
+ * {@link DateFormat}.
+ *
+ * @return A {@link CharSequence} or null.
+ *
+ * @see #setFormat12Hour(CharSequence)
+ * @see #is24HourModeEnabled()
+ */
+ @ExportedProperty
+ public CharSequence getFormat12Hour() {
+ return mFormat12;
+ }
+
+ /**
+ * <p>Specifies the formatting pattern used to display the date and/or time
+ * in 12-hour mode. The formatting pattern syntax is described in
+ * {@link DateFormat}.</p>
+ *
+ * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
+ * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
+ * are set to null, the default pattern for the current locale will be used
+ * instead.</p>
+ *
+ * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
+ * you supply a format string generated by
+ * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
+ * takes care of generating a format string adapted to the desired locale.</p>
+ *
+ *
+ * @param format A date/time formatting pattern as described in {@link DateFormat}
+ *
+ * @see #getFormat12Hour()
+ * @see #is24HourModeEnabled()
+ * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
+ * @see DateFormat
+ *
+ * @attr ref android.R.styleable#TextClock_format12Hour
+ */
+ @RemotableViewMethod
+ public void setFormat12Hour(CharSequence format) {
+ mFormat12 = format;
+
+ chooseFormat();
+ onTimeChanged();
+ }
+
+ /**
+ * Like setFormat12Hour, but for the content description.
+ * @hide
+ */
+ public void setContentDescriptionFormat12Hour(CharSequence format) {
+ mDescFormat12 = format;
+
+ chooseFormat();
+ onTimeChanged();
+ }
+
+ /**
+ * Returns the formatting pattern used to display the date and/or time
+ * in 24-hour mode. The formatting pattern syntax is described in
+ * {@link DateFormat}.
+ *
+ * @return A {@link CharSequence} or null.
+ *
+ * @see #setFormat24Hour(CharSequence)
+ * @see #is24HourModeEnabled()
+ */
+ @ExportedProperty
+ public CharSequence getFormat24Hour() {
+ return mFormat24;
+ }
+
+ /**
+ * <p>Specifies the formatting pattern used to display the date and/or time
+ * in 24-hour mode. The formatting pattern syntax is described in
+ * {@link DateFormat}.</p>
+ *
+ * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
+ * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
+ * are set to null, the default pattern for the current locale will be used
+ * instead.</p>
+ *
+ * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
+ * you supply a format string generated by
+ * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
+ * takes care of generating a format string adapted to the desired locale.</p>
+ *
+ * @param format A date/time formatting pattern as described in {@link DateFormat}
+ *
+ * @see #getFormat24Hour()
+ * @see #is24HourModeEnabled()
+ * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
+ * @see DateFormat
+ *
+ * @attr ref android.R.styleable#TextClock_format24Hour
+ */
+ @RemotableViewMethod
+ public void setFormat24Hour(CharSequence format) {
+ mFormat24 = format;
+
+ chooseFormat();
+ onTimeChanged();
+ }
+
+ /**
+ * Like setFormat24Hour, but for the content description.
+ * @hide
+ */
+ public void setContentDescriptionFormat24Hour(CharSequence format) {
+ mDescFormat24 = format;
+
+ chooseFormat();
+ onTimeChanged();
+ }
+
+ /**
+ * Sets whether this clock should always track the current user and not the user of the
+ * current process. This is used for single instance processes like the systemUI who need
+ * to display time for different users.
+ *
+ * @hide
+ */
+ public void setShowCurrentUserTime(boolean showCurrentUserTime) {
+ mShowCurrentUserTime = showCurrentUserTime;
+
+ chooseFormat();
+ onTimeChanged();
+ unregisterObserver();
+ registerObserver();
+ }
+
+ /**
+ * Indicates whether the system is currently using the 24-hour mode.
+ *
+ * When the system is in 24-hour mode, this view will use the pattern
+ * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
+ * returned by {@link #getFormat12Hour()} is used instead.
+ *
+ * If either one of the formats is null, the other format is used. If
+ * both formats are null, the default formats for the current locale are used.
+ *
+ * @return true if time should be displayed in 24-hour format, false if it
+ * should be displayed in 12-hour format.
+ *
+ * @see #setFormat12Hour(CharSequence)
+ * @see #getFormat12Hour()
+ * @see #setFormat24Hour(CharSequence)
+ * @see #getFormat24Hour()
+ */
+ public boolean is24HourModeEnabled() {
+ if (mShowCurrentUserTime) {
+ return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
+ } else {
+ return DateFormat.is24HourFormat(getContext());
+ }
+ }
+
+ /**
+ * Indicates which time zone is currently used by this view.
+ *
+ * @return The ID of the current time zone or null if the default time zone,
+ * as set by the user, must be used
+ *
+ * @see TimeZone
+ * @see java.util.TimeZone#getAvailableIDs()
+ * @see #setTimeZone(String)
+ */
+ public String getTimeZone() {
+ return mTimeZone;
+ }
+
+ /**
+ * Sets the specified time zone to use in this clock. When the time zone
+ * is set through this method, system time zone changes (when the user
+ * sets the time zone in settings for instance) will be ignored.
+ *
+ * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
+ * or null to user the time zone specified by the user
+ * (system time zone)
+ *
+ * @see #getTimeZone()
+ * @see java.util.TimeZone#getAvailableIDs()
+ * @see TimeZone#getTimeZone(String)
+ *
+ * @attr ref android.R.styleable#TextClock_timeZone
+ */
+ @RemotableViewMethod
+ public void setTimeZone(String timeZone) {
+ mTimeZone = timeZone;
+
+ createTime(timeZone);
+ onTimeChanged();
+ }
+
+ /**
+ * Returns the current format string. Always valid after constructor has
+ * finished, and will never be {@code null}.
+ *
+ * @hide
+ */
+ public CharSequence getFormat() {
+ return mFormat;
+ }
+
+ /**
+ * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
+ * depending on whether the user has selected 24-hour format.
+ */
+ private void chooseFormat() {
+ final boolean format24Requested = is24HourModeEnabled();
+
+ LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
+
+ if (format24Requested) {
+ mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
+ mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
+ } else {
+ mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
+ mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
+ }
+
+ boolean hadSeconds = mHasSeconds;
+ mHasSeconds = DateFormat.hasSeconds(mFormat);
+
+ if (mShouldRunTicker && hadSeconds != mHasSeconds) {
+ if (hadSeconds) getHandler().removeCallbacks(mTicker);
+ else mTicker.run();
+ }
+ }
+
+ /**
+ * Returns a if not null, else return b if not null, else return c.
+ */
+ private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
+ return a == null ? (b == null ? c : b) : a;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (!mRegistered) {
+ mRegistered = true;
+
+ registerReceiver();
+ registerObserver();
+
+ createTime(mTimeZone);
+ }
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+
+ if (!mShouldRunTicker && isVisible) {
+ mShouldRunTicker = true;
+ if (mHasSeconds) {
+ mTicker.run();
+ } else {
+ onTimeChanged();
+ }
+ } else if (mShouldRunTicker && !isVisible) {
+ mShouldRunTicker = false;
+ getHandler().removeCallbacks(mTicker);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mRegistered) {
+ unregisterReceiver();
+ unregisterObserver();
+
+ mRegistered = false;
+ }
+ }
+
+ private void registerReceiver() {
+ final IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+
+ // OK, this is gross but needed. This class is supported by the
+ // remote views mechanism and as a part of that the remote views
+ // can be inflated by a context for another user without the app
+ // having interact users permission - just for loading resources.
+ // For example, when adding widgets from a managed profile to the
+ // home screen. Therefore, we register the receiver as the user
+ // the app is running as not the one the context is for.
+ getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
+ filter, null, getHandler());
+ }
+
+ private void registerObserver() {
+ if (mRegistered) {
+ if (mFormatChangeObserver == null) {
+ mFormatChangeObserver = new FormatChangeObserver(getHandler());
+ }
+ final ContentResolver resolver = getContext().getContentResolver();
+ if (mShowCurrentUserTime) {
+ resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+ mFormatChangeObserver, UserHandle.USER_ALL);
+ } else {
+ resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+ mFormatChangeObserver);
+ }
+ }
+ }
+
+ private void unregisterReceiver() {
+ getContext().unregisterReceiver(mIntentReceiver);
+ }
+
+ private void unregisterObserver() {
+ if (mFormatChangeObserver != null) {
+ final ContentResolver resolver = getContext().getContentResolver();
+ resolver.unregisterContentObserver(mFormatChangeObserver);
+ }
+ }
+
+ /**
+ * Update the displayed time if this view and its ancestors and window is visible
+ */
+ private void onTimeChanged() {
+ // mShouldRunTicker always equals the last value passed into onVisibilityAggregated
+ if (mShouldRunTicker) {
+ mTime.setTimeInMillis(System.currentTimeMillis());
+ setText(DateFormat.format(mFormat, mTime));
+ setContentDescription(DateFormat.format(mDescFormat, mTime));
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+
+ CharSequence s = getFormat12Hour();
+ stream.addProperty("format12Hour", s == null ? null : s.toString());
+
+ s = getFormat24Hour();
+ stream.addProperty("format24Hour", s == null ? null : s.toString());
+ stream.addProperty("format", mFormat == null ? null : mFormat.toString());
+ stream.addProperty("hasSeconds", mHasSeconds);
+ }
+}
diff --git a/android/widget/TextInputTimePickerView.java b/android/widget/TextInputTimePickerView.java
new file mode 100644
index 0000000..0cf8faa
--- /dev/null
+++ b/android/widget/TextInputTimePickerView.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2017 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.LocaleList;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.View;
+
+import com.android.internal.R;
+
+/**
+ * View to show text input based time picker with hour and minute fields and an optional AM/PM
+ * spinner.
+ *
+ * @hide
+ */
+public class TextInputTimePickerView extends RelativeLayout {
+ public static final int HOURS = 0;
+ public static final int MINUTES = 1;
+ public static final int AMPM = 2;
+
+ private static final int AM = 0;
+ private static final int PM = 1;
+
+ private final EditText mHourEditText;
+ private final EditText mMinuteEditText;
+ private final TextView mInputSeparatorView;
+ private final Spinner mAmPmSpinner;
+ private final TextView mErrorLabel;
+ private final TextView mHourLabel;
+ private final TextView mMinuteLabel;
+
+ private boolean mIs24Hour;
+ private boolean mHourFormatStartsAtZero;
+ private OnValueTypedListener mListener;
+
+ private boolean mErrorShowing;
+
+ interface OnValueTypedListener {
+ void onValueChanged(int inputType, int newValue);
+ }
+
+ public TextInputTimePickerView(Context context) {
+ this(context, null);
+ }
+
+ public TextInputTimePickerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle,
+ int defStyleRes) {
+ super(context, attrs, defStyle, defStyleRes);
+
+ inflate(context, R.layout.time_picker_text_input_material, this);
+
+ mHourEditText = findViewById(R.id.input_hour);
+ mMinuteEditText = findViewById(R.id.input_minute);
+ mInputSeparatorView = findViewById(R.id.input_separator);
+ mErrorLabel = findViewById(R.id.label_error);
+ mHourLabel = findViewById(R.id.label_hour);
+ mMinuteLabel = findViewById(R.id.label_minute);
+
+ mHourEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ parseAndSetHourInternal(editable.toString());
+ }
+ });
+
+ mMinuteEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ parseAndSetMinuteInternal(editable.toString());
+ }
+ });
+
+ mAmPmSpinner = findViewById(R.id.am_pm_spinner);
+ final String[] amPmStrings = TimePicker.getAmPmStrings(context);
+ ArrayAdapter<CharSequence> adapter =
+ new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item);
+ adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0]));
+ adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1]));
+ mAmPmSpinner.setAdapter(adapter);
+ mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int position,
+ long id) {
+ if (position == 0) {
+ mListener.onValueChanged(AMPM, AM);
+ } else {
+ mListener.onValueChanged(AMPM, PM);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {}
+ });
+ }
+
+ void setListener(OnValueTypedListener listener) {
+ mListener = listener;
+ }
+
+ void setHourFormat(int maxCharLength) {
+ mHourEditText.setFilters(new InputFilter[] {
+ new InputFilter.LengthFilter(maxCharLength)});
+ mMinuteEditText.setFilters(new InputFilter[] {
+ new InputFilter.LengthFilter(maxCharLength)});
+ final LocaleList locales = mContext.getResources().getConfiguration().getLocales();
+ mHourEditText.setImeHintLocales(locales);
+ mMinuteEditText.setImeHintLocales(locales);
+ }
+
+ boolean validateInput() {
+ final boolean inputValid = parseAndSetHourInternal(mHourEditText.getText().toString())
+ && parseAndSetMinuteInternal(mMinuteEditText.getText().toString());
+ setError(!inputValid);
+ return inputValid;
+ }
+
+ void updateSeparator(String separatorText) {
+ mInputSeparatorView.setText(separatorText);
+ }
+
+ private void setError(boolean enabled) {
+ mErrorShowing = enabled;
+
+ mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
+ mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
+ mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
+ }
+
+ /**
+ * Computes the display value and updates the text of the view.
+ * <p>
+ * This method should be called whenever the current value or display
+ * properties (leading zeroes, max digits) change.
+ */
+ void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour,
+ boolean hourFormatStartsAtZero) {
+ final String format = "%d";
+
+ mIs24Hour = is24Hour;
+ mHourFormatStartsAtZero = hourFormatStartsAtZero;
+
+ mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
+
+ if (amOrPm == AM) {
+ mAmPmSpinner.setSelection(0);
+ } else {
+ mAmPmSpinner.setSelection(1);
+ }
+
+ mHourEditText.setText(String.format(format, localizedHour));
+ mMinuteEditText.setText(String.format(format, minute));
+
+ if (mErrorShowing) {
+ validateInput();
+ }
+ }
+
+ private boolean parseAndSetHourInternal(String input) {
+ try {
+ final int hour = Integer.parseInt(input);
+ if (!isValidLocalizedHour(hour)) {
+ final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+ final int maxHour = mIs24Hour ? 23 : 11 + minHour;
+ mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
+ MathUtils.constrain(hour, minHour, maxHour)));
+ return false;
+ }
+ mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
+ return true;
+ } catch (NumberFormatException e) {
+ // Do nothing since we cannot parse the input.
+ return false;
+ }
+ }
+
+ private boolean parseAndSetMinuteInternal(String input) {
+ try {
+ final int minutes = Integer.parseInt(input);
+ if (minutes < 0 || minutes > 59) {
+ mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
+ return false;
+ }
+ mListener.onValueChanged(MINUTES, minutes);
+ return true;
+ } catch (NumberFormatException e) {
+ // Do nothing since we cannot parse the input.
+ return false;
+ }
+ }
+
+ private boolean isValidLocalizedHour(int localizedHour) {
+ final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+ final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
+ return localizedHour >= minHour && localizedHour <= maxHour;
+ }
+
+ private int getHourOfDayFromLocalizedHour(int localizedHour) {
+ int hourOfDay = localizedHour;
+ if (mIs24Hour) {
+ if (!mHourFormatStartsAtZero && localizedHour == 24) {
+ hourOfDay = 0;
+ }
+ } else {
+ if (!mHourFormatStartsAtZero && localizedHour == 12) {
+ hourOfDay = 0;
+ }
+ if (mAmPmSpinner.getSelectedItemPosition() == 1) {
+ hourOfDay += 12;
+ }
+ }
+ return hourOfDay;
+ }
+}
diff --git a/android/widget/TextSwitcher.java b/android/widget/TextSwitcher.java
new file mode 100644
index 0000000..ecd9a8c
--- /dev/null
+++ b/android/widget/TextSwitcher.java
@@ -0,0 +1,96 @@
+/*
+ * 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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TextSwitcher.class.getName();
+ }
+}
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
new file mode 100644
index 0000000..4b6c4d3
--- /dev/null
+++ b/android/widget/TextView.java
@@ -0,0 +1,12041 @@
+/*
+ * 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 static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
+import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
+
+import android.R;
+import android.annotation.CheckResult;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.annotation.XmlRes;
+import android.app.Activity;
+import android.app.assist.AssistStructure;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UndoManager;
+import android.content.res.ColorStateList;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.BaseCanvas;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.fonts.FontVariationAxis;
+import android.icu.text.DecimalFormatSymbols;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableParcel;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.BoringLayout;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.GraphicsOperations;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.TextWatcher;
+import android.text.method.AllCapsTransformationMethod;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.method.DateKeyListener;
+import android.text.method.DateTimeKeyListener;
+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.TimeKeyListener;
+import android.text.method.TransformationMethod;
+import android.text.method.TransformationMethod2;
+import android.text.method.WordIterator;
+import android.text.style.CharacterStyle;
+import android.text.style.ClickableSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.SpellCheckSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.URLSpan;
+import android.text.style.UpdateAppearance;
+import android.text.util.Linkify;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.view.AccessibilityIterators.TextSegmentIterator;
+import android.view.ActionMode;
+import android.view.Choreographer;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.ViewStructure;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textservice.SpellCheckerSubtype;
+import android.view.textservice.TextServicesManager;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FastMath;
+import com.android.internal.widget.EditableInputConnection;
+
+import libcore.util.EmptyArray;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * A user interface element that displays text to the user.
+ * To provide user-editable text, see {@link EditText}.
+ * <p>
+ * The following code sample shows a typical use, with an XML layout
+ * and code to modify the contents of the text view:
+ * </p>
+
+ * <pre>
+ * <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ * <TextView
+ * android:id="@+id/text_view_id"
+ * android:layout_height="wrap_content"
+ * android:layout_width="wrap_content"
+ * android:text="@string/hello" />
+ * </LinearLayout>
+ * </pre>
+ * <p>
+ * This code sample demonstrates how to modify the contents of the text view
+ * defined in the previous XML layout:
+ * </p>
+ * <pre>
+ * public class MainActivity extends Activity {
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * setContentView(R.layout.activity_main);
+ * final TextView helloTextView = (TextView) findViewById(R.id.text_view_id);
+ * helloTextView.setText(R.string.user_greeting);
+ * }
+ * }
+ * </pre>
+ * <p>
+ * To customize the appearance of TextView, see <a href="https://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>.
+ * </p>
+ * <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_textAppearance
+ * @attr ref android.R.styleable#TextView_textColorLink
+ * @attr ref android.R.styleable#TextView_textSize
+ * @attr ref android.R.styleable#TextView_textScaleX
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @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_freezesText
+ * @attr ref android.R.styleable#TextView_ellipsize
+ * @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_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ * @attr ref android.R.styleable#TextView_drawableTint
+ * @attr ref android.R.styleable#TextView_drawableTintMode
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ * @attr ref android.R.styleable#TextView_inputType
+ * @attr ref android.R.styleable#TextView_imeOptions
+ * @attr ref android.R.styleable#TextView_privateImeOptions
+ * @attr ref android.R.styleable#TextView_imeActionLabel
+ * @attr ref android.R.styleable#TextView_imeActionId
+ * @attr ref android.R.styleable#TextView_editorExtras
+ * @attr ref android.R.styleable#TextView_elegantTextHeight
+ * @attr ref android.R.styleable#TextView_letterSpacing
+ * @attr ref android.R.styleable#TextView_fontFeatureSettings
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @attr ref android.R.styleable#TextView_hyphenationFrequency
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+ * @attr ref android.R.styleable#TextView_autoSizePresetSizes
+ */
+@RemoteView
+public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
+ static final String LOG_TAG = "TextView";
+ static final boolean DEBUG_EXTRACT = false;
+ static final boolean DEBUG_AUTOFILL = false;
+ private static final float[] TEMP_POSITION = new float[2];
+
+ // 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;
+
+ // Enum for the "ellipsize" XML parameter.
+ private static final int ELLIPSIZE_NOT_SET = -1;
+ private static final int ELLIPSIZE_NONE = 0;
+ private static final int ELLIPSIZE_START = 1;
+ private static final int ELLIPSIZE_MIDDLE = 2;
+ private static final int ELLIPSIZE_END = 3;
+ private static final int ELLIPSIZE_MARQUEE = 4;
+
+ // 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;
+
+ /**
+ * Draw marquee text with fading edges as usual
+ */
+ private static final int MARQUEE_FADE_NORMAL = 0;
+
+ /**
+ * Draw marquee text as ellipsize end while inactive instead of with the fade.
+ * (Useful for devices where the fade can be expensive if overdone)
+ */
+ private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
+
+ /**
+ * Draw marquee text with fading edges because it is currently active/animating.
+ */
+ private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
+
+ private static final int LINES = 1;
+ private static final int EMS = LINES;
+ private static final int PIXELS = 2;
+
+ private static final RectF TEMP_RECTF = new RectF();
+
+ /** @hide */
+ static final int VERY_WIDE = 1024 * 1024; // XXX should be much larger
+ private static final int ANIMATED_SCROLL_GAP = 250;
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private static final Spanned EMPTY_SPANNED = new SpannedString("");
+
+ private static final int CHANGE_WATCHER_PRIORITY = 100;
+
+ // New state used to change background based on whether this TextView is multiline.
+ private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
+
+ // Accessibility action to share selected text.
+ private static final int ACCESSIBILITY_ACTION_SHARE = 0x10000000;
+
+ /**
+ * @hide
+ */
+ // Accessibility action start id for "process text" actions.
+ static final int ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID = 0x10000100;
+
+ /**
+ * @hide
+ */
+ static final int PROCESS_TEXT_REQUEST_CODE = 100;
+
+ /**
+ * Return code of {@link #doKeyDown}.
+ */
+ private static final int KEY_EVENT_NOT_HANDLED = 0;
+ private static final int KEY_EVENT_HANDLED = -1;
+ private static final int KEY_DOWN_HANDLED_BY_KEY_LISTENER = 1;
+ private static final int KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD = 2;
+
+ private static final int FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY = 500;
+
+ // System wide time for last cut, copy or text changed action.
+ static long sLastCutCopyOrTextChangedTime;
+
+ private ColorStateList mTextColor;
+ private ColorStateList mHintTextColor;
+ private ColorStateList mLinkTextColor;
+ @ViewDebug.ExportedProperty(category = "text")
+ private int mCurTextColor;
+ private int mCurHintTextColor;
+ private boolean mFreezesText;
+
+ private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
+ private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
+
+ private float mShadowRadius, mShadowDx, mShadowDy;
+ private int mShadowColor;
+
+ private boolean mPreDrawRegistered;
+ private boolean mPreDrawListenerDetached;
+
+ private TextClassifier mTextClassifier;
+
+ // A flag to prevent repeated movements from escaping the enclosing text view. The idea here is
+ // that if a user is holding down a movement key to traverse text, we shouldn't also traverse
+ // the view hierarchy. On the other hand, if the user is using the movement key to traverse
+ // views (i.e. the first movement was to traverse out of this view, or this view was traversed
+ // into by the user holding the movement key down) then we shouldn't prevent the focus from
+ // changing.
+ private boolean mPreventDefaultMovement;
+
+ private TextUtils.TruncateAt mEllipsize;
+
+ static class Drawables {
+ static final int LEFT = 0;
+ static final int TOP = 1;
+ static final int RIGHT = 2;
+ static final int BOTTOM = 3;
+
+ static final int DRAWABLE_NONE = -1;
+ static final int DRAWABLE_RIGHT = 0;
+ static final int DRAWABLE_LEFT = 1;
+
+ final Rect mCompoundRect = new Rect();
+
+ final Drawable[] mShowing = new Drawable[4];
+
+ ColorStateList mTintList;
+ PorterDuff.Mode mTintMode;
+ boolean mHasTint;
+ boolean mHasTintMode;
+
+ Drawable mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp;
+ Drawable mDrawableLeftInitial, mDrawableRightInitial;
+
+ boolean mIsRtlCompatibilityMode;
+ boolean mOverride;
+
+ int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
+ mDrawableSizeStart, mDrawableSizeEnd, mDrawableSizeError, mDrawableSizeTemp;
+
+ int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
+ mDrawableHeightStart, mDrawableHeightEnd, mDrawableHeightError, mDrawableHeightTemp;
+
+ int mDrawablePadding;
+
+ int mDrawableSaved = DRAWABLE_NONE;
+
+ public Drawables(Context context) {
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ mIsRtlCompatibilityMode = targetSdkVersion < VERSION_CODES.JELLY_BEAN_MR1
+ || !context.getApplicationInfo().hasRtlSupport();
+ mOverride = false;
+ }
+
+ /**
+ * @return {@code true} if this object contains metadata that needs to
+ * be retained, {@code false} otherwise
+ */
+ public boolean hasMetadata() {
+ return mDrawablePadding != 0 || mHasTintMode || mHasTint;
+ }
+
+ /**
+ * Updates the list of displayed drawables to account for the current
+ * layout direction.
+ *
+ * @param layoutDirection the current layout direction
+ * @return {@code true} if the displayed drawables changed
+ */
+ public boolean resolveWithLayoutDirection(int layoutDirection) {
+ final Drawable previousLeft = mShowing[Drawables.LEFT];
+ final Drawable previousRight = mShowing[Drawables.RIGHT];
+
+ // First reset "left" and "right" drawables to their initial values
+ mShowing[Drawables.LEFT] = mDrawableLeftInitial;
+ mShowing[Drawables.RIGHT] = mDrawableRightInitial;
+
+ if (mIsRtlCompatibilityMode) {
+ // Use "start" drawable as "left" drawable if the "left" drawable was not defined
+ if (mDrawableStart != null && mShowing[Drawables.LEFT] == null) {
+ mShowing[Drawables.LEFT] = mDrawableStart;
+ mDrawableSizeLeft = mDrawableSizeStart;
+ mDrawableHeightLeft = mDrawableHeightStart;
+ }
+ // Use "end" drawable as "right" drawable if the "right" drawable was not defined
+ if (mDrawableEnd != null && mShowing[Drawables.RIGHT] == null) {
+ mShowing[Drawables.RIGHT] = mDrawableEnd;
+ mDrawableSizeRight = mDrawableSizeEnd;
+ mDrawableHeightRight = mDrawableHeightEnd;
+ }
+ } else {
+ // JB-MR1+ normal case: "start" / "end" drawables are overriding "left" / "right"
+ // drawable if and only if they have been defined
+ switch(layoutDirection) {
+ case LAYOUT_DIRECTION_RTL:
+ if (mOverride) {
+ mShowing[Drawables.RIGHT] = mDrawableStart;
+ mDrawableSizeRight = mDrawableSizeStart;
+ mDrawableHeightRight = mDrawableHeightStart;
+
+ mShowing[Drawables.LEFT] = mDrawableEnd;
+ mDrawableSizeLeft = mDrawableSizeEnd;
+ mDrawableHeightLeft = mDrawableHeightEnd;
+ }
+ break;
+
+ case LAYOUT_DIRECTION_LTR:
+ default:
+ if (mOverride) {
+ mShowing[Drawables.LEFT] = mDrawableStart;
+ mDrawableSizeLeft = mDrawableSizeStart;
+ mDrawableHeightLeft = mDrawableHeightStart;
+
+ mShowing[Drawables.RIGHT] = mDrawableEnd;
+ mDrawableSizeRight = mDrawableSizeEnd;
+ mDrawableHeightRight = mDrawableHeightEnd;
+ }
+ break;
+ }
+ }
+
+ applyErrorDrawableIfNeeded(layoutDirection);
+
+ return mShowing[Drawables.LEFT] != previousLeft
+ || mShowing[Drawables.RIGHT] != previousRight;
+ }
+
+ public void setErrorDrawable(Drawable dr, TextView tv) {
+ if (mDrawableError != dr && mDrawableError != null) {
+ mDrawableError.setCallback(null);
+ }
+ mDrawableError = dr;
+
+ if (mDrawableError != null) {
+ final Rect compoundRect = mCompoundRect;
+ final int[] state = tv.getDrawableState();
+
+ mDrawableError.setState(state);
+ mDrawableError.copyBounds(compoundRect);
+ mDrawableError.setCallback(tv);
+ mDrawableSizeError = compoundRect.width();
+ mDrawableHeightError = compoundRect.height();
+ } else {
+ mDrawableSizeError = mDrawableHeightError = 0;
+ }
+ }
+
+ private void applyErrorDrawableIfNeeded(int layoutDirection) {
+ // first restore the initial state if needed
+ switch (mDrawableSaved) {
+ case DRAWABLE_LEFT:
+ mShowing[Drawables.LEFT] = mDrawableTemp;
+ mDrawableSizeLeft = mDrawableSizeTemp;
+ mDrawableHeightLeft = mDrawableHeightTemp;
+ break;
+ case DRAWABLE_RIGHT:
+ mShowing[Drawables.RIGHT] = mDrawableTemp;
+ mDrawableSizeRight = mDrawableSizeTemp;
+ mDrawableHeightRight = mDrawableHeightTemp;
+ break;
+ case DRAWABLE_NONE:
+ default:
+ }
+ // then, if needed, assign the Error drawable to the correct location
+ if (mDrawableError != null) {
+ switch(layoutDirection) {
+ case LAYOUT_DIRECTION_RTL:
+ mDrawableSaved = DRAWABLE_LEFT;
+
+ mDrawableTemp = mShowing[Drawables.LEFT];
+ mDrawableSizeTemp = mDrawableSizeLeft;
+ mDrawableHeightTemp = mDrawableHeightLeft;
+
+ mShowing[Drawables.LEFT] = mDrawableError;
+ mDrawableSizeLeft = mDrawableSizeError;
+ mDrawableHeightLeft = mDrawableHeightError;
+ break;
+ case LAYOUT_DIRECTION_LTR:
+ default:
+ mDrawableSaved = DRAWABLE_RIGHT;
+
+ mDrawableTemp = mShowing[Drawables.RIGHT];
+ mDrawableSizeTemp = mDrawableSizeRight;
+ mDrawableHeightTemp = mDrawableHeightRight;
+
+ mShowing[Drawables.RIGHT] = mDrawableError;
+ mDrawableSizeRight = mDrawableSizeError;
+ mDrawableHeightRight = mDrawableHeightError;
+ break;
+ }
+ }
+ }
+ }
+
+ Drawables mDrawables;
+
+ private CharWrapper mCharWrapper;
+
+ private Marquee mMarquee;
+ private boolean mRestartMarquee;
+
+ private int mMarqueeRepeatLimit = 3;
+
+ private int mLastLayoutDirection = -1;
+
+ /**
+ * On some devices the fading edges add a performance penalty if used
+ * extensively in the same layout. This mode indicates how the marquee
+ * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
+ */
+ private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+
+ /**
+ * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
+ * the layout that should be used when the mode switches.
+ */
+ private Layout mSavedMarqueeModeLayout;
+
+ @ViewDebug.ExportedProperty(category = "text")
+ private CharSequence mText;
+ private CharSequence mTransformed;
+ private BufferType mBufferType = BufferType.NORMAL;
+
+ private CharSequence mHint;
+ private Layout mHintLayout;
+
+ private MovementMethod mMovement;
+
+ private TransformationMethod mTransformation;
+ private boolean mAllowTransformationLengthChange;
+ private ChangeWatcher mChangeWatcher;
+
+ private ArrayList<TextWatcher> mListeners;
+
+ // display attributes
+ private final TextPaint mTextPaint;
+ private boolean mUserSetTextScaleX;
+ private Layout mLayout;
+ private boolean mLocalesChanged = false;
+
+ // True if setKeyListener() has been explicitly called
+ private boolean mListenerChanged = false;
+ // True if internationalized input should be used for numbers and date and time.
+ private final boolean mUseInternationalizedInput;
+ // True if fallback fonts that end up getting used should be allowed to affect line spacing.
+ /* package */ final boolean mUseFallbackLineSpacing;
+
+ @ViewDebug.ExportedProperty(category = "text")
+ private int mGravity = Gravity.TOP | Gravity.START;
+ private boolean mHorizontallyScrolling;
+
+ private int mAutoLinkMask;
+ private boolean mLinksClickable = true;
+
+ private float mSpacingMult = 1.0f;
+ private float mSpacingAdd = 0.0f;
+
+ private int mBreakStrategy;
+ private int mHyphenationFrequency;
+ private int mJustificationMode;
+
+ private int mMaximum = Integer.MAX_VALUE;
+ private int mMaxMode = LINES;
+ private int mMinimum = 0;
+ private int mMinMode = LINES;
+
+ private int mOldMaximum = mMaximum;
+ private int mOldMaxMode = mMaxMode;
+
+ 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;
+ private int mDeferScroll = -1;
+
+ // tmp primitives, so we don't alloc them on each draw
+ private Rect mTempRect;
+ private long mLastScroll;
+ private Scroller mScroller;
+ private TextPaint mTempTextPaint;
+
+ private BoringLayout.Metrics mBoring, mHintBoring;
+ private BoringLayout mSavedLayout, mSavedHintLayout;
+
+ private TextDirectionHeuristic mTextDir;
+
+ private InputFilter[] mFilters = NO_FILTERS;
+
+ private volatile Locale mCurrentSpellCheckerLocaleCache;
+
+ // It is possible to have a selection even when mEditor is null (programmatically set, like when
+ // a link is pressed). These highlight-related fields do not go in mEditor.
+ int mHighlightColor = 0x6633B5E5;
+ private Path mHighlightPath;
+ private final Paint mHighlightPaint;
+ private boolean mHighlightPathBogus = true;
+
+ // Although these fields are specific to editable text, they are not added to Editor because
+ // they are defined by the TextView's style and are theme-dependent.
+ int mCursorDrawableRes;
+ // These six fields, could be moved to Editor, since we know their default values and we
+ // could condition the creation of the Editor to a non standard value. This is however
+ // brittle since the hardcoded values here (such as
+ // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the
+ // default style is modified.
+ int mTextSelectHandleLeftRes;
+ int mTextSelectHandleRightRes;
+ int mTextSelectHandleRes;
+ int mTextEditSuggestionItemLayout;
+ int mTextEditSuggestionContainerLayout;
+ int mTextEditSuggestionHighlightStyle;
+
+ /**
+ * {@link EditText} specific data, created on demand when one of the Editor fields is used.
+ * See {@link #createEditorIfNeeded()}.
+ */
+ private Editor mEditor;
+
+ private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
+ private static final int DEVICE_PROVISIONED_NO = 1;
+ private static final int DEVICE_PROVISIONED_YES = 2;
+
+ /**
+ * Some special options such as sharing selected text should only be shown if the device
+ * is provisioned. Only check the provisioned state once for a given view instance.
+ */
+ private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN;
+
+ /**
+ * The TextView does not auto-size text (default).
+ */
+ public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0;
+
+ /**
+ * The TextView scales text size both horizontally and vertically to fit within the
+ * container.
+ */
+ public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1;
+
+ /** @hide */
+ @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AutoSizeTextType {}
+ // Default minimum size for auto-sizing text in scaled pixels.
+ private static final int DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP = 12;
+ // Default maximum size for auto-sizing text in scaled pixels.
+ private static final int DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP = 112;
+ // Default value for the step size in pixels.
+ private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1;
+ // Use this to specify that any of the auto-size configuration int values have not been set.
+ private static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
+ // Auto-size text type.
+ private int mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+ // Specify if auto-size text is needed.
+ private boolean mNeedsAutoSizeText = false;
+ // Step size for auto-sizing in pixels.
+ private float mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ // Minimum text size for auto-sizing in pixels.
+ private float mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ // Maximum text size for auto-sizing in pixels.
+ private float mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ // Contains a (specified or computed) distinct sorted set of text sizes in pixels to pick from
+ // when auto-sizing text.
+ private int[] mAutoSizeTextSizesInPx = EmptyArray.INT;
+ // Specifies whether auto-size should use the provided auto size steps set or if it should
+ // build the steps set using mAutoSizeMinTextSizeInPx, mAutoSizeMaxTextSizeInPx and
+ // mAutoSizeStepGranularityInPx.
+ private boolean mHasPresetAutoSizeValues = false;
+
+ // Indicates whether the text was set from resources or dynamically, so it can be used to
+ // sanitize autofill requests.
+ private boolean mTextFromResource = false;
+
+ /**
+ * Kick-start the font cache for the zygote process (to pay the cost of
+ * initializing freetype for our default font only once).
+ * @hide
+ */
+ public static void preloadFontCache() {
+ Paint p = new Paint();
+ p.setAntiAlias(true);
+ // Ensure that the Typeface is loaded here.
+ // Typically, Typeface is preloaded by zygote but not on all devices, e.g. Android Auto.
+ // So, sets Typeface.DEFAULT explicitly here for ensuring that the Typeface is loaded here
+ // since Paint.measureText can not be called without Typeface static initializer.
+ p.setTypeface(Typeface.DEFAULT);
+ // We don't care about the result, just the side-effect of measuring.
+ p.measureText("H");
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an action is
+ * performed on the editor.
+ */
+ public interface OnEditorActionListener {
+ /**
+ * Called when an action is being performed.
+ *
+ * @param v The view that was clicked.
+ * @param actionId Identifier of the action. This will be either the
+ * identifier you supplied, or {@link EditorInfo#IME_NULL
+ * EditorInfo.IME_NULL} if being called due to the enter key
+ * being pressed.
+ * @param event If triggered by an enter key, this is the event;
+ * otherwise, this is null.
+ * @return Return true if you have consumed the action, else false.
+ */
+ boolean onEditorAction(TextView v, int actionId, KeyEvent event);
+ }
+
+ public TextView(Context context) {
+ this(context, null);
+ }
+
+ public TextView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.textViewStyle);
+ }
+
+ public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings("deprecation")
+ public TextView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // TextView is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ mText = "";
+
+ final Resources res = getResources();
+ final CompatibilityInfo compat = res.getCompatibilityInfo();
+
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ mTextPaint.density = res.getDisplayMetrics().density;
+ mTextPaint.setCompatibilityScaling(compat.applicationScale);
+
+ mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mHighlightPaint.setCompatibilityScaling(compat.applicationScale);
+
+ mMovement = getDefaultMovementMethod();
+
+ mTransformation = null;
+
+ final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
+ attributes.mTextColor = ColorStateList.valueOf(0xFF000000);
+ attributes.mTextSize = 15;
+ mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
+ mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
+ mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
+
+ final Resources.Theme theme = context.getTheme();
+
+ /*
+ * 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 a = theme.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
+ TypedArray appearance = null;
+ int ap = a.getResourceId(
+ com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
+ a.recycle();
+ if (ap != -1) {
+ appearance = theme.obtainStyledAttributes(
+ ap, com.android.internal.R.styleable.TextAppearance);
+ }
+ if (appearance != null) {
+ readTextAppearance(context, appearance, attributes, false /* styleArray */);
+ attributes.mFontFamilyExplicit = false;
+ 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, drawableStart = null, drawableEnd = null;
+ ColorStateList drawableTint = null;
+ PorterDuff.Mode drawableTintMode = null;
+ int drawablePadding = 0;
+ int ellipsize = ELLIPSIZE_NOT_SET;
+ boolean singleLine = false;
+ int maxlength = -1;
+ CharSequence text = "";
+ CharSequence hint = null;
+ boolean password = false;
+ float autoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ float autoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ float autoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ int inputType = EditorInfo.TYPE_NULL;
+ a = theme.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+
+ readTextAppearance(context, a, attributes, true /* styleArray */);
+
+ int n = a.getIndexCount();
+
+ boolean fromResourceId = false;
+ 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_drawableStart:
+ drawableStart = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableEnd:
+ drawableEnd = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableTint:
+ drawableTint = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableTintMode:
+ drawableTintMode = Drawable.parseTintMode(a.getInt(attr, -1), drawableTintMode);
+ 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:
+ hint = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_text:
+ fromResourceId = true;
+ 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_marqueeRepeatLimit:
+ setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
+ 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_enabled:
+ setEnabled(a.getBoolean(attr, isEnabled()));
+ 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;
+
+ case com.android.internal.R.styleable.TextView_inputType:
+ inputType = a.getInt(attr, EditorInfo.TYPE_NULL);
+ break;
+
+ case com.android.internal.R.styleable.TextView_allowUndo:
+ createEditorIfNeeded();
+ mEditor.mAllowUndo = a.getBoolean(attr, true);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeOptions:
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeOptions = a.getInt(attr,
+ mEditor.mInputContentType.imeOptions);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeActionLabel:
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeActionLabel = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeActionId:
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeActionId = a.getInt(attr,
+ mEditor.mInputContentType.imeActionId);
+ break;
+
+ case com.android.internal.R.styleable.TextView_privateImeOptions:
+ setPrivateImeOptions(a.getString(attr));
+ break;
+
+ case com.android.internal.R.styleable.TextView_editorExtras:
+ try {
+ setInputExtras(a.getResourceId(attr, 0));
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Failure reading input extras", e);
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Failure reading input extras", e);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_textCursorDrawable:
+ mCursorDrawableRes = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
+ mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textSelectHandleRight:
+ mTextSelectHandleRightRes = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textSelectHandle:
+ mTextSelectHandleRes = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
+ mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textEditSuggestionContainerLayout:
+ mTextEditSuggestionContainerLayout = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textEditSuggestionHighlightStyle:
+ mTextEditSuggestionHighlightStyle = a.getResourceId(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textIsSelectable:
+ setTextIsSelectable(a.getBoolean(attr, false));
+ break;
+
+ case com.android.internal.R.styleable.TextView_breakStrategy:
+ mBreakStrategy = a.getInt(attr, Layout.BREAK_STRATEGY_SIMPLE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_hyphenationFrequency:
+ mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoSizeTextType:
+ mAutoSizeTextType = a.getInt(attr, AUTO_SIZE_TEXT_TYPE_NONE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoSizeStepGranularity:
+ autoSizeStepGranularityInPx = a.getDimension(attr,
+ UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoSizeMinTextSize:
+ autoSizeMinTextSizeInPx = a.getDimension(attr,
+ UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoSizeMaxTextSize:
+ autoSizeMaxTextSizeInPx = a.getDimension(attr,
+ UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoSizePresetSizes:
+ final int autoSizeStepSizeArrayResId = a.getResourceId(attr, 0);
+ if (autoSizeStepSizeArrayResId > 0) {
+ final TypedArray autoSizePresetTextSizes = a.getResources()
+ .obtainTypedArray(autoSizeStepSizeArrayResId);
+ setupAutoSizeUniformPresetSizes(autoSizePresetTextSizes);
+ autoSizePresetTextSizes.recycle();
+ }
+ break;
+ case com.android.internal.R.styleable.TextView_justificationMode:
+ mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE);
+ break;
+ }
+ }
+
+ a.recycle();
+
+ BufferType bufferType = BufferType.EDITABLE;
+
+ final int variation =
+ inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+ final boolean passwordInputType = variation
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+ final boolean webPasswordInputType = variation
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+ final boolean numberPasswordInputType = variation
+ == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
+
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O;
+ // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES.
+ // STOPSHIP if the above TODO is not done.
+ mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT;
+
+ if (inputMethod != null) {
+ Class<?> c;
+
+ try {
+ c = Class.forName(inputMethod.toString());
+ } catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ try {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = (KeyListener) c.newInstance();
+ } catch (InstantiationException ex) {
+ throw new RuntimeException(ex);
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ try {
+ mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
+ ? inputType
+ : mEditor.mKeyListener.getInputType();
+ } catch (IncompatibleClassChangeError e) {
+ mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ }
+ } else if (digits != null) {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = DigitsKeyListener.getInstance(digits.toString());
+ // If no input type was specified, we will default to generic
+ // text, since we can't tell the IME about the set of digits
+ // that was selected.
+ mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
+ ? inputType : EditorInfo.TYPE_CLASS_TEXT;
+ } else if (inputType != EditorInfo.TYPE_NULL) {
+ setInputType(inputType, true);
+ // If set, the input type overrides what was set using the deprecated singleLine flag.
+ singleLine = !isMultilineInputType(inputType);
+ } else if (phone) {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = DialerKeyListener.getInstance();
+ mEditor.mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
+ } else if (numeric != 0) {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = DigitsKeyListener.getInstance(
+ null, // locale
+ (numeric & SIGNED) != 0,
+ (numeric & DECIMAL) != 0);
+ inputType = mEditor.mKeyListener.getInputType();
+ mEditor.mInputType = inputType;
+ } else if (autotext || autocap != -1) {
+ TextKeyListener.Capitalize cap;
+
+ inputType = EditorInfo.TYPE_CLASS_TEXT;
+
+ switch (autocap) {
+ case 1:
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ break;
+
+ case 2:
+ cap = TextKeyListener.Capitalize.WORDS;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+ break;
+
+ case 3:
+ cap = TextKeyListener.Capitalize.CHARACTERS;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ break;
+
+ default:
+ cap = TextKeyListener.Capitalize.NONE;
+ break;
+ }
+
+ createEditorIfNeeded();
+ mEditor.mKeyListener = TextKeyListener.getInstance(autotext, cap);
+ mEditor.mInputType = inputType;
+ } else if (editable) {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = TextKeyListener.getInstance();
+ mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ } else if (isTextSelectable()) {
+ // Prevent text changes from keyboard.
+ if (mEditor != null) {
+ mEditor.mKeyListener = null;
+ mEditor.mInputType = EditorInfo.TYPE_NULL;
+ }
+ bufferType = BufferType.SPANNABLE;
+ // So that selection can be changed using arrow keys and touch is handled.
+ setMovementMethod(ArrowKeyMovementMethod.getInstance());
+ } else {
+ if (mEditor != null) mEditor.mKeyListener = null;
+
+ switch (buffertype) {
+ case 0:
+ bufferType = BufferType.NORMAL;
+ break;
+ case 1:
+ bufferType = BufferType.SPANNABLE;
+ break;
+ case 2:
+ bufferType = BufferType.EDITABLE;
+ break;
+ }
+ }
+
+ if (mEditor != null) {
+ mEditor.adjustInputType(password, passwordInputType, webPasswordInputType,
+ numberPasswordInputType);
+ }
+
+ if (selectallonfocus) {
+ createEditorIfNeeded();
+ mEditor.mSelectAllOnFocus = true;
+
+ if (bufferType == BufferType.NORMAL) {
+ bufferType = BufferType.SPANNABLE;
+ }
+ }
+
+ // Set up the tint (if needed) before setting the drawables so that it
+ // gets applied correctly.
+ if (drawableTint != null || drawableTintMode != null) {
+ if (mDrawables == null) {
+ mDrawables = new Drawables(context);
+ }
+ if (drawableTint != null) {
+ mDrawables.mTintList = drawableTint;
+ mDrawables.mHasTint = true;
+ }
+ if (drawableTintMode != null) {
+ mDrawables.mTintMode = drawableTintMode;
+ mDrawables.mHasTintMode = true;
+ }
+ }
+
+ // This call will save the initial left/right drawables
+ setCompoundDrawablesWithIntrinsicBounds(
+ drawableLeft, drawableTop, drawableRight, drawableBottom);
+ setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
+ setCompoundDrawablePadding(drawablePadding);
+
+ // Same as setSingleLine(), but make sure the transformation method and the maximum number
+ // of lines of height are unchanged for multi-line TextViews.
+ setInputTypeSingleLine(singleLine);
+ applySingleLine(singleLine, singleLine, singleLine);
+
+ if (singleLine && getKeyListener() == null && ellipsize == ELLIPSIZE_NOT_SET) {
+ ellipsize = ELLIPSIZE_END;
+ }
+
+ switch (ellipsize) {
+ case ELLIPSIZE_START:
+ setEllipsize(TextUtils.TruncateAt.START);
+ break;
+ case ELLIPSIZE_MIDDLE:
+ setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ break;
+ case ELLIPSIZE_END:
+ setEllipsize(TextUtils.TruncateAt.END);
+ break;
+ case ELLIPSIZE_MARQUEE:
+ if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
+ setHorizontalFadingEdgeEnabled(true);
+ mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+ } else {
+ setHorizontalFadingEdgeEnabled(false);
+ mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+ }
+ setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ break;
+ }
+
+ final boolean isPassword = password || passwordInputType || webPasswordInputType
+ || numberPasswordInputType;
+ final boolean isMonospaceEnforced = isPassword || (mEditor != null
+ && (mEditor.mInputType
+ & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD));
+ if (isMonospaceEnforced) {
+ attributes.mTypefaceIndex = MONOSPACE;
+ }
+
+ applyTextAppearance(attributes);
+
+ if (isPassword) {
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ }
+
+ if (maxlength >= 0) {
+ setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
+ } else {
+ setFilters(NO_FILTERS);
+ }
+
+ setText(text, bufferType);
+ if (fromResourceId) {
+ mTextFromResource = true;
+ }
+
+ if (hint != null) setHint(hint);
+
+ /*
+ * Views are not normally clickable unless specified to be.
+ * However, TextViews that have input or movement methods *are*
+ * clickable by default. By setting clickable here, we implicitly set focusable as well
+ * if not overridden by the developer.
+ */
+ a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
+ boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
+ boolean clickable = canInputOrMove || isClickable();
+ boolean longClickable = canInputOrMove || isLongClickable();
+ int focusable = getFocusable();
+
+ 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:
+ TypedValue val = new TypedValue();
+ if (a.getValue(attr, val)) {
+ focusable = (val.type == TypedValue.TYPE_INT_BOOLEAN)
+ ? (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE)
+ : val.data;
+ }
+ 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();
+
+ // Some apps were relying on the undefined behavior of focusable winning over
+ // focusableInTouchMode != focusable in TextViews if both were specified in XML (usually
+ // when starting with EditText and setting only focusable=false). To keep those apps from
+ // breaking, re-apply the focusable attribute here.
+ if (focusable != getFocusable()) {
+ setFocusable(focusable);
+ }
+ setClickable(clickable);
+ setLongClickable(longClickable);
+
+ if (mEditor != null) mEditor.prepareCursorControllers();
+
+ // If not explicitly specified this view is important for accessibility.
+ if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ if (supportsAutoSizeText()) {
+ if (mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
+ // If uniform auto-size has been specified but preset values have not been set then
+ // replace the auto-size configuration values that have not been specified with the
+ // defaults.
+ if (!mHasPresetAutoSizeValues) {
+ final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+
+ if (autoSizeMinTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+ autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
+ displayMetrics);
+ }
+
+ if (autoSizeMaxTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+ autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
+ displayMetrics);
+ }
+
+ if (autoSizeStepGranularityInPx
+ == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+ autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX;
+ }
+
+ validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
+ autoSizeMaxTextSizeInPx,
+ autoSizeStepGranularityInPx);
+ }
+
+ setupAutoSizeText();
+ }
+ } else {
+ mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+ }
+ }
+
+ /**
+ * Specify whether this widget should automatically scale the text to try to perfectly fit
+ * within the layout bounds by using the default auto-size configuration.
+ *
+ * @param autoSizeTextType the type of auto-size. Must be one of
+ * {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
+ * {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
+ *
+ * @throws IllegalArgumentException if <code>autoSizeTextType</code> is none of the types above.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ *
+ * @see #getAutoSizeTextType()
+ */
+ public void setAutoSizeTextTypeWithDefaults(@AutoSizeTextType int autoSizeTextType) {
+ if (supportsAutoSizeText()) {
+ switch (autoSizeTextType) {
+ case AUTO_SIZE_TEXT_TYPE_NONE:
+ clearAutoSizeConfiguration();
+ break;
+ case AUTO_SIZE_TEXT_TYPE_UNIFORM:
+ final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
+ displayMetrics);
+ final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
+ displayMetrics);
+
+ validateAndSetAutoSizeTextTypeUniformConfiguration(
+ autoSizeMinTextSizeInPx,
+ autoSizeMaxTextSizeInPx,
+ DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX);
+ if (setupAutoSizeText()) {
+ autoSizeText();
+ invalidate();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown auto-size text type: " + autoSizeTextType);
+ }
+ }
+ }
+
+ /**
+ * Specify whether this widget should automatically scale the text to try to perfectly fit
+ * within the layout bounds. If all the configuration params are valid the type of auto-size is
+ * set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
+ *
+ * @param autoSizeMinTextSize the minimum text size available for auto-size
+ * @param autoSizeMaxTextSize the maximum text size available for auto-size
+ * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
+ * the minimum and maximum text size in order to build the set of
+ * text sizes the system uses to choose from when auto-sizing
+ * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
+ * possible dimension units
+ *
+ * @throws IllegalArgumentException if any of the configuration params are invalid.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+ *
+ * @see #setAutoSizeTextTypeWithDefaults(int)
+ * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+ * @see #getAutoSizeMinTextSize()
+ * @see #getAutoSizeMaxTextSize()
+ * @see #getAutoSizeStepGranularity()
+ * @see #getAutoSizeTextAvailableSizes()
+ */
+ public void setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize,
+ int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit) {
+ if (supportsAutoSizeText()) {
+ final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+ unit, autoSizeMinTextSize, displayMetrics);
+ final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+ unit, autoSizeMaxTextSize, displayMetrics);
+ final float autoSizeStepGranularityInPx = TypedValue.applyDimension(
+ unit, autoSizeStepGranularity, displayMetrics);
+
+ validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
+ autoSizeMaxTextSizeInPx,
+ autoSizeStepGranularityInPx);
+
+ if (setupAutoSizeText()) {
+ autoSizeText();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Specify whether this widget should automatically scale the text to try to perfectly fit
+ * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
+ * then the type of auto-size is set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
+ *
+ * @param presetSizes an {@code int} array of sizes in pixels
+ * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
+ * the possible dimension units
+ *
+ * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ * @attr ref android.R.styleable#TextView_autoSizePresetSizes
+ *
+ * @see #setAutoSizeTextTypeWithDefaults(int)
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ * @see #getAutoSizeMinTextSize()
+ * @see #getAutoSizeMaxTextSize()
+ * @see #getAutoSizeTextAvailableSizes()
+ */
+ public void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit) {
+ if (supportsAutoSizeText()) {
+ final int presetSizesLength = presetSizes.length;
+ if (presetSizesLength > 0) {
+ int[] presetSizesInPx = new int[presetSizesLength];
+
+ if (unit == TypedValue.COMPLEX_UNIT_PX) {
+ presetSizesInPx = Arrays.copyOf(presetSizes, presetSizesLength);
+ } else {
+ final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ // Convert all to sizes to pixels.
+ for (int i = 0; i < presetSizesLength; i++) {
+ presetSizesInPx[i] = Math.round(TypedValue.applyDimension(unit,
+ presetSizes[i], displayMetrics));
+ }
+ }
+
+ mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(presetSizesInPx);
+ if (!setupAutoSizeUniformPresetSizesConfiguration()) {
+ throw new IllegalArgumentException("None of the preset sizes is valid: "
+ + Arrays.toString(presetSizes));
+ }
+ } else {
+ mHasPresetAutoSizeValues = false;
+ }
+
+ if (setupAutoSizeText()) {
+ autoSizeText();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Returns the type of auto-size set for this widget.
+ *
+ * @return an {@code int} corresponding to one of the auto-size types:
+ * {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
+ * {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ *
+ * @see #setAutoSizeTextTypeWithDefaults(int)
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+ */
+ @AutoSizeTextType
+ public int getAutoSizeTextType() {
+ return mAutoSizeTextType;
+ }
+
+ /**
+ * @return the current auto-size step granularity in pixels.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+ *
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ */
+ public int getAutoSizeStepGranularity() {
+ return Math.round(mAutoSizeStepGranularityInPx);
+ }
+
+ /**
+ * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
+ * if auto-size has not been configured this function returns {@code -1}.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+ *
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+ */
+ public int getAutoSizeMinTextSize() {
+ return Math.round(mAutoSizeMinTextSizeInPx);
+ }
+
+ /**
+ * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
+ * if auto-size has not been configured this function returns {@code -1}.
+ *
+ * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+ *
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+ */
+ public int getAutoSizeMaxTextSize() {
+ return Math.round(mAutoSizeMaxTextSizeInPx);
+ }
+
+ /**
+ * @return the current auto-size {@code int} sizes array (in pixels).
+ *
+ * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+ * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+ */
+ public int[] getAutoSizeTextAvailableSizes() {
+ return mAutoSizeTextSizesInPx;
+ }
+
+ private void setupAutoSizeUniformPresetSizes(TypedArray textSizes) {
+ final int textSizesLength = textSizes.length();
+ final int[] parsedSizes = new int[textSizesLength];
+
+ if (textSizesLength > 0) {
+ for (int i = 0; i < textSizesLength; i++) {
+ parsedSizes[i] = textSizes.getDimensionPixelSize(i, -1);
+ }
+ mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(parsedSizes);
+ setupAutoSizeUniformPresetSizesConfiguration();
+ }
+ }
+
+ private boolean setupAutoSizeUniformPresetSizesConfiguration() {
+ final int sizesLength = mAutoSizeTextSizesInPx.length;
+ mHasPresetAutoSizeValues = sizesLength > 0;
+ if (mHasPresetAutoSizeValues) {
+ mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
+ mAutoSizeMinTextSizeInPx = mAutoSizeTextSizesInPx[0];
+ mAutoSizeMaxTextSizeInPx = mAutoSizeTextSizesInPx[sizesLength - 1];
+ mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ }
+ return mHasPresetAutoSizeValues;
+ }
+
+ /**
+ * If all params are valid then save the auto-size configuration.
+ *
+ * @throws IllegalArgumentException if any of the params are invalid
+ */
+ private void validateAndSetAutoSizeTextTypeUniformConfiguration(float autoSizeMinTextSizeInPx,
+ float autoSizeMaxTextSizeInPx, float autoSizeStepGranularityInPx) {
+ // First validate.
+ if (autoSizeMinTextSizeInPx <= 0) {
+ throw new IllegalArgumentException("Minimum auto-size text size ("
+ + autoSizeMinTextSizeInPx + "px) is less or equal to (0px)");
+ }
+
+ if (autoSizeMaxTextSizeInPx <= autoSizeMinTextSizeInPx) {
+ throw new IllegalArgumentException("Maximum auto-size text size ("
+ + autoSizeMaxTextSizeInPx + "px) is less or equal to minimum auto-size "
+ + "text size (" + autoSizeMinTextSizeInPx + "px)");
+ }
+
+ if (autoSizeStepGranularityInPx <= 0) {
+ throw new IllegalArgumentException("The auto-size step granularity ("
+ + autoSizeStepGranularityInPx + "px) is less or equal to (0px)");
+ }
+
+ // All good, persist the configuration.
+ mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
+ mAutoSizeMinTextSizeInPx = autoSizeMinTextSizeInPx;
+ mAutoSizeMaxTextSizeInPx = autoSizeMaxTextSizeInPx;
+ mAutoSizeStepGranularityInPx = autoSizeStepGranularityInPx;
+ mHasPresetAutoSizeValues = false;
+ }
+
+ private void clearAutoSizeConfiguration() {
+ mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+ mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+ mAutoSizeTextSizesInPx = EmptyArray.INT;
+ mNeedsAutoSizeText = false;
+ }
+
+ // Returns distinct sorted positive values.
+ private int[] cleanupAutoSizePresetSizes(int[] presetValues) {
+ final int presetValuesLength = presetValues.length;
+ if (presetValuesLength == 0) {
+ return presetValues;
+ }
+ Arrays.sort(presetValues);
+
+ final IntArray uniqueValidSizes = new IntArray();
+ for (int i = 0; i < presetValuesLength; i++) {
+ final int currentPresetValue = presetValues[i];
+
+ if (currentPresetValue > 0
+ && uniqueValidSizes.binarySearch(currentPresetValue) < 0) {
+ uniqueValidSizes.add(currentPresetValue);
+ }
+ }
+
+ return presetValuesLength == uniqueValidSizes.size()
+ ? presetValues
+ : uniqueValidSizes.toArray();
+ }
+
+ private boolean setupAutoSizeText() {
+ if (supportsAutoSizeText() && mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
+ // Calculate the sizes set based on minimum size, maximum size and step size if we do
+ // not have a predefined set of sizes or if the current sizes array is empty.
+ if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
+ int autoSizeValuesLength = 1;
+ float currentSize = Math.round(mAutoSizeMinTextSizeInPx);
+ while (Math.round(currentSize + mAutoSizeStepGranularityInPx)
+ <= Math.round(mAutoSizeMaxTextSizeInPx)) {
+ autoSizeValuesLength++;
+ currentSize += mAutoSizeStepGranularityInPx;
+ }
+
+ int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
+ float sizeToAdd = mAutoSizeMinTextSizeInPx;
+ for (int i = 0; i < autoSizeValuesLength; i++) {
+ autoSizeTextSizesInPx[i] = Math.round(sizeToAdd);
+ sizeToAdd += mAutoSizeStepGranularityInPx;
+ }
+ mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
+ }
+
+ mNeedsAutoSizeText = true;
+ } else {
+ mNeedsAutoSizeText = false;
+ }
+
+ return mNeedsAutoSizeText;
+ }
+
+ private int[] parseDimensionArray(TypedArray dimens) {
+ if (dimens == null) {
+ return null;
+ }
+ int[] result = new int[dimens.length()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = dimens.getDimensionPixelSize(i, 0);
+ }
+ return result;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == PROCESS_TEXT_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
+ if (result != null) {
+ if (isTextEditable()) {
+ replaceSelectionWithText(result);
+ if (mEditor != null) {
+ mEditor.refreshTextActionMode();
+ }
+ } else {
+ if (result.length() > 0) {
+ Toast.makeText(getContext(), String.valueOf(result), Toast.LENGTH_LONG)
+ .show();
+ }
+ }
+ }
+ } else if (mText instanceof Spannable) {
+ // Reset the selection.
+ Selection.setSelection((Spannable) mText, getSelectionEnd());
+ }
+ }
+ }
+
+ private void setTypefaceFromAttrs(Typeface fontTypeface, String familyName, int typefaceIndex,
+ int styleIndex) {
+ Typeface tf = fontTypeface;
+ if (tf == null && familyName != null) {
+ tf = Typeface.create(familyName, styleIndex);
+ } else if (tf != null && tf.getStyle() != styleIndex) {
+ tf = Typeface.create(tf, styleIndex);
+ }
+ if (tf != null) {
+ setTypeface(tf);
+ return;
+ }
+ switch (typefaceIndex) {
+ case SANS:
+ tf = Typeface.SANS_SERIF;
+ break;
+
+ case SERIF:
+ tf = Typeface.SERIF;
+ break;
+
+ case MONOSPACE:
+ tf = Typeface.MONOSPACE;
+ break;
+ }
+
+ setTypeface(tf, styleIndex);
+ }
+
+ private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
+ boolean hasRelativeDrawables = (start != null) || (end != null);
+ if (hasRelativeDrawables) {
+ Drawables dr = mDrawables;
+ if (dr == null) {
+ mDrawables = dr = new Drawables(getContext());
+ }
+ mDrawables.mOverride = true;
+ final Rect compoundRect = dr.mCompoundRect;
+ int[] state = getDrawableState();
+ if (start != null) {
+ start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
+ start.setState(state);
+ start.copyBounds(compoundRect);
+ start.setCallback(this);
+
+ dr.mDrawableStart = start;
+ dr.mDrawableSizeStart = compoundRect.width();
+ dr.mDrawableHeightStart = compoundRect.height();
+ } else {
+ dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+ }
+ if (end != null) {
+ end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
+ end.setState(state);
+ end.copyBounds(compoundRect);
+ end.setCallback(this);
+
+ dr.mDrawableEnd = end;
+ dr.mDrawableSizeEnd = compoundRect.width();
+ dr.mDrawableHeightEnd = compoundRect.height();
+ } else {
+ dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+ }
+ resetResolvedDrawables();
+ resolveDrawables();
+ applyCompoundDrawableTint();
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == isEnabled()) {
+ return;
+ }
+
+ if (!enabled) {
+ // Hide the soft input if the currently active TextView is disabled
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ }
+
+ super.setEnabled(enabled);
+
+ if (enabled) {
+ // Make sure IME is updated with current editor info.
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ }
+
+ // Will change text color
+ if (mEditor != null) {
+ mEditor.invalidateTextDisplayList();
+ mEditor.prepareCursorControllers();
+
+ // start or stop the cursor blinking as appropriate
+ mEditor.makeBlink();
+ }
+ }
+
+ /**
+ * 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 that TextView is displaying. If {@link #setText(CharSequence)} was called
+ * with an argument of {@link android.widget.TextView.BufferType#SPANNABLE BufferType.SPANNABLE}
+ * or {@link android.widget.TextView.BufferType#EDITABLE BufferType.EDITABLE}, you can cast
+ * the return value from this method to Spannable or Editable, respectively.
+ *
+ * <p>The content of the return value should not be modified. If you want a modifiable one, you
+ * should make your own copy first.</p>
+ *
+ * @return The text displayed by the text view.
+ * @attr ref android.R.styleable#TextView_text
+ */
+ @ViewDebug.CapturedViewProperty
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the length, in characters, of the text managed by this TextView
+ * @return The length of the text managed by the TextView in characters.
+ */
+ public int length() {
+ return mText.length();
+ }
+
+ /**
+ * Return the text that TextView is displaying as an Editable object. If the text is not
+ * editable, null is returned.
+ *
+ * @see #getText
+ */
+ public Editable getEditableText() {
+ return (mText instanceof Editable) ? (Editable) mText : null;
+ }
+
+ /**
+ * Gets the vertical distance between lines of text, 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.
+ * @return The height of one standard line in pixels.
+ */
+ public int getLineHeight() {
+ return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
+ }
+
+ /**
+ * Gets the {@link android.text.Layout} that is currently being used to display the text.
+ * This value can be null if the text or width has recently changed.
+ * @return The Layout that is currently being used to display the text.
+ */
+ public final Layout getLayout() {
+ return mLayout;
+ }
+
+ /**
+ * @return the {@link android.text.Layout} that is currently being used to
+ * display the hint text. This can be null.
+ */
+ final Layout getHintLayout() {
+ return mHintLayout;
+ }
+
+ /**
+ * Retrieve the {@link android.content.UndoManager} that is currently associated
+ * with this TextView. By default there is no associated UndoManager, so null
+ * is returned. One can be associated with the TextView through
+ * {@link #setUndoManager(android.content.UndoManager, String)}
+ *
+ * @hide
+ */
+ public final UndoManager getUndoManager() {
+ // TODO: Consider supporting a global undo manager.
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public final Editor getEditorForTesting() {
+ return mEditor;
+ }
+
+ /**
+ * Associate an {@link android.content.UndoManager} with this TextView. Once
+ * done, all edit operations on the TextView will result in appropriate
+ * {@link android.content.UndoOperation} objects pushed on the given UndoManager's
+ * stack.
+ *
+ * @param undoManager The {@link android.content.UndoManager} to associate with
+ * this TextView, or null to clear any existing association.
+ * @param tag String tag identifying this particular TextView owner in the
+ * UndoManager. This is used to keep the correct association with the
+ * {@link android.content.UndoOwner} of any operations inside of the UndoManager.
+ *
+ * @hide
+ */
+ public final void setUndoManager(UndoManager undoManager, String tag) {
+ // TODO: Consider supporting a global undo manager. An implementation will need to:
+ // * createEditorIfNeeded()
+ // * Promote to BufferType.EDITABLE if needed.
+ // * Update the UndoManager and UndoOwner.
+ // Likewise it will need to be able to restore the default UndoManager.
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ /**
+ * Gets the current {@link KeyListener} for the TextView.
+ * This will frequently be null for non-EditText TextViews.
+ * @return the current key listener for this TextView.
+ *
+ * @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 final KeyListener getKeyListener() {
+ return mEditor == null ? null : mEditor.mKeyListener;
+ }
+
+ /**
+ * Sets the key listener to be used with this TextView. This can be null
+ * to disallow user input. Note that this method has significant and
+ * subtle interactions with soft keyboards and other input method:
+ * see {@link KeyListener#getInputType() KeyListener.getContentType()}
+ * for important details. Calling this method will replace the current
+ * content type of the text view with the content type returned by the
+ * key listener.
+ * <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) {
+ mListenerChanged = true;
+ setKeyListenerOnly(input);
+ fixFocusableAndClickableSettings();
+
+ if (input != null) {
+ createEditorIfNeeded();
+ setInputTypeFromEditor();
+ } else {
+ if (mEditor != null) mEditor.mInputType = EditorInfo.TYPE_NULL;
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ }
+
+ private void setInputTypeFromEditor() {
+ try {
+ mEditor.mInputType = mEditor.mKeyListener.getInputType();
+ } catch (IncompatibleClassChangeError e) {
+ mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ }
+ // Change inputType, without affecting transformation.
+ // No need to applySingleLine since mSingleLine is unchanged.
+ setInputTypeSingleLine(mSingleLine);
+ }
+
+ private void setKeyListenerOnly(KeyListener input) {
+ if (mEditor == null && input == null) return; // null is the default value
+
+ createEditorIfNeeded();
+ if (mEditor.mKeyListener != input) {
+ mEditor.mKeyListener = input;
+ if (input != null && !(mText instanceof Editable)) {
+ setText(mText);
+ }
+
+ setFilters((Editable) mText, mFilters);
+ }
+ }
+
+ /**
+ * Gets the {@link android.text.method.MovementMethod} being used for this TextView,
+ * which provides positioning, scrolling, and text selection functionality.
+ * This will frequently be null for non-EditText TextViews.
+ * @return the movement method being used for this TextView.
+ * @see android.text.method.MovementMethod
+ */
+ public final MovementMethod getMovementMethod() {
+ return mMovement;
+ }
+
+ /**
+ * Sets the {@link android.text.method.MovementMethod} for handling arrow key movement
+ * 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) {
+ if (mMovement != movement) {
+ mMovement = movement;
+
+ if (movement != null && !(mText instanceof Spannable)) {
+ setText(mText);
+ }
+
+ fixFocusableAndClickableSettings();
+
+ // SelectionModifierCursorController depends on textCanBeSelected, which depends on
+ // mMovement
+ if (mEditor != null) mEditor.prepareCursorControllers();
+ }
+ }
+
+ private void fixFocusableAndClickableSettings() {
+ if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
+ setFocusable(FOCUSABLE);
+ setClickable(true);
+ setLongClickable(true);
+ } else {
+ setFocusable(FOCUSABLE_AUTO);
+ setClickable(false);
+ setLongClickable(false);
+ }
+ }
+
+ /**
+ * Gets the current {@link android.text.method.TransformationMethod} for the TextView.
+ * This is frequently null, except for single-line and password fields.
+ * @return the current transformation method for this TextView.
+ *
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ 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 (method == mTransformation) {
+ // Avoid the setText() below if the transformation is
+ // the same.
+ return;
+ }
+ if (mTransformation != null) {
+ if (mText instanceof Spannable) {
+ ((Spannable) mText).removeSpan(mTransformation);
+ }
+ }
+
+ mTransformation = method;
+
+ if (method instanceof TransformationMethod2) {
+ TransformationMethod2 method2 = (TransformationMethod2) method;
+ mAllowTransformationLengthChange = !isTextSelectable() && !(mText instanceof Editable);
+ method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
+ } else {
+ mAllowTransformationLengthChange = false;
+ }
+
+ setText(mText);
+
+ if (hasPasswordTransformationMethod()) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ // PasswordTransformationMethod always have LTR text direction heuristics returned by
+ // getTextDirectionHeuristic, needs reset
+ mTextDir = getTextDirectionHeuristic();
+ }
+
+ /**
+ * Returns the top padding of the view, plus space for the top
+ * Drawable if any.
+ */
+ public int getCompoundPaddingTop() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mShowing[Drawables.TOP] == null) {
+ return mPaddingTop;
+ } else {
+ return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
+ }
+ }
+
+ /**
+ * Returns the bottom padding of the view, plus space for the bottom
+ * Drawable if any.
+ */
+ public int getCompoundPaddingBottom() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mShowing[Drawables.BOTTOM] == null) {
+ return mPaddingBottom;
+ } else {
+ return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
+ }
+ }
+
+ /**
+ * Returns the left padding of the view, plus space for the left
+ * Drawable if any.
+ */
+ public int getCompoundPaddingLeft() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mShowing[Drawables.LEFT] == null) {
+ return mPaddingLeft;
+ } else {
+ return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
+ }
+ }
+
+ /**
+ * Returns the right padding of the view, plus space for the right
+ * Drawable if any.
+ */
+ public int getCompoundPaddingRight() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mShowing[Drawables.RIGHT] == null) {
+ return mPaddingRight;
+ } else {
+ return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
+ }
+ }
+
+ /**
+ * Returns the start padding of the view, plus space for the start
+ * Drawable if any.
+ */
+ public int getCompoundPaddingStart() {
+ resolveDrawables();
+ switch(getLayoutDirection()) {
+ default:
+ case LAYOUT_DIRECTION_LTR:
+ return getCompoundPaddingLeft();
+ case LAYOUT_DIRECTION_RTL:
+ return getCompoundPaddingRight();
+ }
+ }
+
+ /**
+ * Returns the end padding of the view, plus space for the end
+ * Drawable if any.
+ */
+ public int getCompoundPaddingEnd() {
+ resolveDrawables();
+ switch(getLayoutDirection()) {
+ default:
+ case LAYOUT_DIRECTION_LTR:
+ return getCompoundPaddingRight();
+ case LAYOUT_DIRECTION_RTL:
+ return getCompoundPaddingLeft();
+ }
+ }
+
+ /**
+ * 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 == null) {
+ assumeLayout();
+ }
+
+ 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 == null) {
+ assumeLayout();
+ }
+
+ 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 start padding of the view, including the start
+ * Drawable if any.
+ */
+ public int getTotalPaddingStart() {
+ return getCompoundPaddingStart();
+ }
+
+ /**
+ * Returns the total end padding of the view, including the end
+ * Drawable if any.
+ */
+ public int getTotalPaddingEnd() {
+ return getCompoundPaddingEnd();
+ }
+
+ /**
+ * 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 {@code null} if you do not want a
+ * Drawable there. The Drawables must already have had
+ * {@link Drawable#setBounds} called.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @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(@Nullable Drawable left, @Nullable Drawable top,
+ @Nullable Drawable right, @Nullable Drawable bottom) {
+ Drawables dr = mDrawables;
+
+ // We're switching to absolute, discard relative.
+ if (dr != null) {
+ if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
+ dr.mDrawableStart = null;
+ if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
+ dr.mDrawableEnd = null;
+ dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+ dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+ }
+
+ final boolean drawables = left != null || top != null || right != null || bottom != null;
+ if (!drawables) {
+ // Clearing drawables... can we free the data structure?
+ if (dr != null) {
+ if (!dr.hasMetadata()) {
+ mDrawables = null;
+ } else {
+ // We need to retain the last set padding, so just clear
+ // out all of the fields in the existing structure.
+ for (int i = dr.mShowing.length - 1; i >= 0; i--) {
+ if (dr.mShowing[i] != null) {
+ dr.mShowing[i].setCallback(null);
+ }
+ dr.mShowing[i] = null;
+ }
+ dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+ dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+ } else {
+ if (dr == null) {
+ mDrawables = dr = new Drawables(getContext());
+ }
+
+ mDrawables.mOverride = false;
+
+ if (dr.mShowing[Drawables.LEFT] != left && dr.mShowing[Drawables.LEFT] != null) {
+ dr.mShowing[Drawables.LEFT].setCallback(null);
+ }
+ dr.mShowing[Drawables.LEFT] = left;
+
+ if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
+ dr.mShowing[Drawables.TOP].setCallback(null);
+ }
+ dr.mShowing[Drawables.TOP] = top;
+
+ if (dr.mShowing[Drawables.RIGHT] != right && dr.mShowing[Drawables.RIGHT] != null) {
+ dr.mShowing[Drawables.RIGHT].setCallback(null);
+ }
+ dr.mShowing[Drawables.RIGHT] = right;
+
+ if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
+ dr.mShowing[Drawables.BOTTOM].setCallback(null);
+ }
+ dr.mShowing[Drawables.BOTTOM] = bottom;
+
+ final Rect compoundRect = dr.mCompoundRect;
+ int[] state;
+
+ state = getDrawableState();
+
+ if (left != null) {
+ left.setState(state);
+ left.copyBounds(compoundRect);
+ left.setCallback(this);
+ dr.mDrawableSizeLeft = compoundRect.width();
+ dr.mDrawableHeightLeft = compoundRect.height();
+ } else {
+ dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+ }
+
+ if (right != null) {
+ right.setState(state);
+ right.copyBounds(compoundRect);
+ right.setCallback(this);
+ dr.mDrawableSizeRight = compoundRect.width();
+ dr.mDrawableHeightRight = compoundRect.height();
+ } else {
+ dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+ }
+
+ if (top != null) {
+ top.setState(state);
+ top.copyBounds(compoundRect);
+ top.setCallback(this);
+ dr.mDrawableSizeTop = compoundRect.height();
+ dr.mDrawableWidthTop = compoundRect.width();
+ } else {
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ }
+
+ if (bottom != null) {
+ bottom.setState(state);
+ bottom.copyBounds(compoundRect);
+ bottom.setCallback(this);
+ dr.mDrawableSizeBottom = compoundRect.height();
+ dr.mDrawableWidthBottom = compoundRect.width();
+ } else {
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+
+ // Save initial left/right drawables
+ if (dr != null) {
+ dr.mDrawableLeftInitial = left;
+ dr.mDrawableRightInitial = right;
+ }
+
+ resetResolvedDrawables();
+ resolveDrawables();
+ applyCompoundDrawableTint();
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above, to the
+ * right of, and below the text. Use 0 if you do not want a Drawable there.
+ * The Drawables' bounds will be set to their intrinsic bounds.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @param left Resource identifier of the left Drawable.
+ * @param top Resource identifier of the top Drawable.
+ * @param right Resource identifier of the right Drawable.
+ * @param bottom Resource identifier of the bottom Drawable.
+ *
+ * @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
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablesWithIntrinsicBounds(@DrawableRes int left,
+ @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+ final Context context = getContext();
+ setCompoundDrawablesWithIntrinsicBounds(left != 0 ? context.getDrawable(left) : null,
+ top != 0 ? context.getDrawable(top) : null,
+ right != 0 ? context.getDrawable(right) : null,
+ bottom != 0 ? context.getDrawable(bottom) : null);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above, to the
+ * right of, and below the text. Use {@code null} if you do not want a
+ * Drawable there. The Drawables' bounds will be set to their intrinsic
+ * bounds.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @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
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
+ @Nullable Drawable top, @Nullable Drawable right, @Nullable 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);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use {@code null} if you do not want a Drawable
+ * there. The Drawables must already have had {@link Drawable#setBounds}
+ * called.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top,
+ @Nullable Drawable end, @Nullable Drawable bottom) {
+ Drawables dr = mDrawables;
+
+ // We're switching to relative, discard absolute.
+ if (dr != null) {
+ if (dr.mShowing[Drawables.LEFT] != null) {
+ dr.mShowing[Drawables.LEFT].setCallback(null);
+ }
+ dr.mShowing[Drawables.LEFT] = dr.mDrawableLeftInitial = null;
+ if (dr.mShowing[Drawables.RIGHT] != null) {
+ dr.mShowing[Drawables.RIGHT].setCallback(null);
+ }
+ dr.mShowing[Drawables.RIGHT] = dr.mDrawableRightInitial = null;
+ dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+ dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+ }
+
+ final boolean drawables = start != null || top != null
+ || end != null || bottom != null;
+
+ if (!drawables) {
+ // Clearing drawables... can we free the data structure?
+ if (dr != null) {
+ if (!dr.hasMetadata()) {
+ mDrawables = null;
+ } else {
+ // We need to retain the last set padding, so just clear
+ // out all of the fields in the existing structure.
+ if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
+ dr.mDrawableStart = null;
+ if (dr.mShowing[Drawables.TOP] != null) {
+ dr.mShowing[Drawables.TOP].setCallback(null);
+ }
+ dr.mShowing[Drawables.TOP] = null;
+ if (dr.mDrawableEnd != null) {
+ dr.mDrawableEnd.setCallback(null);
+ }
+ dr.mDrawableEnd = null;
+ if (dr.mShowing[Drawables.BOTTOM] != null) {
+ dr.mShowing[Drawables.BOTTOM].setCallback(null);
+ }
+ dr.mShowing[Drawables.BOTTOM] = null;
+ dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+ dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+ } else {
+ if (dr == null) {
+ mDrawables = dr = new Drawables(getContext());
+ }
+
+ mDrawables.mOverride = true;
+
+ if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
+ dr.mDrawableStart.setCallback(null);
+ }
+ dr.mDrawableStart = start;
+
+ if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
+ dr.mShowing[Drawables.TOP].setCallback(null);
+ }
+ dr.mShowing[Drawables.TOP] = top;
+
+ if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
+ dr.mDrawableEnd.setCallback(null);
+ }
+ dr.mDrawableEnd = end;
+
+ if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
+ dr.mShowing[Drawables.BOTTOM].setCallback(null);
+ }
+ dr.mShowing[Drawables.BOTTOM] = bottom;
+
+ final Rect compoundRect = dr.mCompoundRect;
+ int[] state;
+
+ state = getDrawableState();
+
+ if (start != null) {
+ start.setState(state);
+ start.copyBounds(compoundRect);
+ start.setCallback(this);
+ dr.mDrawableSizeStart = compoundRect.width();
+ dr.mDrawableHeightStart = compoundRect.height();
+ } else {
+ dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+ }
+
+ if (end != null) {
+ end.setState(state);
+ end.copyBounds(compoundRect);
+ end.setCallback(this);
+ dr.mDrawableSizeEnd = compoundRect.width();
+ dr.mDrawableHeightEnd = compoundRect.height();
+ } else {
+ dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+ }
+
+ if (top != null) {
+ top.setState(state);
+ top.copyBounds(compoundRect);
+ top.setCallback(this);
+ dr.mDrawableSizeTop = compoundRect.height();
+ dr.mDrawableWidthTop = compoundRect.width();
+ } else {
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ }
+
+ if (bottom != null) {
+ bottom.setState(state);
+ bottom.copyBounds(compoundRect);
+ bottom.setCallback(this);
+ dr.mDrawableSizeBottom = compoundRect.height();
+ dr.mDrawableWidthBottom = compoundRect.width();
+ } else {
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+
+ resetResolvedDrawables();
+ resolveDrawables();
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use 0 if you do not want a Drawable there. The
+ * Drawables' bounds will be set to their intrinsic bounds.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @param start Resource identifier of the start Drawable.
+ * @param top Resource identifier of the top Drawable.
+ * @param end Resource identifier of the end Drawable.
+ * @param bottom Resource identifier of the bottom Drawable.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablesRelativeWithIntrinsicBounds(@DrawableRes int start,
+ @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+ final Context context = getContext();
+ setCompoundDrawablesRelativeWithIntrinsicBounds(
+ start != 0 ? context.getDrawable(start) : null,
+ top != 0 ? context.getDrawable(top) : null,
+ end != 0 ? context.getDrawable(end) : null,
+ bottom != 0 ? context.getDrawable(bottom) : null);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use {@code null} if you do not want a Drawable
+ * there. The Drawables' bounds will be set to their intrinsic bounds.
+ * <p>
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablesRelativeWithIntrinsicBounds(@Nullable Drawable start,
+ @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) {
+
+ if (start != null) {
+ start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
+ }
+ if (end != null) {
+ end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
+ }
+ if (top != null) {
+ top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
+ }
+ if (bottom != null) {
+ bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
+ }
+ setCompoundDrawablesRelative(start, top, end, bottom);
+ }
+
+ /**
+ * Returns drawables for the left, top, right, and bottom borders.
+ *
+ * @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
+ */
+ @NonNull
+ public Drawable[] getCompoundDrawables() {
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ return dr.mShowing.clone();
+ } else {
+ return new Drawable[] { null, null, null, null };
+ }
+ }
+
+ /**
+ * Returns drawables for the start, top, end, and bottom borders.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @NonNull
+ public Drawable[] getCompoundDrawablesRelative() {
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ return new Drawable[] {
+ dr.mDrawableStart, dr.mShowing[Drawables.TOP],
+ dr.mDrawableEnd, dr.mShowing[Drawables.BOTTOM]
+ };
+ } else {
+ return new Drawable[] { null, null, null, null };
+ }
+ }
+
+ /**
+ * Sets the size of the padding between the compound drawables and
+ * the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ @android.view.RemotableViewMethod
+ public void setCompoundDrawablePadding(int pad) {
+ Drawables dr = mDrawables;
+ if (pad == 0) {
+ if (dr != null) {
+ dr.mDrawablePadding = pad;
+ }
+ } else {
+ if (dr == null) {
+ mDrawables = dr = new Drawables(getContext());
+ }
+ dr.mDrawablePadding = pad;
+ }
+
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Returns the padding between the compound drawables and the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ public int getCompoundDrawablePadding() {
+ final Drawables dr = mDrawables;
+ return dr != null ? dr.mDrawablePadding : 0;
+ }
+
+ /**
+ * Applies a tint to the compound drawables. Does not modify the
+ * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to
+ * {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)}
+ * and related methods will automatically mutate the drawables and apply
+ * the specified tint and tint mode using
+ * {@link Drawable#setTintList(ColorStateList)}.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @attr ref android.R.styleable#TextView_drawableTint
+ * @see #getCompoundDrawableTintList()
+ * @see Drawable#setTintList(ColorStateList)
+ */
+ public void setCompoundDrawableTintList(@Nullable ColorStateList tint) {
+ if (mDrawables == null) {
+ mDrawables = new Drawables(getContext());
+ }
+ mDrawables.mTintList = tint;
+ mDrawables.mHasTint = true;
+
+ applyCompoundDrawableTint();
+ }
+
+ /**
+ * @return the tint applied to the compound drawables
+ * @attr ref android.R.styleable#TextView_drawableTint
+ * @see #setCompoundDrawableTintList(ColorStateList)
+ */
+ public ColorStateList getCompoundDrawableTintList() {
+ return mDrawables != null ? mDrawables.mTintList : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setCompoundDrawableTintList(ColorStateList)} to the compound
+ * drawables. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @attr ref android.R.styleable#TextView_drawableTintMode
+ * @see #setCompoundDrawableTintList(ColorStateList)
+ * @see Drawable#setTintMode(PorterDuff.Mode)
+ */
+ public void setCompoundDrawableTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mDrawables == null) {
+ mDrawables = new Drawables(getContext());
+ }
+ mDrawables.mTintMode = tintMode;
+ mDrawables.mHasTintMode = true;
+
+ applyCompoundDrawableTint();
+ }
+
+ /**
+ * Returns the blending mode used to apply the tint to the compound
+ * drawables, if specified.
+ *
+ * @return the blending mode used to apply the tint to the compound
+ * drawables
+ * @attr ref android.R.styleable#TextView_drawableTintMode
+ * @see #setCompoundDrawableTintMode(PorterDuff.Mode)
+ */
+ public PorterDuff.Mode getCompoundDrawableTintMode() {
+ return mDrawables != null ? mDrawables.mTintMode : null;
+ }
+
+ private void applyCompoundDrawableTint() {
+ if (mDrawables == null) {
+ return;
+ }
+
+ if (mDrawables.mHasTint || mDrawables.mHasTintMode) {
+ final ColorStateList tintList = mDrawables.mTintList;
+ final PorterDuff.Mode tintMode = mDrawables.mTintMode;
+ final boolean hasTint = mDrawables.mHasTint;
+ final boolean hasTintMode = mDrawables.mHasTintMode;
+ final int[] state = getDrawableState();
+
+ for (Drawable dr : mDrawables.mShowing) {
+ if (dr == null) {
+ continue;
+ }
+
+ if (dr == mDrawables.mDrawableError) {
+ // From a developer's perspective, the error drawable isn't
+ // a compound drawable. Don't apply the generic compound
+ // drawable tint to it.
+ continue;
+ }
+
+ dr.mutate();
+
+ if (hasTint) {
+ dr.setTintList(tintList);
+ }
+
+ if (hasTintMode) {
+ dr.setTintMode(tintMode);
+ }
+
+ // The drawable (or one of its children) may not have been
+ // stateful before applying the tint, so let's try again.
+ if (dr.isStateful()) {
+ dr.setState(state);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ if (left != mPaddingLeft
+ || right != mPaddingRight
+ || top != mPaddingTop
+ || bottom != mPaddingBottom) {
+ nullLayouts();
+ }
+
+ // the super call will requestLayout()
+ super.setPadding(left, top, right, bottom);
+ invalidate();
+ }
+
+ @Override
+ public void setPaddingRelative(int start, int top, int end, int bottom) {
+ if (start != getPaddingStart()
+ || end != getPaddingEnd()
+ || top != mPaddingTop
+ || bottom != mPaddingBottom) {
+ nullLayouts();
+ }
+
+ // the super call will requestLayout()
+ super.setPaddingRelative(start, top, end, 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 appearance from the specified style resource.
+ * <p>
+ * Use a framework-defined {@code TextAppearance} style like
+ * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}
+ * or see {@link android.R.styleable#TextAppearance TextAppearance} for the
+ * set of attributes that can be used in a custom style.
+ *
+ * @param resId the resource identifier of the style to apply
+ * @attr ref android.R.styleable#TextView_textAppearance
+ */
+ @SuppressWarnings("deprecation")
+ public void setTextAppearance(@StyleRes int resId) {
+ setTextAppearance(mContext, resId);
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ *
+ * @deprecated Use {@link #setTextAppearance(int)} instead.
+ */
+ @Deprecated
+ public void setTextAppearance(Context context, @StyleRes int resId) {
+ final TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.TextAppearance);
+ final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
+ readTextAppearance(context, ta, attributes, false /* styleArray */);
+ ta.recycle();
+ applyTextAppearance(attributes);
+ }
+
+ /**
+ * Set of attributes that can be defined in a Text Appearance. This is used to simplify the code
+ * that reads these attributes in the constructor and in {@link #setTextAppearance}.
+ */
+ private static class TextAppearanceAttributes {
+ int mTextColorHighlight = 0;
+ ColorStateList mTextColor = null;
+ ColorStateList mTextColorHint = null;
+ ColorStateList mTextColorLink = null;
+ int mTextSize = 0;
+ String mFontFamily = null;
+ Typeface mFontTypeface = null;
+ boolean mFontFamilyExplicit = false;
+ int mTypefaceIndex = -1;
+ int mStyleIndex = -1;
+ boolean mAllCaps = false;
+ int mShadowColor = 0;
+ float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
+ boolean mHasElegant = false;
+ boolean mElegant = false;
+ boolean mHasLetterSpacing = false;
+ float mLetterSpacing = 0;
+ String mFontFeatureSettings = null;
+
+ @Override
+ public String toString() {
+ return "TextAppearanceAttributes {\n"
+ + " mTextColorHighlight:" + mTextColorHighlight + "\n"
+ + " mTextColor:" + mTextColor + "\n"
+ + " mTextColorHint:" + mTextColorHint + "\n"
+ + " mTextColorLink:" + mTextColorLink + "\n"
+ + " mTextSize:" + mTextSize + "\n"
+ + " mFontFamily:" + mFontFamily + "\n"
+ + " mFontTypeface:" + mFontTypeface + "\n"
+ + " mFontFamilyExplicit:" + mFontFamilyExplicit + "\n"
+ + " mTypefaceIndex:" + mTypefaceIndex + "\n"
+ + " mStyleIndex:" + mStyleIndex + "\n"
+ + " mAllCaps:" + mAllCaps + "\n"
+ + " mShadowColor:" + mShadowColor + "\n"
+ + " mShadowDx:" + mShadowDx + "\n"
+ + " mShadowDy:" + mShadowDy + "\n"
+ + " mShadowRadius:" + mShadowRadius + "\n"
+ + " mHasElegant:" + mHasElegant + "\n"
+ + " mElegant:" + mElegant + "\n"
+ + " mHasLetterSpacing:" + mHasLetterSpacing + "\n"
+ + " mLetterSpacing:" + mLetterSpacing + "\n"
+ + " mFontFeatureSettings:" + mFontFeatureSettings + "\n"
+ + "}";
+ }
+ }
+
+ // Maps styleable attributes that exist both in TextView style and TextAppearance.
+ private static final SparseIntArray sAppearanceValues = new SparseIntArray();
+ static {
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHighlight,
+ com.android.internal.R.styleable.TextAppearance_textColorHighlight);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColor,
+ com.android.internal.R.styleable.TextAppearance_textColor);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHint,
+ com.android.internal.R.styleable.TextAppearance_textColorHint);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorLink,
+ com.android.internal.R.styleable.TextAppearance_textColorLink);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textSize,
+ com.android.internal.R.styleable.TextAppearance_textSize);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_typeface,
+ com.android.internal.R.styleable.TextAppearance_typeface);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFamily,
+ com.android.internal.R.styleable.TextAppearance_fontFamily);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textStyle,
+ com.android.internal.R.styleable.TextAppearance_textStyle);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_textAllCaps,
+ com.android.internal.R.styleable.TextAppearance_textAllCaps);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowColor,
+ com.android.internal.R.styleable.TextAppearance_shadowColor);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDx,
+ com.android.internal.R.styleable.TextAppearance_shadowDx);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDy,
+ com.android.internal.R.styleable.TextAppearance_shadowDy);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowRadius,
+ com.android.internal.R.styleable.TextAppearance_shadowRadius);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight,
+ com.android.internal.R.styleable.TextAppearance_elegantTextHeight);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing,
+ com.android.internal.R.styleable.TextAppearance_letterSpacing);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings,
+ com.android.internal.R.styleable.TextAppearance_fontFeatureSettings);
+ }
+
+ /**
+ * Read the Text Appearance attributes from a given TypedArray and set its values to the given
+ * set. If the TypedArray contains a value that was already set in the given attributes, that
+ * will be overriden.
+ *
+ * @param context The Context to be used
+ * @param appearance The TypedArray to read properties from
+ * @param attributes the TextAppearanceAttributes to fill in
+ * @param styleArray Whether the given TypedArray is a style or a TextAppearance. This defines
+ * what attribute indexes will be used to read the properties.
+ */
+ private void readTextAppearance(Context context, TypedArray appearance,
+ TextAppearanceAttributes attributes, boolean styleArray) {
+ final int n = appearance.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ final int attr = appearance.getIndex(i);
+ int index = attr;
+ // Translate style array index ids to TextAppearance ids.
+ if (styleArray) {
+ index = sAppearanceValues.get(attr, -1);
+ if (index == -1) {
+ // This value is not part of a Text Appearance and should be ignored.
+ continue;
+ }
+ }
+ switch (index) {
+ case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
+ attributes.mTextColorHighlight =
+ appearance.getColor(attr, attributes.mTextColorHighlight);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textColor:
+ attributes.mTextColor = appearance.getColorStateList(attr);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textColorHint:
+ attributes.mTextColorHint = appearance.getColorStateList(attr);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textColorLink:
+ attributes.mTextColorLink = appearance.getColorStateList(attr);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textSize:
+ attributes.mTextSize =
+ appearance.getDimensionPixelSize(attr, attributes.mTextSize);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_typeface:
+ attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex);
+ if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
+ attributes.mFontFamily = null;
+ }
+ break;
+ case com.android.internal.R.styleable.TextAppearance_fontFamily:
+ if (!context.isRestricted() && context.canLoadUnsafeResources()) {
+ try {
+ attributes.mFontTypeface = appearance.getFont(attr);
+ } catch (UnsupportedOperationException | Resources.NotFoundException e) {
+ // Expected if it is not a font resource.
+ }
+ }
+ if (attributes.mFontTypeface == null) {
+ attributes.mFontFamily = appearance.getString(attr);
+ }
+ attributes.mFontFamilyExplicit = true;
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textStyle:
+ attributes.mStyleIndex = appearance.getInt(attr, attributes.mStyleIndex);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_textAllCaps:
+ attributes.mAllCaps = appearance.getBoolean(attr, attributes.mAllCaps);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_shadowColor:
+ attributes.mShadowColor = appearance.getInt(attr, attributes.mShadowColor);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_shadowDx:
+ attributes.mShadowDx = appearance.getFloat(attr, attributes.mShadowDx);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_shadowDy:
+ attributes.mShadowDy = appearance.getFloat(attr, attributes.mShadowDy);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_shadowRadius:
+ attributes.mShadowRadius = appearance.getFloat(attr, attributes.mShadowRadius);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_elegantTextHeight:
+ attributes.mHasElegant = true;
+ attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_letterSpacing:
+ attributes.mHasLetterSpacing = true;
+ attributes.mLetterSpacing =
+ appearance.getFloat(attr, attributes.mLetterSpacing);
+ break;
+ case com.android.internal.R.styleable.TextAppearance_fontFeatureSettings:
+ attributes.mFontFeatureSettings = appearance.getString(attr);
+ break;
+ default:
+ }
+ }
+ }
+
+ private void applyTextAppearance(TextAppearanceAttributes attributes) {
+ if (attributes.mTextColor != null) {
+ setTextColor(attributes.mTextColor);
+ }
+
+ if (attributes.mTextColorHint != null) {
+ setHintTextColor(attributes.mTextColorHint);
+ }
+
+ if (attributes.mTextColorLink != null) {
+ setLinkTextColor(attributes.mTextColorLink);
+ }
+
+ if (attributes.mTextColorHighlight != 0) {
+ setHighlightColor(attributes.mTextColorHighlight);
+ }
+
+ if (attributes.mTextSize != 0) {
+ setRawTextSize(attributes.mTextSize, true /* shouldRequestLayout */);
+ }
+
+ if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
+ attributes.mFontFamily = null;
+ }
+ setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
+ attributes.mTypefaceIndex, attributes.mStyleIndex);
+
+ if (attributes.mShadowColor != 0) {
+ setShadowLayer(attributes.mShadowRadius, attributes.mShadowDx, attributes.mShadowDy,
+ attributes.mShadowColor);
+ }
+
+ if (attributes.mAllCaps) {
+ setTransformationMethod(new AllCapsTransformationMethod(getContext()));
+ }
+
+ if (attributes.mHasElegant) {
+ setElegantTextHeight(attributes.mElegant);
+ }
+
+ if (attributes.mHasLetterSpacing) {
+ setLetterSpacing(attributes.mLetterSpacing);
+ }
+
+ if (attributes.mFontFeatureSettings != null) {
+ setFontFeatureSettings(attributes.mFontFeatureSettings);
+ }
+ }
+
+ /**
+ * Get the default primary {@link Locale} of the text in this TextView. This will always be
+ * the first member of {@link #getTextLocales()}.
+ * @return the default primary {@link Locale} of the text in this TextView.
+ */
+ @NonNull
+ public Locale getTextLocale() {
+ return mTextPaint.getTextLocale();
+ }
+
+ /**
+ * Get the default {@link LocaleList} of the text in this TextView.
+ * @return the default {@link LocaleList} of the text in this TextView.
+ */
+ @NonNull @Size(min = 1)
+ public LocaleList getTextLocales() {
+ return mTextPaint.getTextLocales();
+ }
+
+ private void changeListenerLocaleTo(@Nullable Locale locale) {
+ if (mListenerChanged) {
+ // If a listener has been explicitly set, don't change it. We may break something.
+ return;
+ }
+ // The following null check is not absolutely necessary since all calling points of
+ // changeListenerLocaleTo() guarantee a non-null mEditor at the moment. But this is left
+ // here in case others would want to call this method in the future.
+ if (mEditor != null) {
+ KeyListener listener = mEditor.mKeyListener;
+ if (listener instanceof DigitsKeyListener) {
+ listener = DigitsKeyListener.getInstance(locale, (DigitsKeyListener) listener);
+ } else if (listener instanceof DateKeyListener) {
+ listener = DateKeyListener.getInstance(locale);
+ } else if (listener instanceof TimeKeyListener) {
+ listener = TimeKeyListener.getInstance(locale);
+ } else if (listener instanceof DateTimeKeyListener) {
+ listener = DateTimeKeyListener.getInstance(locale);
+ } else {
+ return;
+ }
+ final boolean wasPasswordType = isPasswordInputType(mEditor.mInputType);
+ setKeyListenerOnly(listener);
+ setInputTypeFromEditor();
+ if (wasPasswordType) {
+ final int newInputClass = mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS;
+ if (newInputClass == EditorInfo.TYPE_CLASS_TEXT) {
+ mEditor.mInputType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ } else if (newInputClass == EditorInfo.TYPE_CLASS_NUMBER) {
+ mEditor.mInputType |= EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the default {@link Locale} of the text in this TextView to a one-member
+ * {@link LocaleList} containing just the given Locale.
+ *
+ * @param locale the {@link Locale} for drawing text, must not be null.
+ *
+ * @see #setTextLocales
+ */
+ public void setTextLocale(@NonNull Locale locale) {
+ mLocalesChanged = true;
+ mTextPaint.setTextLocale(locale);
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Set the default {@link LocaleList} of the text in this TextView to the given value.
+ *
+ * This value is used to choose appropriate typefaces for ambiguous characters (typically used
+ * for CJK locales to disambiguate Hanzi/Kanji/Hanja characters). It also affects
+ * other aspects of text display, including line breaking.
+ *
+ * @param locales the {@link LocaleList} for drawing text, must not be null or empty.
+ *
+ * @see Paint#setTextLocales
+ */
+ public void setTextLocales(@NonNull @Size(min = 1) LocaleList locales) {
+ mLocalesChanged = true;
+ mTextPaint.setTextLocales(locales);
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (!mLocalesChanged) {
+ mTextPaint.setTextLocales(LocaleList.getDefault());
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return the size (in pixels) of the default text size in this TextView.
+ */
+ @ViewDebug.ExportedProperty(category = "text")
+ public float getTextSize() {
+ return mTextPaint.getTextSize();
+ }
+
+ /**
+ * @return the size (in scaled pixels) of the default text size in this TextView.
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "text")
+ public float getScaledTextSize() {
+ return mTextPaint.getTextSize() / mTextPaint.density;
+ }
+
+ /** @hide */
+ @ViewDebug.ExportedProperty(category = "text", mapping = {
+ @ViewDebug.IntToString(from = Typeface.NORMAL, to = "NORMAL"),
+ @ViewDebug.IntToString(from = Typeface.BOLD, to = "BOLD"),
+ @ViewDebug.IntToString(from = Typeface.ITALIC, to = "ITALIC"),
+ @ViewDebug.IntToString(from = Typeface.BOLD_ITALIC, to = "BOLD_ITALIC")
+ })
+ public int getTypefaceStyle() {
+ Typeface typeface = mTextPaint.getTypeface();
+ return typeface != null ? typeface.getStyle() : Typeface.NORMAL;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
+ *
+ * @param size The scaled pixel size.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ @android.view.RemotableViewMethod
+ 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.
+ *
+ * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
+ *
+ * @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) {
+ if (!isAutoSizeEnabled()) {
+ setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
+ }
+ }
+
+ private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
+ Context c = getContext();
+ Resources r;
+
+ if (c == null) {
+ r = Resources.getSystem();
+ } else {
+ r = c.getResources();
+ }
+
+ setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
+ shouldRequestLayout);
+ }
+
+ private void setRawTextSize(float size, boolean shouldRequestLayout) {
+ if (size != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(size);
+
+ if (shouldRequestLayout && mLayout != null) {
+ // Do not auto-size right after setting the text size.
+ mNeedsAutoSizeText = false;
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Gets the extent by which text should be stretched horizontally.
+ * This will usually be 1.0.
+ * @return The horizontal scale factor.
+ */
+ public float getTextScaleX() {
+ return mTextPaint.getTextScaleX();
+ }
+
+ /**
+ * Sets the horizontal scale factor for text. The default value
+ * is 1.0. Values greater than 1.0 stretch the text wider.
+ * Values less than 1.0 make the text narrower. By default, this value is 1.0.
+ * @param size The horizontal scale factor.
+ * @attr ref android.R.styleable#TextView_textScaleX
+ */
+ @android.view.RemotableViewMethod
+ public void setTextScaleX(float size) {
+ if (size != mTextPaint.getTextScaleX()) {
+ mUserSetTextScaleX = true;
+ 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.
+ *
+ * @see #getTypeface()
+ *
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @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();
+ }
+ }
+ }
+
+ /**
+ * Gets the current {@link Typeface} that is used to style the text.
+ * @return The current Typeface.
+ *
+ * @see #setTypeface(Typeface)
+ *
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public Typeface getTypeface() {
+ return mTextPaint.getTypeface();
+ }
+
+ /**
+ * Set the TextView's elegant height metrics flag. This setting selects font
+ * variants that have not been compacted to fit Latin-based vertical
+ * metrics, and also increases top and bottom bounds to provide more space.
+ *
+ * @param elegant set the paint's elegant metrics flag.
+ *
+ * @see Paint#isElegantTextHeight(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_elegantTextHeight
+ */
+ public void setElegantTextHeight(boolean elegant) {
+ if (elegant != mTextPaint.isElegantTextHeight()) {
+ mTextPaint.setElegantTextHeight(elegant);
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Get the value of the TextView's elegant height metrics flag. This setting selects font
+ * variants that have not been compacted to fit Latin-based vertical
+ * metrics, and also increases top and bottom bounds to provide more space.
+ * @return {@code true} if the elegant height metrics flag is set.
+ *
+ * @see #setElegantTextHeight(boolean)
+ * @see Paint#setElegantTextHeight(boolean)
+ */
+ public boolean isElegantTextHeight() {
+ return mTextPaint.isElegantTextHeight();
+ }
+
+ /**
+ * Gets the text letter-space value, which determines the spacing between characters.
+ * The value returned is in ems. Normally, this value is 0.0.
+ * @return The text letter-space value in ems.
+ *
+ * @see #setLetterSpacing(float)
+ * @see Paint#setLetterSpacing
+ */
+ public float getLetterSpacing() {
+ return mTextPaint.getLetterSpacing();
+ }
+
+ /**
+ * Sets text letter-spacing in em units. Typical values
+ * for slight expansion will be around 0.05. Negative values tighten text.
+ *
+ * @see #getLetterSpacing()
+ * @see Paint#getLetterSpacing
+ *
+ * @param letterSpacing A text letter-space value in ems.
+ * @attr ref android.R.styleable#TextView_letterSpacing
+ */
+ @android.view.RemotableViewMethod
+ public void setLetterSpacing(float letterSpacing) {
+ if (letterSpacing != mTextPaint.getLetterSpacing()) {
+ mTextPaint.setLetterSpacing(letterSpacing);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Returns the font feature settings. The format is the same as the CSS
+ * font-feature-settings attribute:
+ * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+ *
+ * @return the currently set font feature settings. Default is null.
+ *
+ * @see #setFontFeatureSettings(String)
+ * @see Paint#setFontFeatureSettings(String) Paint.setFontFeatureSettings(String)
+ */
+ @Nullable
+ public String getFontFeatureSettings() {
+ return mTextPaint.getFontFeatureSettings();
+ }
+
+ /**
+ * Returns the font variation settings.
+ *
+ * @return the currently set font variation settings. Returns null if no variation is
+ * specified.
+ *
+ * @see #setFontVariationSettings(String)
+ * @see Paint#setFontVariationSettings(String) Paint.setFontVariationSettings(String)
+ */
+ @Nullable
+ public String getFontVariationSettings() {
+ return mTextPaint.getFontVariationSettings();
+ }
+
+ /**
+ * Sets the break strategy for breaking paragraphs into lines. The default value for
+ * TextView is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}, and the default value for
+ * EditText is {@link Layout#BREAK_STRATEGY_SIMPLE}, the latter to avoid the
+ * text "dancing" when being edited.
+ *
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @see #getBreakStrategy()
+ */
+ public void setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Gets the current strategy for breaking paragraphs into lines.
+ * @return the current strategy for breaking paragraphs into lines.
+ *
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @see #setBreakStrategy(int)
+ */
+ @Layout.BreakStrategy
+ public int getBreakStrategy() {
+ return mBreakStrategy;
+ }
+
+ /**
+ * Sets the frequency of automatic hyphenation to use when determining word breaks.
+ * The default value for both TextView and {@link EditText} is
+ * {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+ * Note that the default hyphenation frequency value is set from the theme.
+ *
+ * @param hyphenationFrequency The hyphenation frequency to use.
+ * @attr ref android.R.styleable#TextView_hyphenationFrequency
+ * @see #getHyphenationFrequency()
+ */
+ public void setHyphenationFrequency(@Layout.HyphenationFrequency int hyphenationFrequency) {
+ mHyphenationFrequency = hyphenationFrequency;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Gets the current frequency of automatic hyphenation to be used when determining word breaks.
+ * @return the current frequency of automatic hyphenation to be used when determining word
+ * breaks.
+ *
+ * @attr ref android.R.styleable#TextView_hyphenationFrequency
+ * @see #setHyphenationFrequency(int)
+ */
+ @Layout.HyphenationFrequency
+ public int getHyphenationFrequency() {
+ return mHyphenationFrequency;
+ }
+
+ /**
+ * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the
+ * last line is too short for justification, the last line will be displayed with the
+ * alignment set by {@link android.view.View#setTextAlignment}.
+ *
+ * @see #getJustificationMode()
+ */
+ @Layout.JustificationMode
+ public void setJustificationMode(@Layout.JustificationMode int justificationMode) {
+ mJustificationMode = justificationMode;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * @return true if currently paragraph justification mode.
+ *
+ * @see #setJustificationMode(int)
+ */
+ public @Layout.JustificationMode int getJustificationMode() {
+ return mJustificationMode;
+ }
+
+ /**
+ * Sets font feature settings. The format is the same as the CSS
+ * font-feature-settings attribute:
+ * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+ * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+ *
+ * @param fontFeatureSettings font feature settings represented as CSS compatible string
+ *
+ * @see #getFontFeatureSettings()
+ * @see Paint#getFontFeatureSettings() Paint.getFontFeatureSettings()
+ *
+ * @attr ref android.R.styleable#TextView_fontFeatureSettings
+ */
+ @android.view.RemotableViewMethod
+ public void setFontFeatureSettings(@Nullable String fontFeatureSettings) {
+ if (fontFeatureSettings != mTextPaint.getFontFeatureSettings()) {
+ mTextPaint.setFontFeatureSettings(fontFeatureSettings);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+
+ /**
+ * Sets TrueType or OpenType font variation settings. The settings string is constructed from
+ * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
+ * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
+ * are longer or shorter than four characters, or contain characters outside of U+0020..U+007E
+ * are invalid. If a specified axis name is not defined in the font, the settings will be
+ * ignored.
+ *
+ * <p>
+ * Examples,
+ * <ul>
+ * <li>Set font width to 150.
+ * <pre>
+ * <code>
+ * TextView textView = (TextView) findViewById(R.id.textView);
+ * textView.setFontVariationSettings("'wdth' 150");
+ * </code>
+ * </pre>
+ * </li>
+ *
+ * <li>Set the font slant to 20 degrees and ask for italic style.
+ * <pre>
+ * <code>
+ * TextView textView = (TextView) findViewById(R.id.textView);
+ * textView.setFontVariationSettings("'slnt' 20, 'ital' 1");
+ * </code>
+ * </pre>
+ * </p>
+ * </li>
+ * </ul>
+ *
+ * @param fontVariationSettings font variation settings. You can pass null or empty string as
+ * no variation settings.
+ * @return true if the given settings is effective to at least one font file underlying this
+ * TextView. This function also returns true for empty settings string. Otherwise
+ * returns false.
+ *
+ * @throws IllegalArgumentException If given string is not a valid font variation settings
+ * format.
+ *
+ * @see #getFontVariationSettings()
+ * @see FontVariationAxis
+ */
+ public boolean setFontVariationSettings(@Nullable String fontVariationSettings) {
+ final String existingSettings = mTextPaint.getFontVariationSettings();
+ if (fontVariationSettings == existingSettings
+ || (fontVariationSettings != null
+ && fontVariationSettings.equals(existingSettings))) {
+ return true;
+ }
+ boolean effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+
+ if (effective && mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ return effective;
+ }
+
+ /**
+ * Sets the text color for all the states (normal, selected,
+ * focused) to be this color.
+ *
+ * @param color A color value in the form 0xAARRGGBB.
+ * Do not pass a resource ID. To get a color value from a resource ID, call
+ * {@link android.support.v4.content.ContextCompat#getColor(Context, int) getColor}.
+ *
+ * @see #setTextColor(ColorStateList)
+ * @see #getTextColors()
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ @android.view.RemotableViewMethod
+ public void setTextColor(@ColorInt int color) {
+ mTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the text color.
+ *
+ * @see #setTextColor(int)
+ * @see #getTextColors()
+ * @see #setHintTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ @android.view.RemotableViewMethod
+ public void setTextColor(ColorStateList colors) {
+ if (colors == null) {
+ throw new NullPointerException();
+ }
+
+ mTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * Gets the text colors for the different states (normal, selected, focused) of the TextView.
+ *
+ * @see #setTextColor(ColorStateList)
+ * @see #setTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public final ColorStateList getTextColors() {
+ return mTextColor;
+ }
+
+ /**
+ * Return the current color selected for normal text.
+ *
+ * @return Returns the current text color.
+ */
+ @ColorInt
+ public final int getCurrentTextColor() {
+ return mCurTextColor;
+ }
+
+ /**
+ * Sets the color used to display the selection highlight.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ @android.view.RemotableViewMethod
+ public void setHighlightColor(@ColorInt int color) {
+ if (mHighlightColor != color) {
+ mHighlightColor = color;
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the color used to display the selection highlight
+ *
+ * @see #setHighlightColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ @ColorInt
+ public int getHighlightColor() {
+ return mHighlightColor;
+ }
+
+ /**
+ * Sets whether the soft input method will be made visible when this
+ * TextView gets focused. The default is true.
+ */
+ @android.view.RemotableViewMethod
+ public final void setShowSoftInputOnFocus(boolean show) {
+ createEditorIfNeeded();
+ mEditor.mShowSoftInputOnFocus = show;
+ }
+
+ /**
+ * Returns whether the soft input method will be made visible when this
+ * TextView gets focused. The default is true.
+ */
+ public final boolean getShowSoftInputOnFocus() {
+ // When there is no Editor, return default true value
+ return mEditor == null || mEditor.mShowSoftInputOnFocus;
+ }
+
+ /**
+ * Gives the text a shadow of the specified blur radius and color, the specified
+ * distance from its drawn position.
+ * <p>
+ * The text shadow produced does not interact with the properties on view
+ * that are responsible for real time shadows,
+ * {@link View#getElevation() elevation} and
+ * {@link View#getTranslationZ() translationZ}.
+ *
+ * @see Paint#setShadowLayer(float, float, float, int)
+ *
+ * @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;
+ mShadowColor = color;
+
+ // Will change text clip region
+ if (mEditor != null) {
+ mEditor.invalidateTextDisplayList();
+ mEditor.invalidateHandlesAndActionMode();
+ }
+ invalidate();
+ }
+
+ /**
+ * Gets the radius of the shadow layer.
+ *
+ * @return the radius of the shadow layer. If 0, the shadow layer is not visible
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ */
+ public float getShadowRadius() {
+ return mShadowRadius;
+ }
+
+ /**
+ * @return the horizontal offset of the shadow layer
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowDx
+ */
+ public float getShadowDx() {
+ return mShadowDx;
+ }
+
+ /**
+ * Gets the vertical offset of the shadow layer.
+ * @return The vertical offset of the shadow layer.
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowDy
+ */
+ public float getShadowDy() {
+ return mShadowDy;
+ }
+
+ /**
+ * Gets the color of the shadow layer.
+ * @return the color of the shadow layer
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowColor
+ */
+ @ColorInt
+ public int getShadowColor() {
+ return mShadowColor;
+ }
+
+ /**
+ * Gets the {@link TextPaint} used for the text.
+ * Use this only to consult the Paint's properties and not to change them.
+ * @return The base paint used for the text.
+ */
+ 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
+ */
+ @android.view.RemotableViewMethod
+ 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
+ */
+ @android.view.RemotableViewMethod
+ 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 {@link android.text.style.URLSpan 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 for all the states (disabled, focussed, selected...) of this
+ * TextView.
+ *
+ * @see #setHintTextColor(ColorStateList)
+ * @see #getHintTextColors()
+ * @see #setTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ @android.view.RemotableViewMethod
+ public final void setHintTextColor(@ColorInt int color) {
+ mHintTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @see #getHintTextColors()
+ * @see #setHintTextColor(int)
+ * @see #setTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor(ColorStateList colors) {
+ mHintTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * @return the color of the hint text, for the different states of this TextView.
+ *
+ * @see #setHintTextColor(ColorStateList)
+ * @see #setHintTextColor(int)
+ * @see #setTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ 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.
+ */
+ @ColorInt
+ public final int getCurrentHintTextColor() {
+ return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @see #setLinkTextColor(ColorStateList)
+ * @see #getLinkTextColors()
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ @android.view.RemotableViewMethod
+ public final void setLinkTextColor(@ColorInt int color) {
+ mLinkTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @see #setLinkTextColor(int)
+ * @see #getLinkTextColors()
+ * @see #setTextColor(ColorStateList)
+ * @see #setHintTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor(ColorStateList colors) {
+ mLinkTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * @return the list of colors used to paint the links in the text, for the different states of
+ * this TextView
+ *
+ * @see #setLinkTextColor(ColorStateList)
+ * @see #setLinkTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ 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.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.START;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ boolean newLayout = false;
+
+ if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
+ != (mGravity & Gravity.RELATIVE_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;
+ }
+
+ /**
+ * Gets the flags on the Paint being used to display the text.
+ * @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
+ */
+ @android.view.RemotableViewMethod
+ 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) {
+ if (mHorizontallyScrolling != whether) {
+ mHorizontallyScrolling = whether;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Returns whether the text is allowed to be wider than the View is.
+ * If false, the text will be wrapped to the width of the View.
+ *
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ * @hide
+ */
+ public boolean getHorizontallyScrolling() {
+ return mHorizontallyScrolling;
+ }
+
+ /**
+ * Sets the height of the TextView to be at least {@code minLines} tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides other previous minimum height configurations such
+ * as {@link #setMinHeight(int)} or {@link #setHeight(int)}. {@link #setSingleLine()} will set
+ * this value to 1.
+ *
+ * @param minLines the minimum height of TextView in terms of number of lines
+ *
+ * @see #getMinLines()
+ * @see #setLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ @android.view.RemotableViewMethod
+ public void setMinLines(int minLines) {
+ mMinimum = minLines;
+ mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the minimum height of TextView in terms of number of lines or -1 if the minimum
+ * height was set using {@link #setMinHeight(int)} or {@link #setHeight(int)}.
+ *
+ * @return the minimum height of TextView in terms of number of lines or -1 if the minimum
+ * height is not defined in lines
+ *
+ * @see #setMinLines(int)
+ * @see #setLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ public int getMinLines() {
+ return mMinMode == LINES ? mMinimum : -1;
+ }
+
+ /**
+ * Sets the height of the TextView to be at least {@code minPixels} tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides previous minimum height configurations such as
+ * {@link #setMinLines(int)} or {@link #setLines(int)}.
+ * <p>
+ * The value given here is different than {@link #setMinimumHeight(int)}. Between
+ * {@code minHeight} and the value set in {@link #setMinimumHeight(int)}, the greater one is
+ * used to decide the final height.
+ *
+ * @param minPixels the minimum height of TextView in terms of pixels
+ *
+ * @see #getMinHeight()
+ * @see #setHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ @android.view.RemotableViewMethod
+ public void setMinHeight(int minPixels) {
+ mMinimum = minPixels;
+ mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the minimum height of TextView in terms of pixels or -1 if the minimum height was
+ * set using {@link #setMinLines(int)} or {@link #setLines(int)}.
+ *
+ * @return the minimum height of TextView in terms of pixels or -1 if the minimum height is not
+ * defined in pixels
+ *
+ * @see #setMinHeight(int)
+ * @see #setHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ public int getMinHeight() {
+ return mMinMode == PIXELS ? mMinimum : -1;
+ }
+
+ /**
+ * Sets the height of the TextView to be at most {@code maxLines} tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides previous maximum height configurations such as
+ * {@link #setMaxHeight(int)} or {@link #setLines(int)}.
+ *
+ * @param maxLines the maximum height of TextView in terms of number of lines
+ *
+ * @see #getMaxLines()
+ * @see #setLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxLines(int maxLines) {
+ mMaximum = maxLines;
+ mMaxMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the maximum height of TextView in terms of number of lines or -1 if the
+ * maximum height was set using {@link #setMaxHeight(int)} or {@link #setHeight(int)}.
+ *
+ * @return the maximum height of TextView in terms of number of lines. -1 if the maximum height
+ * is not defined in lines.
+ *
+ * @see #setMaxLines(int)
+ * @see #setLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ public int getMaxLines() {
+ return mMaxMode == LINES ? mMaximum : -1;
+ }
+
+ /**
+ * Sets the height of the TextView to be at most {@code maxPixels} tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides previous maximum height configurations such as
+ * {@link #setMaxLines(int)} or {@link #setLines(int)}.
+ *
+ * @param maxPixels the maximum height of TextView in terms of pixels
+ *
+ * @see #getMaxHeight()
+ * @see #setHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxHeight(int maxPixels) {
+ mMaximum = maxPixels;
+ mMaxMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the maximum height of TextView in terms of pixels or -1 if the maximum height was
+ * set using {@link #setMaxLines(int)} or {@link #setLines(int)}.
+ *
+ * @return the maximum height of TextView in terms of pixels or -1 if the maximum height
+ * is not defined in pixels
+ *
+ * @see #setMaxHeight(int)
+ * @see #setHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ public int getMaxHeight() {
+ return mMaxMode == PIXELS ? mMaximum : -1;
+ }
+
+ /**
+ * Sets the height of the TextView to be exactly {@code lines} tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides previous minimum/maximum height configurations
+ * such as {@link #setMinLines(int)} or {@link #setMaxLines(int)}. {@link #setSingleLine()} will
+ * set this value to 1.
+ *
+ * @param lines the exact height of the TextView in terms of lines
+ *
+ * @see #setHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_lines
+ */
+ @android.view.RemotableViewMethod
+ public void setLines(int lines) {
+ mMaximum = mMinimum = lines;
+ mMaxMode = mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the height of the TextView to be exactly <code>pixels</code> tall.
+ * <p>
+ * This value is used for height calculation if LayoutParams does not force TextView to have an
+ * exact height. Setting this value overrides previous minimum/maximum height configurations
+ * such as {@link #setMinHeight(int)} or {@link #setMaxHeight(int)}.
+ *
+ * @param pixels the exact height of the TextView in terms of pixels
+ *
+ * @see #setLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_height
+ */
+ @android.view.RemotableViewMethod
+ public void setHeight(int pixels) {
+ mMaximum = mMinimum = pixels;
+ mMaxMode = mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the width of the TextView to be at least {@code minEms} wide.
+ * <p>
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous minimum width configurations such as
+ * {@link #setMinWidth(int)} or {@link #setWidth(int)}.
+ *
+ * @param minEms the minimum width of TextView in terms of ems
+ *
+ * @see #getMinEms()
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ @android.view.RemotableViewMethod
+ public void setMinEms(int minEms) {
+ mMinWidth = minEms;
+ mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the minimum width of TextView in terms of ems or -1 if the minimum width was set
+ * using {@link #setMinWidth(int)} or {@link #setWidth(int)}.
+ *
+ * @return the minimum width of TextView in terms of ems. -1 if the minimum width is not
+ * defined in ems
+ *
+ * @see #setMinEms(int)
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ public int getMinEms() {
+ return mMinWidthMode == EMS ? mMinWidth : -1;
+ }
+
+ /**
+ * Sets the width of the TextView to be at least {@code minPixels} wide.
+ * <p>
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous minimum width configurations such as
+ * {@link #setMinEms(int)} or {@link #setEms(int)}.
+ * <p>
+ * The value given here is different than {@link #setMinimumWidth(int)}. Between
+ * {@code minWidth} and the value set in {@link #setMinimumWidth(int)}, the greater one is used
+ * to decide the final width.
+ *
+ * @param minPixels the minimum width of TextView in terms of pixels
+ *
+ * @see #getMinWidth()
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ @android.view.RemotableViewMethod
+ public void setMinWidth(int minPixels) {
+ mMinWidth = minPixels;
+ mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the minimum width of TextView in terms of pixels or -1 if the minimum width was set
+ * using {@link #setMinEms(int)} or {@link #setEms(int)}.
+ *
+ * @return the minimum width of TextView in terms of pixels or -1 if the minimum width is not
+ * defined in pixels
+ *
+ * @see #setMinWidth(int)
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ public int getMinWidth() {
+ return mMinWidthMode == PIXELS ? mMinWidth : -1;
+ }
+
+ /**
+ * Sets the width of the TextView to be at most {@code maxEms} wide.
+ * <p>
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous maximum width configurations such as
+ * {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
+ *
+ * @param maxEms the maximum width of TextView in terms of ems
+ *
+ * @see #getMaxEms()
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxEms(int maxEms) {
+ mMaxWidth = maxEms;
+ mMaxWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the maximum width of TextView in terms of ems or -1 if the maximum width was set
+ * using {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
+ *
+ * @return the maximum width of TextView in terms of ems or -1 if the maximum width is not
+ * defined in ems
+ *
+ * @see #setMaxEms(int)
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ public int getMaxEms() {
+ return mMaxWidthMode == EMS ? mMaxWidth : -1;
+ }
+
+ /**
+ * Sets the width of the TextView to be at most {@code maxPixels} wide.
+ * <p>
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous maximum width configurations such as
+ * {@link #setMaxEms(int)} or {@link #setEms(int)}.
+ *
+ * @param maxPixels the maximum width of TextView in terms of pixels
+ *
+ * @see #getMaxWidth()
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxWidth(int maxPixels) {
+ mMaxWidth = maxPixels;
+ mMaxWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Returns the maximum width of TextView in terms of pixels or -1 if the maximum width was set
+ * using {@link #setMaxEms(int)} or {@link #setEms(int)}.
+ *
+ * @return the maximum width of TextView in terms of pixels. -1 if the maximum width is not
+ * defined in pixels
+ *
+ * @see #setMaxWidth(int)
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ public int getMaxWidth() {
+ return mMaxWidthMode == PIXELS ? mMaxWidth : -1;
+ }
+
+ /**
+ * Sets the width of the TextView to be exactly {@code ems} wide.
+ *
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous minimum/maximum configurations such as
+ * {@link #setMinEms(int)} or {@link #setMaxEms(int)}.
+ *
+ * @param ems the exact width of the TextView in terms of ems
+ *
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_ems
+ */
+ @android.view.RemotableViewMethod
+ public void setEms(int ems) {
+ mMaxWidth = mMinWidth = ems;
+ mMaxWidthMode = mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the width of the TextView to be exactly {@code pixels} wide.
+ * <p>
+ * This value is used for width calculation if LayoutParams does not force TextView to have an
+ * exact width. Setting this value overrides previous minimum/maximum width configurations
+ * such as {@link #setMinWidth(int)} or {@link #setMaxWidth(int)}.
+ *
+ * @param pixels the exact width of the TextView in terms of pixels
+ *
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_width
+ */
+ @android.view.RemotableViewMethod
+ public void setWidth(int pixels) {
+ mMaxWidth = mMinWidth = pixels;
+ mMaxWidthMode = mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets line spacing for this TextView. Each line other than the last line will have its height
+ * multiplied by {@code mult} and have {@code add} added to it.
+ *
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ public void setLineSpacing(float add, float mult) {
+ if (mSpacingAdd != add || mSpacingMult != mult) {
+ mSpacingAdd = add;
+ mSpacingMult = mult;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Gets the line spacing multiplier
+ *
+ * @return the value by which each line's height is multiplied to get its actual height.
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacingExtra()
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ public float getLineSpacingMultiplier() {
+ return mSpacingMult;
+ }
+
+ /**
+ * Gets the line spacing extra space
+ *
+ * @return the extra space that is added to the height of each lines of this TextView.
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacingMultiplier()
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ */
+ public float getLineSpacingExtra() {
+ return mSpacingAdd;
+ }
+
+ /**
+ * Convenience method to append the specified text to the TextView's
+ * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+ * if it was not already editable.
+ *
+ * @param text text to be appended to the already displayed text
+ */
+ public final void append(CharSequence text) {
+ append(text, 0, text.length());
+ }
+
+ /**
+ * Convenience method to append the specified text slice to the TextView's
+ * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+ * if it was not already editable.
+ *
+ * @param text text to be appended to the already displayed text
+ * @param start the index of the first character in the {@code text}
+ * @param end the index of the character following the last character in the {@code text}
+ *
+ * @see Appendable#append(CharSequence, int, int)
+ */
+ public void append(CharSequence text, int start, int end) {
+ if (!(mText instanceof Editable)) {
+ setText(mText, BufferType.EDITABLE);
+ }
+
+ ((Editable) mText).append(text, start, end);
+
+ if (mAutoLinkMask != 0) {
+ boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask);
+ // Do not change the movement method for text that support text selection as it
+ // would prevent an arbitrary cursor displacement.
+ if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
+ setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ private void updateTextColors() {
+ boolean inval = false;
+ final int[] drawableState = getDrawableState();
+ int color = mTextColor.getColorForState(drawableState, 0);
+ if (color != mCurTextColor) {
+ mCurTextColor = color;
+ inval = true;
+ }
+ if (mLinkTextColor != null) {
+ color = mLinkTextColor.getColorForState(drawableState, 0);
+ if (color != mTextPaint.linkColor) {
+ mTextPaint.linkColor = color;
+ inval = true;
+ }
+ }
+ if (mHintTextColor != null) {
+ color = mHintTextColor.getColorForState(drawableState, 0);
+ if (color != mCurHintTextColor) {
+ mCurHintTextColor = color;
+ if (mText.length() == 0) {
+ inval = true;
+ }
+ }
+ }
+ if (inval) {
+ // Text needs to be redrawn with the new color
+ if (mEditor != null) mEditor.invalidateTextDisplayList();
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mTextColor != null && mTextColor.isStateful()
+ || (mHintTextColor != null && mHintTextColor.isStateful())
+ || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
+ updateTextColors();
+ }
+
+ if (mDrawables != null) {
+ final int[] state = getDrawableState();
+ for (Drawable dr : mDrawables.mShowing) {
+ if (dr != null && dr.isStateful() && dr.setState(state)) {
+ invalidateDrawable(dr);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void drawableHotspotChanged(float x, float y) {
+ super.drawableHotspotChanged(x, y);
+
+ if (mDrawables != null) {
+ for (Drawable dr : mDrawables.mShowing) {
+ if (dr != null) {
+ dr.setHotspot(x, y);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ // Save state if we are forced to
+ final boolean freezesText = getFreezesText();
+ boolean hasSelection = false;
+ int start = -1;
+ int end = -1;
+
+ if (mText != null) {
+ start = getSelectionStart();
+ end = getSelectionEnd();
+ if (start >= 0 || end >= 0) {
+ // Or save state if there is a selection
+ hasSelection = true;
+ }
+ }
+
+ if (freezesText || hasSelection) {
+ SavedState ss = new SavedState(superState);
+
+ if (freezesText) {
+ if (mText instanceof Spanned) {
+ final Spannable sp = new SpannableStringBuilder(mText);
+
+ if (mEditor != null) {
+ removeMisspelledSpans(sp);
+ sp.removeSpan(mEditor.mSuggestionRangeSpan);
+ }
+
+ ss.text = sp;
+ } else {
+ ss.text = mText.toString();
+ }
+ }
+
+ if (hasSelection) {
+ // XXX Should also save the current scroll position!
+ ss.selStart = start;
+ ss.selEnd = end;
+ }
+
+ if (isFocused() && start >= 0 && end >= 0) {
+ ss.frozenWithFocus = true;
+ }
+
+ ss.error = getError();
+
+ if (mEditor != null) {
+ ss.editorState = mEditor.saveInstanceState();
+ }
+ return ss;
+ }
+
+ return superState;
+ }
+
+ void removeMisspelledSpans(Spannable spannable) {
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
+ SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ int flags = suggestionSpans[i].getFlags();
+ if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+ && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+ spannable.removeSpan(suggestionSpans[i]);
+ }
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ 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(LOG_TAG, "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) {
+ createEditorIfNeeded();
+ mEditor.mFrozenWithFocus = true;
+ }
+ }
+ }
+ }
+
+ if (ss.error != null) {
+ final CharSequence error = ss.error;
+ // Display the error later, after the first layout pass
+ post(new Runnable() {
+ public void run() {
+ if (mEditor == null || !mEditor.mErrorWasChanged) {
+ setError(error);
+ }
+ }
+ });
+ }
+
+ if (ss.editorState != null) {
+ createEditorIfNeeded();
+ mEditor.restoreInstanceState(ss.editorState);
+ }
+ }
+
+ /**
+ * 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. For
+ * {@link android.widget.EditText} it is always enabled, regardless of
+ * the value of the attribute.
+ *
+ * @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
+ */
+ @android.view.RemotableViewMethod
+ public void setFreezesText(boolean freezesText) {
+ mFreezesText = freezesText;
+ }
+
+ /**
+ * Return whether this text view is including its entire text contents
+ * in frozen icicles. For {@link android.widget.EditText} it always returns true.
+ *
+ * @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 {@link Editable Editables}.
+ *
+ * @param factory {@link android.text.Editable.Factory Editable.Factory} to be used
+ *
+ * @see android.text.Editable.Factory
+ * @see android.widget.TextView.BufferType#EDITABLE
+ */
+ public final void setEditableFactory(Editable.Factory factory) {
+ mEditableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the Factory used to create new {@link Spannable Spannables}.
+ *
+ * @param factory {@link android.text.Spannable.Factory Spannable.Factory} to be used
+ *
+ * @see android.text.Spannable.Factory
+ * @see android.widget.TextView.BufferType#SPANNABLE
+ */
+ public final void setSpannableFactory(Spannable.Factory factory) {
+ mSpannableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the text to be displayed. 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}, or see the
+ * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
+ * Available Resource Types</a> documentation for an example of setting
+ * formatted text in the XML resource file.
+ * <p/>
+ * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+ * intermediate {@link Spannable Spannables}. Likewise it will use
+ * {@link android.text.Editable.Factory} to create final or intermediate
+ * {@link Editable Editables}.
+ *
+ * @param text text to be displayed
+ *
+ * @attr ref android.R.styleable#TextView_text
+ */
+ @android.view.RemotableViewMethod
+ public final void setText(CharSequence text) {
+ setText(text, mBufferType);
+ }
+
+ /**
+ * Sets the text to be displayed but retains the cursor position. Same as
+ * {@link #setText(CharSequence)} except that the cursor position (if any) is retained in the
+ * new text.
+ * <p/>
+ * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+ * intermediate {@link Spannable Spannables}. Likewise it will use
+ * {@link android.text.Editable.Factory} to create final or intermediate
+ * {@link Editable Editables}.
+ *
+ * @param text text to be displayed
+ *
+ * @see #setText(CharSequence)
+ */
+ @android.view.RemotableViewMethod
+ public final void setTextKeepState(CharSequence text) {
+ setTextKeepState(text, mBufferType);
+ }
+
+ /**
+ * Sets the text to be displayed and the {@link android.widget.TextView.BufferType}.
+ * <p/>
+ * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+ * intermediate {@link Spannable Spannables}. Likewise it will use
+ * {@link android.text.Editable.Factory} to create final or intermediate
+ * {@link Editable Editables}.
+ *
+ * @param text text to be displayed
+ * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+ * stored as a static text, styleable/spannable text, or editable text
+ *
+ * @see #setText(CharSequence)
+ * @see android.widget.TextView.BufferType
+ * @see #setSpannableFactory(Spannable.Factory)
+ * @see #setEditableFactory(Editable.Factory)
+ *
+ * @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) {
+ mTextFromResource = false;
+ if (text == null) {
+ text = "";
+ }
+
+ // If suggestions are not enabled, remove the suggestion spans from the text
+ if (!isSuggestionsEnabled()) {
+ text = removeSuggestionSpans(text);
+ }
+
+ if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
+
+ if (text instanceof Spanned
+ && ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
+ if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
+ setHorizontalFadingEdgeEnabled(true);
+ mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+ } else {
+ setHorizontalFadingEdgeEnabled(false);
+ mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+ }
+ setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ }
+
+ 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());
+ }
+ }
+
+ boolean needEditableForNotification = false;
+
+ if (mListeners != null && mListeners.size() != 0) {
+ needEditableForNotification = true;
+ }
+
+ if (type == BufferType.EDITABLE || getKeyListener() != null
+ || needEditableForNotification) {
+ createEditorIfNeeded();
+ mEditor.forgetUndoRedo();
+ Editable t = mEditableFactory.newEditable(text);
+ text = t;
+ setFilters(t, mFilters);
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ } 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;
+
+ // Do not change the movement method for text that support text selection as it
+ // would prevent an arbitrary cursor displacement.
+ if (mLinksClickable && !textCanBeSelected()) {
+ 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 && !mAllowTransformationLengthChange) {
+ 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
+ | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
+
+ if (mEditor != null) mEditor.addSpanWatchers(sp);
+
+ 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.
+ */
+ if (mEditor != null) mEditor.mSelectionMoved = false;
+ }
+ }
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ sendOnTextChanged(text, 0, oldlen, textLength);
+ onTextChanged(text, 0, oldlen, textLength);
+
+ notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
+
+ if (needEditableForNotification) {
+ sendAfterTextChanged((Editable) text);
+ } else {
+ // Always notify AutoFillManager - it will return right away if autofill is disabled.
+ notifyAutoFillManagerAfterTextChangedIfNeeded();
+ }
+
+ // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
+ if (mEditor != null) mEditor.prepareCursorControllers();
+ }
+
+ /**
+ * 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.
+ *
+ * @param text char array to be displayed
+ * @param start start index in the char array
+ * @param len length of char count after {@code start}
+ */
+ 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);
+ }
+
+ /**
+ * Sets the text to be displayed and the {@link android.widget.TextView.BufferType} but retains
+ * the cursor position. Same as
+ * {@link #setText(CharSequence, android.widget.TextView.BufferType)} except that the cursor
+ * position (if any) is retained in the new text.
+ * <p/>
+ * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+ * intermediate {@link Spannable Spannables}. Likewise it will use
+ * {@link android.text.Editable.Factory} to create final or intermediate
+ * {@link Editable Editables}.
+ *
+ * @param text text to be displayed
+ * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+ * stored as a static text, styleable/spannable text, or editable 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)));
+ }
+ }
+ }
+
+ /**
+ * Sets the text to be displayed using a string resource identifier.
+ *
+ * @param resid the resource identifier of the string resource to be displayed
+ *
+ * @see #setText(CharSequence)
+ *
+ * @attr ref android.R.styleable#TextView_text
+ */
+ @android.view.RemotableViewMethod
+ public final void setText(@StringRes int resid) {
+ setText(getContext().getResources().getText(resid));
+ mTextFromResource = true;
+ }
+
+ /**
+ * Sets the text to be displayed using a string resource identifier and the
+ * {@link android.widget.TextView.BufferType}.
+ * <p/>
+ * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+ * intermediate {@link Spannable Spannables}. Likewise it will use
+ * {@link android.text.Editable.Factory} to create final or intermediate
+ * {@link Editable Editables}.
+ *
+ * @param resid the resource identifier of the string resource to be displayed
+ * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+ * stored as a static text, styleable/spannable text, or editable text
+ *
+ * @see #setText(int)
+ * @see #setText(CharSequence)
+ * @see android.widget.TextView.BufferType
+ * @see #setSpannableFactory(Spannable.Factory)
+ * @see #setEditableFactory(Editable.Factory)
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ */
+ public final void setText(@StringRes int resid, BufferType type) {
+ setText(getContext().getResources().getText(resid), type);
+ mTextFromResource = true;
+ }
+
+ /**
+ * 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
+ */
+ @android.view.RemotableViewMethod
+ public final void setHint(CharSequence hint) {
+ mHint = TextUtils.stringOrSpannedString(hint);
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ if (mText.length() == 0) {
+ invalidate();
+ }
+
+ // Invalidate display list if hint is currently used
+ if (mEditor != null && mText.length() == 0 && mHint != null) {
+ mEditor.invalidateTextDisplayList();
+ }
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty,
+ * from a resource.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ @android.view.RemotableViewMethod
+ public final void setHint(@StringRes 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
+ */
+ @ViewDebug.CapturedViewProperty
+ public CharSequence getHint() {
+ return mHint;
+ }
+
+ boolean isSingleLine() {
+ return mSingleLine;
+ }
+
+ private static boolean isMultilineInputType(int type) {
+ return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
+ }
+
+ /**
+ * Removes the suggestion spans.
+ */
+ CharSequence removeSuggestionSpans(CharSequence text) {
+ if (text instanceof Spanned) {
+ Spannable spannable;
+ if (text instanceof Spannable) {
+ spannable = (Spannable) text;
+ } else {
+ spannable = mSpannableFactory.newSpannable(text);
+ text = spannable;
+ }
+
+ SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
+ for (int i = 0; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
+ * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
+ * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
+ * then a soft keyboard will not be displayed for this text view.
+ *
+ * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
+ * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
+ * type.
+ *
+ * @see #getInputType()
+ * @see #setRawInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setInputType(int type) {
+ final boolean wasPassword = isPasswordInputType(getInputType());
+ final boolean wasVisiblePassword = isVisiblePasswordInputType(getInputType());
+ setInputType(type, false);
+ final boolean isPassword = isPasswordInputType(type);
+ final boolean isVisiblePassword = isVisiblePasswordInputType(type);
+ boolean forceUpdate = false;
+ if (isPassword) {
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0);
+ } else if (isVisiblePassword) {
+ if (mTransformation == PasswordTransformationMethod.getInstance()) {
+ forceUpdate = true;
+ }
+ setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0);
+ } else if (wasPassword || wasVisiblePassword) {
+ // not in password mode, clean up typeface and transformation
+ setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, -1, -1);
+ if (mTransformation == PasswordTransformationMethod.getInstance()) {
+ forceUpdate = true;
+ }
+ }
+
+ boolean singleLine = !isMultilineInputType(type);
+
+ // We need to update the single line mode if it has changed or we
+ // were previously in password mode.
+ if (mSingleLine != singleLine || forceUpdate) {
+ // Change single line mode, but only change the transformation if
+ // we are not in password mode.
+ applySingleLine(singleLine, !isPassword, true);
+ }
+
+ if (!isSuggestionsEnabled()) {
+ mText = removeSuggestionSpans(mText);
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ }
+
+ /**
+ * It would be better to rely on the input type for everything. A password inputType should have
+ * a password transformation. We should hence use isPasswordInputType instead of this method.
+ *
+ * We should:
+ * - Call setInputType in setKeyListener instead of changing the input type directly (which
+ * would install the correct transformation).
+ * - Refuse the installation of a non-password transformation in setTransformation if the input
+ * type is password.
+ *
+ * However, this is like this for legacy reasons and we cannot break existing apps. This method
+ * is useful since it matches what the user can see (obfuscated text or not).
+ *
+ * @return true if the current transformation method is of the password type.
+ */
+ boolean hasPasswordTransformationMethod() {
+ return mTransformation instanceof PasswordTransformationMethod;
+ }
+
+ static boolean isPasswordInputType(int inputType) {
+ final int variation =
+ inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+ return variation
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
+ || variation
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
+ || variation
+ == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
+ }
+
+ private static boolean isVisiblePasswordInputType(int inputType) {
+ final int variation =
+ inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+ return variation
+ == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
+ }
+
+ /**
+ * Directly change the content type integer of the text view, without
+ * modifying any other state.
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setRawInputType(int type) {
+ if (type == InputType.TYPE_NULL && mEditor == null) return; //TYPE_NULL is the default value
+ createEditorIfNeeded();
+ mEditor.mInputType = type;
+ }
+
+ /**
+ * @return {@code null} if the key listener should use pre-O (locale-independent). Otherwise
+ * a {@code Locale} object that can be used to customize key various listeners.
+ * @see DateKeyListener#getInstance(Locale)
+ * @see DateTimeKeyListener#getInstance(Locale)
+ * @see DigitsKeyListener#getInstance(Locale)
+ * @see TimeKeyListener#getInstance(Locale)
+ */
+ @Nullable
+ private Locale getCustomLocaleForKeyListenerOrNull() {
+ if (!mUseInternationalizedInput) {
+ // If the application does not target O, stick to the previous behavior.
+ return null;
+ }
+ final LocaleList locales = getImeHintLocales();
+ if (locales == null) {
+ // If the application does not explicitly specify IME hint locale, also stick to the
+ // previous behavior.
+ return null;
+ }
+ return locales.get(0);
+ }
+
+ private void setInputType(int type, boolean direct) {
+ final int cls = type & EditorInfo.TYPE_MASK_CLASS;
+ KeyListener input;
+ if (cls == EditorInfo.TYPE_CLASS_TEXT) {
+ boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
+ TextKeyListener.Capitalize cap;
+ if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
+ cap = TextKeyListener.Capitalize.CHARACTERS;
+ } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
+ cap = TextKeyListener.Capitalize.WORDS;
+ } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ } else {
+ cap = TextKeyListener.Capitalize.NONE;
+ }
+ input = TextKeyListener.getInstance(autotext, cap);
+ } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
+ final Locale locale = getCustomLocaleForKeyListenerOrNull();
+ input = DigitsKeyListener.getInstance(
+ locale,
+ (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
+ (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
+ if (locale != null) {
+ // Override type, if necessary for i18n.
+ int newType = input.getInputType();
+ final int newClass = newType & EditorInfo.TYPE_MASK_CLASS;
+ if (newClass != EditorInfo.TYPE_CLASS_NUMBER) {
+ // The class is different from the original class. So we need to override
+ // 'type'. But we want to keep the password flag if it's there.
+ if ((type & EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD) != 0) {
+ newType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ type = newType;
+ }
+ }
+ } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
+ final Locale locale = getCustomLocaleForKeyListenerOrNull();
+ switch (type & EditorInfo.TYPE_MASK_VARIATION) {
+ case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
+ input = DateKeyListener.getInstance(locale);
+ break;
+ case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
+ input = TimeKeyListener.getInstance(locale);
+ break;
+ default:
+ input = DateTimeKeyListener.getInstance(locale);
+ break;
+ }
+ if (mUseInternationalizedInput) {
+ type = input.getInputType(); // Override type, if necessary for i18n.
+ }
+ } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
+ input = DialerKeyListener.getInstance();
+ } else {
+ input = TextKeyListener.getInstance();
+ }
+ setRawInputType(type);
+ mListenerChanged = false;
+ if (direct) {
+ createEditorIfNeeded();
+ mEditor.mKeyListener = input;
+ } else {
+ setKeyListenerOnly(input);
+ }
+ }
+
+ /**
+ * Get the type of the editable content.
+ *
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ */
+ public int getInputType() {
+ return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType;
+ }
+
+ /**
+ * Change the editor type integer associated with the text view, which
+ * is reported to an Input Method Editor (IME) with {@link EditorInfo#imeOptions}
+ * when it has focus.
+ * @see #getImeOptions
+ * @see android.view.inputmethod.EditorInfo
+ * @attr ref android.R.styleable#TextView_imeOptions
+ */
+ public void setImeOptions(int imeOptions) {
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeOptions = imeOptions;
+ }
+
+ /**
+ * Get the type of the Input Method Editor (IME).
+ * @return the type of the IME
+ * @see #setImeOptions(int)
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public int getImeOptions() {
+ return mEditor != null && mEditor.mInputContentType != null
+ ? mEditor.mInputContentType.imeOptions : EditorInfo.IME_NULL;
+ }
+
+ /**
+ * Change the custom IME action associated with the text view, which
+ * will be reported to an IME with {@link EditorInfo#actionLabel}
+ * and {@link EditorInfo#actionId} when it has focus.
+ * @see #getImeActionLabel
+ * @see #getImeActionId
+ * @see android.view.inputmethod.EditorInfo
+ * @attr ref android.R.styleable#TextView_imeActionLabel
+ * @attr ref android.R.styleable#TextView_imeActionId
+ */
+ public void setImeActionLabel(CharSequence label, int actionId) {
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeActionLabel = label;
+ mEditor.mInputContentType.imeActionId = actionId;
+ }
+
+ /**
+ * Get the IME action label previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public CharSequence getImeActionLabel() {
+ return mEditor != null && mEditor.mInputContentType != null
+ ? mEditor.mInputContentType.imeActionLabel : null;
+ }
+
+ /**
+ * Get the IME action ID previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public int getImeActionId() {
+ return mEditor != null && mEditor.mInputContentType != null
+ ? mEditor.mInputContentType.imeActionId : 0;
+ }
+
+ /**
+ * Set a special listener to be called when an action is performed
+ * on the text view. This will be called when the enter key is pressed,
+ * or when an action supplied to the IME is selected by the user. Setting
+ * this means that the normal hard key event will not insert a newline
+ * into the text view, even if it is multi-line; holding down the ALT
+ * modifier will, however, allow the user to insert a newline character.
+ */
+ public void setOnEditorActionListener(OnEditorActionListener l) {
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.onEditorActionListener = l;
+ }
+
+ /**
+ * Called when an attached input method calls
+ * {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction()}
+ * for this text view. The default implementation will call your action
+ * listener supplied to {@link #setOnEditorActionListener}, or perform
+ * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
+ * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
+ * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
+ * EditorInfo.IME_ACTION_DONE}.
+ *
+ * <p>For backwards compatibility, if no IME options have been set and the
+ * text view would not normally advance focus on enter, then
+ * the NEXT and DONE actions received here will be turned into an enter
+ * key down/up pair to go through the normal key handling.
+ *
+ * @param actionCode The code of the action being performed.
+ *
+ * @see #setOnEditorActionListener
+ */
+ public void onEditorAction(int actionCode) {
+ final Editor.InputContentType ict = mEditor == null ? null : mEditor.mInputContentType;
+ if (ict != null) {
+ if (ict.onEditorActionListener != null) {
+ if (ict.onEditorActionListener.onEditorAction(this,
+ actionCode, null)) {
+ return;
+ }
+ }
+
+ // This is the handling for some default action.
+ // Note that for backwards compatibility we don't do this
+ // default handling if explicit ime options have not been given,
+ // instead turning this into the normal enter key codes that an
+ // app may be expecting.
+ if (actionCode == EditorInfo.IME_ACTION_NEXT) {
+ View v = focusSearch(FOCUS_FORWARD);
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_FORWARD)) {
+ throw new IllegalStateException("focus search returned a view "
+ + "that wasn't able to take focus!");
+ }
+ }
+ return;
+
+ } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
+ View v = focusSearch(FOCUS_BACKWARD);
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_BACKWARD)) {
+ throw new IllegalStateException("focus search returned a view "
+ + "that wasn't able to take focus!");
+ }
+ }
+ return;
+
+ } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ return;
+ }
+ }
+
+ ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl != null) {
+ long eventTime = SystemClock.uptimeMillis();
+ viewRootImpl.dispatchKeyFromIme(
+ new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+ | KeyEvent.FLAG_EDITOR_ACTION));
+ viewRootImpl.dispatchKeyFromIme(
+ new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+ | KeyEvent.FLAG_EDITOR_ACTION));
+ }
+ }
+
+ /**
+ * Set the private content type of the text, which is the
+ * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
+ * field that will be filled in when creating an input connection.
+ *
+ * @see #getPrivateImeOptions()
+ * @see EditorInfo#privateImeOptions
+ * @attr ref android.R.styleable#TextView_privateImeOptions
+ */
+ public void setPrivateImeOptions(String type) {
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.privateImeOptions = type;
+ }
+
+ /**
+ * Get the private type of the content.
+ *
+ * @see #setPrivateImeOptions(String)
+ * @see EditorInfo#privateImeOptions
+ */
+ public String getPrivateImeOptions() {
+ return mEditor != null && mEditor.mInputContentType != null
+ ? mEditor.mInputContentType.privateImeOptions : null;
+ }
+
+ /**
+ * Set the extra input data of the text, which is the
+ * {@link EditorInfo#extras TextBoxAttribute.extras}
+ * Bundle that will be filled in when creating an input connection. The
+ * given integer is the resource identifier of an XML resource holding an
+ * {@link android.R.styleable#InputExtras <input-extras>} XML tree.
+ *
+ * @see #getInputExtras(boolean)
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public void setInputExtras(@XmlRes int xmlResId) throws XmlPullParserException, IOException {
+ createEditorIfNeeded();
+ XmlResourceParser parser = getResources().getXml(xmlResId);
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.extras = new Bundle();
+ getResources().parseBundleExtras(parser, mEditor.mInputContentType.extras);
+ }
+
+ /**
+ * Retrieve the input extras currently associated with the text view, which
+ * can be viewed as well as modified.
+ *
+ * @param create If true, the extras will be created if they don't already
+ * exist. Otherwise, null will be returned if none have been created.
+ * @see #setInputExtras(int)
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public Bundle getInputExtras(boolean create) {
+ if (mEditor == null && !create) return null;
+ createEditorIfNeeded();
+ if (mEditor.mInputContentType == null) {
+ if (!create) return null;
+ mEditor.createInputContentTypeIfNeeded();
+ }
+ if (mEditor.mInputContentType.extras == null) {
+ if (!create) return null;
+ mEditor.mInputContentType.extras = new Bundle();
+ }
+ return mEditor.mInputContentType.extras;
+ }
+
+ /**
+ * Change "hint" locales associated with the text view, which will be reported to an IME with
+ * {@link EditorInfo#hintLocales} when it has focus.
+ *
+ * Starting with Android O, this also causes internationalized listeners to be created (or
+ * change locale) based on the first locale in the input locale list.
+ *
+ * <p><strong>Note:</strong> If you want new "hint" to take effect immediately you need to
+ * call {@link InputMethodManager#restartInput(View)}.</p>
+ * @param hintLocales List of the languages that the user is supposed to switch to no matter
+ * what input method subtype is currently used. Set {@code null} to clear the current "hint".
+ * @see #getImeHintLocales()
+ * @see android.view.inputmethod.EditorInfo#hintLocales
+ */
+ public void setImeHintLocales(@Nullable LocaleList hintLocales) {
+ createEditorIfNeeded();
+ mEditor.createInputContentTypeIfNeeded();
+ mEditor.mInputContentType.imeHintLocales = hintLocales;
+ if (mUseInternationalizedInput) {
+ changeListenerLocaleTo(hintLocales == null ? null : hintLocales.get(0));
+ }
+ }
+
+ /**
+ * @return The current languages list "hint". {@code null} when no "hint" is available.
+ * @see #setImeHintLocales(LocaleList)
+ * @see android.view.inputmethod.EditorInfo#hintLocales
+ */
+ @Nullable
+ public LocaleList getImeHintLocales() {
+ if (mEditor == null) {
+ return null;
+ }
+ if (mEditor.mInputContentType == null) {
+ return null;
+ }
+ return mEditor.mInputContentType.imeHintLocales;
+ }
+
+ /**
+ * 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 mEditor == null ? null : mEditor.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.
+ */
+ @android.view.RemotableViewMethod
+ public void setError(CharSequence error) {
+ if (error == null) {
+ setError(null, null);
+ } else {
+ Drawable dr = getContext().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) {
+ createEditorIfNeeded();
+ mEditor.setError(error, icon);
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean result = super.setFrame(l, t, r, b);
+
+ if (mEditor != null) mEditor.setFrame();
+
+ restartMarqueeIfNeeded();
+
+ return result;
+ }
+
+ private void restartMarqueeIfNeeded() {
+ if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ mRestartMarquee = false;
+ startMarquee();
+ }
+ }
+
+ /**
+ * 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 (mEditor != null) {
+ final boolean undoFilter = mEditor.mUndoInputFilter != null;
+ final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
+ int num = 0;
+ if (undoFilter) num++;
+ if (keyFilter) num++;
+ if (num > 0) {
+ InputFilter[] nf = new InputFilter[filters.length + num];
+
+ System.arraycopy(filters, 0, nf, 0, filters.length);
+ num = 0;
+ if (undoFilter) {
+ nf[filters.length] = mEditor.mUndoInputFilter;
+ num++;
+ }
+ if (keyFilter) {
+ nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
+ }
+
+ e.setFilters(nf);
+ return;
+ }
+ }
+ e.setFilters(filters);
+ }
+
+ /**
+ * Returns the current list of input filters.
+ *
+ * @attr ref android.R.styleable#TextView_maxLength
+ */
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+
+ private int getBoxHeight(Layout l) {
+ Insets opticalInsets = isLayoutModeOptical(mParent) ? getOpticalInsets() : Insets.NONE;
+ int padding = (l == mHintLayout)
+ ? getCompoundPaddingTop() + getCompoundPaddingBottom()
+ : getExtendedPaddingTop() + getExtendedPaddingBottom();
+ return getMeasuredHeight() - padding + opticalInsets.top + opticalInsets.bottom;
+ }
+
+ 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 = getBoxHeight(l);
+ 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 = getBoxHeight(l);
+ 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;
+ }
+
+ void invalidateCursorPath() {
+ if (mHighlightPathBogus) {
+ invalidateCursor();
+ } else {
+ final int horizontalPadding = getCompoundPaddingLeft();
+ final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
+
+ if (mEditor.mCursorDrawable == null) {
+ synchronized (TEMP_RECTF) {
+ /*
+ * The reason for this concern about the thickness of the
+ * cursor and doing the floor/ceil on the coordinates is that
+ * some EditTexts (notably textfields in the Browser) have
+ * anti-aliased text where not all the characters are
+ * necessarily at integer-multiple locations. This should
+ * make sure the entire cursor gets invalidated instead of
+ * sometimes missing half a pixel.
+ */
+ float thick = (float) Math.ceil(mTextPaint.getStrokeWidth());
+ if (thick < 1.0f) {
+ thick = 1.0f;
+ }
+
+ thick /= 2.0f;
+
+ // mHighlightPath is guaranteed to be non null at that point.
+ mHighlightPath.computeBounds(TEMP_RECTF, false);
+
+ invalidate((int) Math.floor(horizontalPadding + TEMP_RECTF.left - thick),
+ (int) Math.floor(verticalPadding + TEMP_RECTF.top - thick),
+ (int) Math.ceil(horizontalPadding + TEMP_RECTF.right + thick),
+ (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick));
+ }
+ } else {
+ final Rect bounds = mEditor.mCursorDrawable.getBounds();
+ invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
+ bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
+ }
+ }
+ }
+
+ void invalidateCursor() {
+ int where = getSelectionEnd();
+
+ invalidateCursor(where, where, where);
+ }
+
+ private void invalidateCursor(int a, int b, int c) {
+ if (a >= 0 || b >= 0 || c >= 0) {
+ int start = Math.min(Math.min(a, b), c);
+ int end = Math.max(Math.max(a, b), c);
+ invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
+ }
+ }
+
+ /**
+ * Invalidates the region of text enclosed between the start and end text offsets.
+ */
+ void invalidateRegion(int start, int end, boolean invalidateCursor) {
+ if (mLayout == null) {
+ invalidate();
+ } else {
+ int lineStart = mLayout.getLineForOffset(start);
+ int top = mLayout.getLineTop(lineStart);
+
+ // 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 (lineStart > 0) {
+ top -= mLayout.getLineDescent(lineStart - 1);
+ }
+
+ int lineEnd;
+
+ if (start == end) {
+ lineEnd = lineStart;
+ } else {
+ lineEnd = mLayout.getLineForOffset(end);
+ }
+
+ int bottom = mLayout.getLineBottom(lineEnd);
+
+ // mEditor can be null in case selection is set programmatically.
+ if (invalidateCursor && mEditor != null && mEditor.mCursorDrawable != null) {
+ final Rect bounds = mEditor.mCursorDrawable.getBounds();
+ top = Math.min(top, bounds.top);
+ bottom = Math.max(bottom, bounds.bottom);
+ }
+
+ final int compoundPaddingLeft = getCompoundPaddingLeft();
+ final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
+
+ int left, right;
+ if (lineStart == lineEnd && !invalidateCursor) {
+ left = (int) mLayout.getPrimaryHorizontal(start);
+ right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
+ left += compoundPaddingLeft;
+ right += compoundPaddingLeft;
+ } else {
+ // Rectangle bounding box when the region spans several lines
+ left = compoundPaddingLeft;
+ right = getWidth() - getCompoundPaddingRight();
+ }
+
+ invalidate(mScrollX + left, verticalPadding + top,
+ mScrollX + right, verticalPadding + bottom);
+ }
+ }
+
+ private void registerForPreDraw() {
+ if (!mPreDrawRegistered) {
+ getViewTreeObserver().addOnPreDrawListener(this);
+ mPreDrawRegistered = true;
+ }
+ }
+
+ private void unregisterForPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ mPreDrawRegistered = false;
+ mPreDrawListenerDetached = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPreDraw() {
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ if (mMovement != null) {
+ /* This code also provides auto-scrolling when a cursor is moved using a
+ * CursorController (insertion point or selection limits).
+ * For selection, ensure start or end is visible depending on controller's state.
+ */
+ int curs = getSelectionEnd();
+ // Do not create the controller if it is not already created.
+ if (mEditor != null && mEditor.mSelectionModifierCursorController != null
+ && mEditor.mSelectionModifierCursorController.isSelectionStartDragged()) {
+ curs = getSelectionStart();
+ }
+
+ /*
+ * 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) {
+ bringPointIntoView(curs);
+ }
+ } else {
+ bringTextIntoView();
+ }
+
+ // This has to be checked here since:
+ // - onFocusChanged cannot start it when focus is given to a view with selected text (after
+ // a screen rotation) since layout is not yet initialized at that point.
+ if (mEditor != null && mEditor.mCreatedWithASelection) {
+ mEditor.refreshTextActionMode();
+ mEditor.mCreatedWithASelection = false;
+ }
+
+ unregisterForPreDraw();
+
+ return true;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mEditor != null) mEditor.onAttachedToWindow();
+
+ if (mPreDrawListenerDetached) {
+ getViewTreeObserver().addOnPreDrawListener(this);
+ mPreDrawListenerDetached = false;
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void onDetachedFromWindowInternal() {
+ if (mPreDrawRegistered) {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ mPreDrawListenerDetached = true;
+ }
+
+ resetResolvedDrawables();
+
+ if (mEditor != null) mEditor.onDetachedFromWindow();
+
+ super.onDetachedFromWindowInternal();
+ }
+
+ @Override
+ public void onScreenStateChanged(int screenState) {
+ super.onScreenStateChanged(screenState);
+ if (mEditor != null) mEditor.onScreenStateChanged(screenState);
+ }
+
+ @Override
+ protected boolean isPaddingOffsetRequired() {
+ return mShadowRadius != 0 || mDrawables != null;
+ }
+
+ @Override
+ protected int getLeftPaddingOffset() {
+ return getCompoundPaddingLeft() - mPaddingLeft
+ + (int) Math.min(0, mShadowDx - mShadowRadius);
+ }
+
+ @Override
+ protected int getTopPaddingOffset() {
+ return (int) Math.min(0, mShadowDy - mShadowRadius);
+ }
+
+ @Override
+ protected int getBottomPaddingOffset() {
+ return (int) Math.max(0, mShadowDy + mShadowRadius);
+ }
+
+ @Override
+ protected int getRightPaddingOffset() {
+ return -(getCompoundPaddingRight() - mPaddingRight)
+ + (int) Math.max(0, mShadowDx + mShadowRadius);
+ }
+
+ @Override
+ protected boolean verifyDrawable(@NonNull Drawable who) {
+ final boolean verified = super.verifyDrawable(who);
+ if (!verified && mDrawables != null) {
+ for (Drawable dr : mDrawables.mShowing) {
+ if (who == dr) {
+ return true;
+ }
+ }
+ }
+ return verified;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mDrawables != null) {
+ for (Drawable dr : mDrawables.mShowing) {
+ if (dr != null) {
+ dr.jumpToCurrentState();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ boolean handled = false;
+
+ if (verifyDrawable(drawable)) {
+ final Rect dirty = drawable.getBounds();
+ int scrollX = mScrollX;
+ int scrollY = mScrollY;
+
+ // IMPORTANT: The coordinates below are based on the coordinates computed
+ // for each compound drawable in onDraw(). Make sure to update each section
+ // accordingly.
+ final TextView.Drawables drawables = mDrawables;
+ if (drawables != null) {
+ if (drawable == drawables.mShowing[Drawables.LEFT]) {
+ final int compoundPaddingTop = getCompoundPaddingTop();
+ final int compoundPaddingBottom = getCompoundPaddingBottom();
+ final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+
+ scrollX += mPaddingLeft;
+ scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
+ handled = true;
+ } else if (drawable == drawables.mShowing[Drawables.RIGHT]) {
+ final int compoundPaddingTop = getCompoundPaddingTop();
+ final int compoundPaddingBottom = getCompoundPaddingBottom();
+ final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+
+ scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
+ scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
+ handled = true;
+ } else if (drawable == drawables.mShowing[Drawables.TOP]) {
+ final int compoundPaddingLeft = getCompoundPaddingLeft();
+ final int compoundPaddingRight = getCompoundPaddingRight();
+ final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
+
+ scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
+ scrollY += mPaddingTop;
+ handled = true;
+ } else if (drawable == drawables.mShowing[Drawables.BOTTOM]) {
+ final int compoundPaddingLeft = getCompoundPaddingLeft();
+ final int compoundPaddingRight = getCompoundPaddingRight();
+ final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
+
+ scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
+ scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ invalidate(dirty.left + scrollX, dirty.top + scrollY,
+ dirty.right + scrollX, dirty.bottom + scrollY);
+ }
+ }
+
+ if (!handled) {
+ super.invalidateDrawable(drawable);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ // horizontal fading edge causes SaveLayerAlpha, which doesn't support alpha modulation
+ return ((getBackground() != null && getBackground().getCurrent() != null)
+ || mText instanceof Spannable || hasSelection()
+ || isHorizontalFadingEdgeEnabled());
+ }
+
+ /**
+ *
+ * Returns the state of the {@code textIsSelectable} flag (See
+ * {@link #setTextIsSelectable setTextIsSelectable()}). Although you have to set this flag
+ * to allow users to select and copy text in a non-editable TextView, the content of an
+ * {@link EditText} can always be selected, independently of the value of this flag.
+ * <p>
+ *
+ * @return True if the text displayed in this TextView can be selected by the user.
+ *
+ * @attr ref android.R.styleable#TextView_textIsSelectable
+ */
+ public boolean isTextSelectable() {
+ return mEditor == null ? false : mEditor.mTextIsSelectable;
+ }
+
+ /**
+ * Sets whether the content of this view is selectable by the user. The default is
+ * {@code false}, meaning that the content is not selectable.
+ * <p>
+ * When you use a TextView to display a useful piece of information to the user (such as a
+ * contact's address), make it selectable, so that the user can select and copy its
+ * content. You can also use set the XML attribute
+ * {@link android.R.styleable#TextView_textIsSelectable} to "true".
+ * <p>
+ * When you call this method to set the value of {@code textIsSelectable}, it sets
+ * the flags {@code focusable}, {@code focusableInTouchMode}, {@code clickable},
+ * and {@code longClickable} to the same value. These flags correspond to the attributes
+ * {@link android.R.styleable#View_focusable android:focusable},
+ * {@link android.R.styleable#View_focusableInTouchMode android:focusableInTouchMode},
+ * {@link android.R.styleable#View_clickable android:clickable}, and
+ * {@link android.R.styleable#View_longClickable android:longClickable}. To restore any of these
+ * flags to a state you had set previously, call one or more of the following methods:
+ * {@link #setFocusable(boolean) setFocusable()},
+ * {@link #setFocusableInTouchMode(boolean) setFocusableInTouchMode()},
+ * {@link #setClickable(boolean) setClickable()} or
+ * {@link #setLongClickable(boolean) setLongClickable()}.
+ *
+ * @param selectable Whether the content of this TextView should be selectable.
+ */
+ public void setTextIsSelectable(boolean selectable) {
+ if (!selectable && mEditor == null) return; // false is default value with no edit data
+
+ createEditorIfNeeded();
+ if (mEditor.mTextIsSelectable == selectable) return;
+
+ mEditor.mTextIsSelectable = selectable;
+ setFocusableInTouchMode(selectable);
+ setFocusable(FOCUSABLE_AUTO);
+ setClickable(selectable);
+ setLongClickable(selectable);
+
+ // mInputType should already be EditorInfo.TYPE_NULL and mInput should be null
+
+ setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
+ setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
+
+ // Called by setText above, but safer in case of future code changes
+ mEditor.prepareCursorControllers();
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState;
+
+ if (mSingleLine) {
+ drawableState = super.onCreateDrawableState(extraSpace);
+ } else {
+ drawableState = super.onCreateDrawableState(extraSpace + 1);
+ mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
+ }
+
+ if (isTextSelectable()) {
+ // Disable pressed state, which was introduced when TextView was made clickable.
+ // Prevents text color change.
+ // setClickable(false) would have a similar effect, but it also disables focus changes
+ // and long press actions, which are both needed by text selection.
+ final int length = drawableState.length;
+ for (int i = 0; i < length; i++) {
+ if (drawableState[i] == R.attr.state_pressed) {
+ final int[] nonPressedState = new int[length - 1];
+ System.arraycopy(drawableState, 0, nonPressedState, 0, i);
+ System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
+ return nonPressedState;
+ }
+ }
+ }
+
+ return drawableState;
+ }
+
+ private Path getUpdatedHighlightPath() {
+ Path highlight = null;
+ Paint highlightPaint = mHighlightPaint;
+
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+ if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
+ if (selStart == selEnd) {
+ if (mEditor != null && mEditor.isCursorVisible()
+ && (SystemClock.uptimeMillis() - mEditor.mShowCursor)
+ % (2 * Editor.BLINK) < Editor.BLINK) {
+ if (mHighlightPathBogus) {
+ if (mHighlightPath == null) mHighlightPath = new Path();
+ mHighlightPath.reset();
+ mLayout.getCursorPath(selStart, mHighlightPath, mText);
+ mEditor.updateCursorPosition();
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ highlightPaint.setColor(mCurTextColor);
+ highlightPaint.setStyle(Paint.Style.STROKE);
+ highlight = mHighlightPath;
+ }
+ } else {
+ if (mHighlightPathBogus) {
+ if (mHighlightPath == null) mHighlightPath = new Path();
+ mHighlightPath.reset();
+ mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ highlightPaint.setColor(mHighlightColor);
+ highlightPaint.setStyle(Paint.Style.FILL);
+
+ highlight = mHighlightPath;
+ }
+ }
+ return highlight;
+ }
+
+ /**
+ * @hide
+ */
+ public int getHorizontalOffsetForDrawables() {
+ return 0;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ restartMarqueeIfNeeded();
+
+ // 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;
+ final boolean isLayoutRtl = isLayoutRtl();
+ final int offset = getHorizontalOffsetForDrawables();
+ final int leftOffset = isLayoutRtl ? 0 : offset;
+ final int rightOffset = isLayoutRtl ? offset : 0;
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ /*
+ * 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;
+
+ // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+ // Make sure to update invalidateDrawable() when changing this code.
+ if (dr.mShowing[Drawables.LEFT] != null) {
+ canvas.save();
+ canvas.translate(scrollX + mPaddingLeft + leftOffset,
+ scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
+ dr.mShowing[Drawables.LEFT].draw(canvas);
+ canvas.restore();
+ }
+
+ // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+ // Make sure to update invalidateDrawable() when changing this code.
+ if (dr.mShowing[Drawables.RIGHT] != null) {
+ canvas.save();
+ canvas.translate(scrollX + right - left - mPaddingRight
+ - dr.mDrawableSizeRight - rightOffset,
+ scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
+ dr.mShowing[Drawables.RIGHT].draw(canvas);
+ canvas.restore();
+ }
+
+ // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+ // Make sure to update invalidateDrawable() when changing this code.
+ if (dr.mShowing[Drawables.TOP] != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft
+ + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
+ dr.mShowing[Drawables.TOP].draw(canvas);
+ canvas.restore();
+ }
+
+ // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+ // Make sure to update invalidateDrawable() when changing this code.
+ if (dr.mShowing[Drawables.BOTTOM] != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft
+ + (hspace - dr.mDrawableWidthBottom) / 2,
+ scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
+ dr.mShowing[Drawables.BOTTOM].draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ int color = mCurTextColor;
+
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ Layout layout = mLayout;
+
+ 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();
+
+ final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+ final int maxScrollY = mLayout.getHeight() - vspace;
+
+ float clipLeft = compoundPaddingLeft + scrollX;
+ float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
+ float clipRight = right - left - getCompoundPaddingRight() + scrollX;
+ float clipBottom = bottom - top + scrollY
+ - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
+
+ 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);
+
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+ if (isMarqueeFadeEnabled()) {
+ if (!mSingleLine && getLineCount() == 1 && canMarquee()
+ && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
+ final int width = mRight - mLeft;
+ final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
+ final float dx = mLayout.getLineRight(0) - (width - padding);
+ canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+ }
+
+ if (mMarquee != null && mMarquee.isRunning()) {
+ final float dx = -mMarquee.getScroll();
+ canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+ }
+ }
+
+ final int cursorOffsetVertical = voffsetCursor - voffsetText;
+
+ Path highlight = getUpdatedHighlightPath();
+ if (mEditor != null) {
+ mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
+ } else {
+ layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+ }
+
+ if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+ final float dx = mMarquee.getGhostOffset();
+ canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+ layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+ }
+
+ canvas.restore();
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ if (mLayout == null) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int selEnd = getSelectionEnd();
+ if (selEnd < 0) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int selStart = getSelectionStart();
+ if (selStart < 0 || selStart >= selEnd) {
+ int line = mLayout.getLineForOffset(selEnd);
+ r.top = mLayout.getLineTop(line);
+ r.bottom = mLayout.getLineBottom(line);
+ r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
+ r.right = r.left + 4;
+ } else {
+ int lineStart = mLayout.getLineForOffset(selStart);
+ int lineEnd = mLayout.getLineForOffset(selEnd);
+ r.top = mLayout.getLineTop(lineStart);
+ r.bottom = mLayout.getLineBottom(lineEnd);
+ if (lineStart == lineEnd) {
+ r.left = (int) mLayout.getPrimaryHorizontal(selStart);
+ r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
+ } else {
+ // Selection extends across multiple lines -- make the focused
+ // rect cover the entire width.
+ if (mHighlightPathBogus) {
+ if (mHighlightPath == null) mHighlightPath = new Path();
+ mHighlightPath.reset();
+ mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
+ mHighlightPathBogus = false;
+ }
+ synchronized (TEMP_RECTF) {
+ mHighlightPath.computeBounds(TEMP_RECTF, true);
+ r.left = (int) TEMP_RECTF.left - 1;
+ r.right = (int) TEMP_RECTF.right + 1;
+ }
+ }
+ }
+
+ // Adjust for padding and gravity.
+ int paddingLeft = getCompoundPaddingLeft();
+ int paddingTop = getExtendedPaddingTop();
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ paddingTop += getVerticalOffset(false);
+ }
+ r.offset(paddingLeft, paddingTop);
+ int paddingBottom = getExtendedPaddingBottom();
+ r.bottom += paddingBottom;
+ }
+
+ /**
+ * 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();
+ }
+
+ return getBaselineOffset() + mLayout.getLineBaseline(0);
+ }
+
+ int getBaselineOffset() {
+ int voffset = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset = getVerticalOffset(true);
+ }
+
+ if (isLayoutModeOptical(mParent)) {
+ voffset -= getOpticalInsets().top;
+ }
+
+ return getExtendedPaddingTop() + voffset;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected int getFadeTop(boolean offsetRequired) {
+ if (mLayout == null) return 0;
+
+ int voffset = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset = getVerticalOffset(true);
+ }
+
+ if (offsetRequired) voffset += getTopPaddingOffset();
+
+ return getExtendedPaddingTop() + voffset;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected int getFadeHeight(boolean offsetRequired) {
+ return mLayout != null ? mLayout.getHeight() : 0;
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (mText instanceof Spannable && mLinksClickable) {
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ final int offset = getOffsetForPosition(x, y);
+ final ClickableSpan[] clickables = ((Spannable) mText).getSpans(offset, offset,
+ ClickableSpan.class);
+ if (clickables.length > 0) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+ }
+ }
+ if (isTextSelectable() || isTextEditable()) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ // Note: If the IME is in fullscreen mode and IMS#mExtractEditText is in text action mode,
+ // InputMethodService#onKeyDown and InputMethodService#onKeyUp are responsible to call
+ // InputMethodService#mExtractEditText.maybeHandleBackInTextActionMode(event).
+ if (keyCode == KeyEvent.KEYCODE_BACK && handleBackInTextActionModeIfNeeded(event)) {
+ return true;
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean handleBackInTextActionModeIfNeeded(KeyEvent event) {
+ // Do nothing unless mEditor is in text action mode.
+ if (mEditor == null || mEditor.getTextActionMode() == null) {
+ return false;
+ }
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ state.handleUpEvent(event);
+ }
+ if (event.isTracking() && !event.isCanceled()) {
+ stopTextActionMode();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ final int which = doKeyDown(keyCode, event, null);
+ if (which == KEY_EVENT_NOT_HANDLED) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
+ final int which = doKeyDown(keyCode, down, event);
+ if (which == KEY_EVENT_NOT_HANDLED) {
+ // Go through default dispatching.
+ return super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+ if (which == KEY_EVENT_HANDLED) {
+ // Consumed the whole thing.
+ return true;
+ }
+
+ repeatCount--;
+
+ // We are going to dispatch the remaining events to either the input
+ // or movement method. To do this, we will just send a repeated stream
+ // of down and up events until we have done the complete repeatCount.
+ // It would be nice if those interfaces had an onKeyMultiple() method,
+ // but adding that is a more complicated change.
+ KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
+ if (which == KEY_DOWN_HANDLED_BY_KEY_LISTENER) {
+ // mEditor and mEditor.mInput are not null from doKeyDown
+ mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
+ while (--repeatCount > 0) {
+ mEditor.mKeyListener.onKeyDown(this, (Editable) mText, keyCode, down);
+ mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
+ }
+ hideErrorIfUnchanged();
+
+ } else if (which == KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD) {
+ // mMovement is not null from doKeyDown
+ mMovement.onKeyUp(this, (Spannable) mText, keyCode, up);
+ while (--repeatCount > 0) {
+ mMovement.onKeyDown(this, (Spannable) mText, keyCode, down);
+ mMovement.onKeyUp(this, (Spannable) mText, keyCode, up);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if pressing ENTER in this field advances focus instead
+ * of inserting the character. This is true mostly in single-line fields,
+ * but also in mail addresses and subjects which will display on multiple
+ * lines but where it doesn't make sense to insert newlines.
+ */
+ private boolean shouldAdvanceFocusOnEnter() {
+ if (getKeyListener() == null) {
+ return false;
+ }
+
+ if (mSingleLine) {
+ return true;
+ }
+
+ if (mEditor != null
+ && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if pressing TAB in this field advances focus instead
+ * of inserting the character. Insert tabs only in multi-line editors.
+ */
+ private boolean shouldAdvanceFocusOnTab() {
+ if (getKeyListener() != null && !mSingleLine && mEditor != null
+ && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+ if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
+ || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isDirectionalNavigationKey(int keyCode) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ return true;
+ }
+ return false;
+ }
+
+ private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
+ if (!isEnabled()) {
+ return KEY_EVENT_NOT_HANDLED;
+ }
+
+ // If this is the initial keydown, we don't want to prevent a movement away from this view.
+ // While this shouldn't be necessary because any time we're preventing default movement we
+ // should be restricting the focus to remain within this view, thus we'll also receive
+ // the key up event, occasionally key up events will get dropped and we don't want to
+ // prevent the user from traversing out of this on the next key down.
+ if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
+ mPreventDefaultMovement = false;
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.hasNoModifiers()) {
+ // When mInputContentType is set, we know that we are
+ // running in a "modern" cupcake environment, so don't need
+ // to worry about the application trying to capture
+ // enter key events.
+ if (mEditor != null && mEditor.mInputContentType != null) {
+ // If there is an action listener, given them a
+ // chance to consume the event.
+ if (mEditor.mInputContentType.onEditorActionListener != null
+ && mEditor.mInputContentType.onEditorActionListener.onEditorAction(
+ this, EditorInfo.IME_NULL, event)) {
+ mEditor.mInputContentType.enterDown = true;
+ // We are consuming the enter key for them.
+ return KEY_EVENT_HANDLED;
+ }
+ }
+
+ // If our editor should move focus when enter is pressed, or
+ // this is a generated event from an IME action button, then
+ // don't let it be inserted into the text.
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+ || shouldAdvanceFocusOnEnter()) {
+ if (hasOnClickListeners()) {
+ return KEY_EVENT_NOT_HANDLED;
+ }
+ return KEY_EVENT_HANDLED;
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.hasNoModifiers()) {
+ if (shouldAdvanceFocusOnEnter()) {
+ return KEY_EVENT_NOT_HANDLED;
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+ if (shouldAdvanceFocusOnTab()) {
+ return KEY_EVENT_NOT_HANDLED;
+ }
+ }
+ break;
+
+ // Has to be done on key down (and not on key up) to correctly be intercepted.
+ case KeyEvent.KEYCODE_BACK:
+ if (mEditor != null && mEditor.getTextActionMode() != null) {
+ stopTextActionMode();
+ return KEY_EVENT_HANDLED;
+ }
+ break;
+
+ case KeyEvent.KEYCODE_CUT:
+ if (event.hasNoModifiers() && canCut()) {
+ if (onTextContextMenuItem(ID_CUT)) {
+ return KEY_EVENT_HANDLED;
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_COPY:
+ if (event.hasNoModifiers() && canCopy()) {
+ if (onTextContextMenuItem(ID_COPY)) {
+ return KEY_EVENT_HANDLED;
+ }
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PASTE:
+ if (event.hasNoModifiers() && canPaste()) {
+ if (onTextContextMenuItem(ID_PASTE)) {
+ return KEY_EVENT_HANDLED;
+ }
+ }
+ break;
+ }
+
+ if (mEditor != null && mEditor.mKeyListener != null) {
+ boolean doDown = true;
+ if (otherEvent != null) {
+ try {
+ beginBatchEdit();
+ final boolean handled = mEditor.mKeyListener.onKeyOther(this, (Editable) mText,
+ otherEvent);
+ hideErrorIfUnchanged();
+ doDown = false;
+ if (handled) {
+ return KEY_EVENT_HANDLED;
+ }
+ } catch (AbstractMethodError e) {
+ // onKeyOther was added after 1.0, so if it isn't
+ // implemented we need to try to dispatch as a regular down.
+ } finally {
+ endBatchEdit();
+ }
+ }
+
+ if (doDown) {
+ beginBatchEdit();
+ final boolean handled = mEditor.mKeyListener.onKeyDown(this, (Editable) mText,
+ keyCode, event);
+ endBatchEdit();
+ hideErrorIfUnchanged();
+ if (handled) return KEY_DOWN_HANDLED_BY_KEY_LISTENER;
+ }
+ }
+
+ // 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) {
+ boolean doDown = true;
+ if (otherEvent != null) {
+ try {
+ boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
+ otherEvent);
+ doDown = false;
+ if (handled) {
+ return KEY_EVENT_HANDLED;
+ }
+ } catch (AbstractMethodError e) {
+ // onKeyOther was added after 1.0, so if it isn't
+ // implemented we need to try to dispatch as a regular down.
+ }
+ }
+ if (doDown) {
+ if (mMovement.onKeyDown(this, (Spannable) mText, keyCode, event)) {
+ if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
+ mPreventDefaultMovement = true;
+ }
+ return KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD;
+ }
+ }
+ // Consume arrows from keyboard devices to prevent focus leaving the editor.
+ // DPAD/JOY devices (Gamepads, TV remotes) often lack a TAB key so allow those
+ // to move focus with arrows.
+ if (event.getSource() == InputDevice.SOURCE_KEYBOARD
+ && isDirectionalNavigationKey(keyCode)) {
+ return KEY_EVENT_HANDLED;
+ }
+ }
+
+ return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
+ ? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
+ }
+
+ /**
+ * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
+ * can be recorded.
+ * @hide
+ */
+ public void resetErrorChangedFlag() {
+ /*
+ * 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.
+ */
+ if (mEditor != null) mEditor.mErrorWasChanged = false;
+ }
+
+ /**
+ * @hide
+ */
+ public void hideErrorIfUnchanged() {
+ if (mEditor != null && mEditor.mError != null && !mEditor.mErrorWasChanged) {
+ setError(null, null);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (!isEnabled()) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ if (!KeyEvent.isModifierKey(keyCode)) {
+ mPreventDefaultMovement = false;
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.hasNoModifiers()) {
+ /*
+ * If there is a click listener, just call through to
+ * super, which will invoke it.
+ *
+ * If there isn't a click listener, try to show the soft
+ * input method. (It will also
+ * call performClick(), but that won't do anything in
+ * this case.)
+ */
+ if (!hasOnClickListeners()) {
+ if (mMovement != null && mText instanceof Editable
+ && mLayout != null && onCheckIsTextEditor()) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ viewClicked(imm);
+ if (imm != null && getShowSoftInputOnFocus()) {
+ imm.showSoftInput(this, 0);
+ }
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.hasNoModifiers()) {
+ if (mEditor != null && mEditor.mInputContentType != null
+ && mEditor.mInputContentType.onEditorActionListener != null
+ && mEditor.mInputContentType.enterDown) {
+ mEditor.mInputContentType.enterDown = false;
+ if (mEditor.mInputContentType.onEditorActionListener.onEditorAction(
+ this, EditorInfo.IME_NULL, event)) {
+ return true;
+ }
+ }
+
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+ || shouldAdvanceFocusOnEnter()) {
+ /*
+ * 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 (!hasOnClickListeners()) {
+ 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;
+ } else if ((event.getFlags()
+ & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
+ // No target for next focus, but make sure the IME
+ // if this came from it.
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+ break;
+ }
+
+ if (mEditor != null && mEditor.mKeyListener != null) {
+ if (mEditor.mKeyListener.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);
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return mEditor != null && mEditor.mInputType != EditorInfo.TYPE_NULL;
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (onCheckIsTextEditor() && isEnabled()) {
+ mEditor.createInputMethodStateIfNeeded();
+ outAttrs.inputType = getInputType();
+ if (mEditor.mInputContentType != null) {
+ outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
+ outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
+ outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
+ outAttrs.actionId = mEditor.mInputContentType.imeActionId;
+ outAttrs.extras = mEditor.mInputContentType.extras;
+ outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
+ } else {
+ outAttrs.imeOptions = EditorInfo.IME_NULL;
+ outAttrs.hintLocales = null;
+ }
+ if (focusSearch(FOCUS_DOWN) != null) {
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
+ }
+ if (focusSearch(FOCUS_UP) != null) {
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
+ }
+ if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
+ == EditorInfo.IME_ACTION_UNSPECIFIED) {
+ if ((outAttrs.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
+ // An action has not been set, but the enter key will move to
+ // the next focus, so set the action to that.
+ outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
+ } else {
+ // An action has not been set, and there is no focus to move
+ // to, so let's just supply a "done" action.
+ outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
+ }
+ if (!shouldAdvanceFocusOnEnter()) {
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+ }
+ }
+ if (isMultilineInputType(outAttrs.inputType)) {
+ // Multi-line text editors should always show an enter key.
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+ }
+ outAttrs.hintText = mHint;
+ if (mText instanceof Editable) {
+ InputConnection ic = new EditableInputConnection(this);
+ outAttrs.initialSelStart = getSelectionStart();
+ outAttrs.initialSelEnd = getSelectionEnd();
+ outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
+ return ic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * If this TextView contains editable content, extract a portion of it
+ * based on the information in <var>request</var> in to <var>outText</var>.
+ * @return Returns true if the text was successfully extracted, else false.
+ */
+ public boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+ createEditorIfNeeded();
+ return mEditor.extractText(request, outText);
+ }
+
+ /**
+ * This is used to remove all style-impacting spans from text before new
+ * extracted text is being replaced into it, so that we don't have any
+ * lingering spans applied during the replace.
+ */
+ static void removeParcelableSpans(Spannable spannable, int start, int end) {
+ Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ spannable.removeSpan(spans[i]);
+ }
+ }
+
+ /**
+ * Apply to this text view the given extracted text, as previously
+ * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
+ */
+ public void setExtractedText(ExtractedText text) {
+ Editable content = getEditableText();
+ if (text.text != null) {
+ if (content == null) {
+ setText(text.text, TextView.BufferType.EDITABLE);
+ } else {
+ int start = 0;
+ int end = content.length();
+
+ if (text.partialStartOffset >= 0) {
+ final int N = content.length();
+ start = text.partialStartOffset;
+ if (start > N) start = N;
+ end = text.partialEndOffset;
+ if (end > N) end = N;
+ }
+
+ removeParcelableSpans(content, start, end);
+ if (TextUtils.equals(content.subSequence(start, end), text.text)) {
+ if (text.text instanceof Spanned) {
+ // OK to copy spans only.
+ TextUtils.copySpansFrom((Spanned) text.text, 0, end - start,
+ Object.class, content, start);
+ }
+ } else {
+ content.replace(start, end, text.text);
+ }
+ }
+ }
+
+ // Now set the selection position... make sure it is in range, to
+ // avoid crashes. If this is a partial update, it is possible that
+ // the underlying text may have changed, causing us problems here.
+ // Also we just don't want to trust clients to do the right thing.
+ Spannable sp = (Spannable) getText();
+ final int N = sp.length();
+ int start = text.selectionStart;
+ if (start < 0) {
+ start = 0;
+ } else if (start > N) {
+ start = N;
+ }
+ int end = text.selectionEnd;
+ if (end < 0) {
+ end = 0;
+ } else if (end > N) {
+ end = N;
+ }
+ Selection.setSelection(sp, start, end);
+
+ // Finally, update the selection mode.
+ if ((text.flags & ExtractedText.FLAG_SELECTING) != 0) {
+ MetaKeyKeyListener.startSelecting(this, sp);
+ } else {
+ MetaKeyKeyListener.stopSelecting(this, sp);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void setExtracting(ExtractedTextRequest req) {
+ if (mEditor.mInputMethodState != null) {
+ mEditor.mInputMethodState.mExtractedTextRequest = req;
+ }
+ // This would stop a possible selection mode, but no such mode is started in case
+ // extracted mode will start. Some text is selected though, and will trigger an action mode
+ // in the extracted view.
+ mEditor.hideCursorAndSpanControllers();
+ stopTextActionMode();
+ if (mEditor.mSelectionModifierCursorController != null) {
+ mEditor.mSelectionModifierCursorController.resetTouchOffsets();
+ }
+ }
+
+ /**
+ * Called by the framework in response to a text completion from
+ * the current input method, provided by it calling
+ * {@link InputConnection#commitCompletion
+ * InputConnection.commitCompletion()}. The default implementation does
+ * nothing; text views that are supporting auto-completion should override
+ * this to do their desired behavior.
+ *
+ * @param text The auto complete text the user has selected.
+ */
+ public void onCommitCompletion(CompletionInfo text) {
+ // intentionally empty
+ }
+
+ /**
+ * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+ * dictionary) from the current input method, provided by it calling
+ * {@link InputConnection#commitCorrection(CorrectionInfo) InputConnection.commitCorrection()}.
+ * The default implementation flashes the background of the corrected word to provide
+ * feedback to the user.
+ *
+ * @param info The auto correct info about the text that was corrected.
+ */
+ public void onCommitCorrection(CorrectionInfo info) {
+ if (mEditor != null) mEditor.onCommitCorrection(info);
+ }
+
+ public void beginBatchEdit() {
+ if (mEditor != null) mEditor.beginBatchEdit();
+ }
+
+ public void endBatchEdit() {
+ if (mEditor != null) mEditor.endBatchEdit();
+ }
+
+ /**
+ * Called by the framework in response to a request to begin a batch
+ * of edit operations through a call to link {@link #beginBatchEdit()}.
+ */
+ public void onBeginBatchEdit() {
+ // intentionally empty
+ }
+
+ /**
+ * Called by the framework in response to a request to end a batch
+ * of edit operations through a call to link {@link #endBatchEdit}.
+ */
+ public void onEndBatchEdit() {
+ // intentionally empty
+ }
+
+ /**
+ * Called by the framework in response to a private command from the
+ * current method, provided by it calling
+ * {@link InputConnection#performPrivateCommand
+ * InputConnection.performPrivateCommand()}.
+ *
+ * @param action The action name of the command.
+ * @param data Any additional data for the command. This may be null.
+ * @return Return true if you handled the command, else false.
+ */
+ public boolean onPrivateIMECommand(String action, Bundle data) {
+ return false;
+ }
+
+ private void nullLayouts() {
+ if (mLayout instanceof BoringLayout && mSavedLayout == null) {
+ mSavedLayout = (BoringLayout) mLayout;
+ }
+ if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
+ mSavedHintLayout = (BoringLayout) mHintLayout;
+ }
+
+ mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
+
+ mBoring = mHintBoring = null;
+
+ // Since it depends on the value of mLayout
+ if (mEditor != null) mEditor.prepareCursorControllers();
+ }
+
+ /**
+ * 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);
+ }
+
+ private Layout.Alignment getLayoutAlignment() {
+ Layout.Alignment alignment;
+ switch (getTextAlignment()) {
+ case TEXT_ALIGNMENT_GRAVITY:
+ switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.START:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ break;
+ case Gravity.END:
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
+ break;
+ case Gravity.LEFT:
+ alignment = Layout.Alignment.ALIGN_LEFT;
+ break;
+ case Gravity.RIGHT:
+ alignment = Layout.Alignment.ALIGN_RIGHT;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ alignment = Layout.Alignment.ALIGN_CENTER;
+ break;
+ default:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ break;
+ }
+ break;
+ case TEXT_ALIGNMENT_TEXT_START:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ break;
+ case TEXT_ALIGNMENT_TEXT_END:
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
+ break;
+ case TEXT_ALIGNMENT_CENTER:
+ alignment = Layout.Alignment.ALIGN_CENTER;
+ break;
+ case TEXT_ALIGNMENT_VIEW_START:
+ alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
+ ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
+ break;
+ case TEXT_ALIGNMENT_VIEW_END:
+ alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
+ ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
+ break;
+ case TEXT_ALIGNMENT_INHERIT:
+ // This should never happen as we have already resolved the text alignment
+ // but better safe than sorry so we just fall through
+ default:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ break;
+ }
+ return alignment;
+ }
+
+ /**
+ * The width passed in is now the desired layout width,
+ * not the full view width with padding.
+ * {@hide}
+ */
+ protected void makeNewLayout(int wantWidth, int hintWidth,
+ BoringLayout.Metrics boring,
+ BoringLayout.Metrics hintBoring,
+ int ellipsisWidth, boolean bringIntoView) {
+ stopMarquee();
+
+ // Update "old" cached values
+ mOldMaximum = mMaximum;
+ mOldMaxMode = mMaxMode;
+
+ mHighlightPathBogus = true;
+
+ if (wantWidth < 0) {
+ wantWidth = 0;
+ }
+ if (hintWidth < 0) {
+ hintWidth = 0;
+ }
+
+ Layout.Alignment alignment = getLayoutAlignment();
+ final boolean testDirChange = mSingleLine && mLayout != null
+ && (alignment == Layout.Alignment.ALIGN_NORMAL
+ || alignment == Layout.Alignment.ALIGN_OPPOSITE);
+ int oldDir = 0;
+ if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
+ boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
+ final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE
+ && mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
+ TruncateAt effectiveEllipsize = mEllipsize;
+ if (mEllipsize == TruncateAt.MARQUEE
+ && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
+ effectiveEllipsize = TruncateAt.END_SMALL;
+ }
+
+ if (mTextDir == null) {
+ mTextDir = getTextDirectionHeuristic();
+ }
+
+ mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
+ effectiveEllipsize, effectiveEllipsize == mEllipsize);
+ if (switchEllipsize) {
+ TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE
+ ? TruncateAt.END : TruncateAt.MARQUEE;
+ mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
+ shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
+ }
+
+ shouldEllipsize = mEllipsize != null;
+ mHintLayout = null;
+
+ if (mHint != null) {
+ if (shouldEllipsize) hintWidth = wantWidth;
+
+ if (hintBoring == UNKNOWN_BORING) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
+ mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring != null) {
+ if (hintBoring.width <= hintWidth
+ && (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
+ 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 if (shouldEllipsize && hintBoring.width <= hintWidth) {
+ if (mSavedHintLayout != null) {
+ mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ hintBoring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mHintLayout = BoringLayout.make(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ hintBoring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ }
+ }
+ }
+ // TODO: code duplication with makeSingleLayout()
+ if (mHintLayout == null) {
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
+ mHint.length(), mTextPaint, hintWidth)
+ .setAlignment(alignment)
+ .setTextDirection(mTextDir)
+ .setLineSpacing(mSpacingAdd, mSpacingMult)
+ .setIncludePad(mIncludePad)
+ .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+ .setBreakStrategy(mBreakStrategy)
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setJustificationMode(mJustificationMode)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ if (shouldEllipsize) {
+ builder.setEllipsize(mEllipsize)
+ .setEllipsizedWidth(ellipsisWidth);
+ }
+ mHintLayout = builder.build();
+ }
+ }
+
+ if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
+ registerForPreDraw();
+ }
+
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (!compressText(ellipsisWidth)) {
+ final int height = mLayoutParams.height;
+ // If the size of the view does not depend on the size of the text, try to
+ // start the marquee immediately
+ if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
+ startMarquee();
+ } else {
+ // Defer the start of the marquee until we know our width (see setFrame())
+ mRestartMarquee = true;
+ }
+ }
+ }
+
+ // CursorControllers need a non-null mLayout
+ if (mEditor != null) mEditor.prepareCursorControllers();
+ }
+
+ /**
+ * @hide
+ */
+ protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
+ Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
+ boolean useSaved) {
+ Layout result = null;
+ if (mText instanceof Spannable) {
+ final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
+ wantWidth)
+ .setDisplayText(mTransformed)
+ .setAlignment(alignment)
+ .setTextDirection(mTextDir)
+ .setLineSpacing(mSpacingAdd, mSpacingMult)
+ .setIncludePad(mIncludePad)
+ .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+ .setBreakStrategy(mBreakStrategy)
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setJustificationMode(mJustificationMode)
+ .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
+ .setEllipsizedWidth(ellipsisWidth);
+ result = builder.build();
+ } else {
+ if (boring == UNKNOWN_BORING) {
+ boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ }
+
+ if (boring != null) {
+ if (boring.width <= wantWidth
+ && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
+ if (useSaved && mSavedLayout != null) {
+ result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ } else {
+ result = BoringLayout.make(mTransformed, mTextPaint,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ }
+
+ if (useSaved) {
+ mSavedLayout = (BoringLayout) result;
+ }
+ } else if (shouldEllipsize && boring.width <= wantWidth) {
+ if (useSaved && mSavedLayout != null) {
+ result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, effectiveEllipsize,
+ ellipsisWidth);
+ } else {
+ result = BoringLayout.make(mTransformed, mTextPaint,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, effectiveEllipsize,
+ ellipsisWidth);
+ }
+ }
+ }
+ }
+ if (result == null) {
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
+ 0, mTransformed.length(), mTextPaint, wantWidth)
+ .setAlignment(alignment)
+ .setTextDirection(mTextDir)
+ .setLineSpacing(mSpacingAdd, mSpacingMult)
+ .setIncludePad(mIncludePad)
+ .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+ .setBreakStrategy(mBreakStrategy)
+ .setHyphenationFrequency(mHyphenationFrequency)
+ .setJustificationMode(mJustificationMode)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ if (shouldEllipsize) {
+ builder.setEllipsize(effectiveEllipsize)
+ .setEllipsizedWidth(ellipsisWidth);
+ }
+ result = builder.build();
+ }
+ return result;
+ }
+
+ private boolean compressText(float width) {
+ if (isHardwareAccelerated()) return false;
+
+ // Only compress the text if it hasn't been compressed by the previous pass
+ if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX
+ && mTextPaint.getTextScaleX() == 1.0f) {
+ final float textWidth = mLayout.getLineWidth(0);
+ final float overflow = (textWidth + 1.0f - width) / width;
+ if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
+ mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
+ post(new Runnable() {
+ public void run() {
+ requestLayout();
+ }
+ });
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ 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) Math.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.
+ *
+ * @see #getIncludeFontPadding()
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ public void setIncludeFontPadding(boolean includepad) {
+ if (mIncludePad != includepad) {
+ mIncludePad = includepad;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Gets whether the TextView includes extra top and bottom padding to make
+ * room for accents that go above the normal ascent and descent.
+ *
+ * @see #setIncludeFontPadding(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ public boolean getIncludeFontPadding() {
+ return mIncludePad;
+ }
+
+ 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;
+
+ if (mTextDir == null) {
+ mTextDir = getTextDirectionHeuristic();
+ }
+
+ int des = -1;
+ boolean fromexisting = false;
+ final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
+ ? (float) widthSize : Float.MAX_VALUE;
+
+ 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, mTextDir, mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ } else {
+ fromexisting = true;
+ }
+
+ if (boring == null || boring == UNKNOWN_BORING) {
+ if (des < 0) {
+ des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
+ mTransformed.length(), mTextPaint, mTextDir, widthLimit));
+ }
+ width = des;
+ } else {
+ width = boring.width;
+ }
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ width = Math.max(width, dr.mDrawableWidthTop);
+ width = Math.max(width, dr.mDrawableWidthBottom);
+ }
+
+ if (mHint != null) {
+ int hintDes = -1;
+ int hintWidth;
+
+ if (mHintLayout != null && mEllipsize == null) {
+ hintDes = desired(mHintLayout);
+ }
+
+ if (hintDes < 0) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
+ if (hintDes < 0) {
+ hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
+ mHint.length(), mTextPaint, mTextDir, widthLimit));
+ }
+ 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;
+
+ if (mHorizontallyScrolling) want = VERY_WIDE;
+
+ int hintWant = want;
+ int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
+
+ if (mLayout == null) {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+ } else {
+ final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
+ || (mLayout.getEllipsizedWidth()
+ != width - getCompoundPaddingLeft() - getCompoundPaddingRight());
+
+ final boolean widthChanged = (mHint == null) && (mEllipsize == null)
+ && (want > mLayout.getWidth())
+ && (mLayout instanceof BoringLayout
+ || (fromexisting && des >= 0 && des <= want));
+
+ final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
+
+ if (layoutChanged || maximumChanged) {
+ if (!maximumChanged && widthChanged) {
+ mLayout.increaseWidthTo(want);
+ } else {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+ }
+ } else {
+ // Nothing has 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, heightSize);
+ }
+ }
+
+ 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);
+ }
+
+ /**
+ * Automatically computes and sets the text size.
+ */
+ private void autoSizeText() {
+ if (!isAutoSizeEnabled()) {
+ return;
+ }
+
+ if (mNeedsAutoSizeText) {
+ if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
+ return;
+ }
+
+ final int availableWidth = mHorizontallyScrolling
+ ? VERY_WIDE
+ : getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
+ final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
+ - getExtendedPaddingTop();
+
+ if (availableWidth <= 0 || availableHeight <= 0) {
+ return;
+ }
+
+ synchronized (TEMP_RECTF) {
+ TEMP_RECTF.setEmpty();
+ TEMP_RECTF.right = availableWidth;
+ TEMP_RECTF.bottom = availableHeight;
+ final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
+
+ if (optimalTextSize != getTextSize()) {
+ setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
+ false /* shouldRequestLayout */);
+
+ makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
+ mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+ false /* bringIntoView */);
+ }
+ }
+ }
+ // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
+ // after the next layout pass should set this to false.
+ mNeedsAutoSizeText = true;
+ }
+
+ /**
+ * Performs a binary search to find the largest text size that will still fit within the size
+ * available to this view.
+ */
+ private int findLargestTextSizeWhichFits(RectF availableSpace) {
+ final int sizesCount = mAutoSizeTextSizesInPx.length;
+ if (sizesCount == 0) {
+ throw new IllegalStateException("No available text sizes to choose from.");
+ }
+
+ int bestSizeIndex = 0;
+ int lowIndex = bestSizeIndex + 1;
+ int highIndex = sizesCount - 1;
+ int sizeToTryIndex;
+ while (lowIndex <= highIndex) {
+ sizeToTryIndex = (lowIndex + highIndex) / 2;
+ if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
+ bestSizeIndex = lowIndex;
+ lowIndex = sizeToTryIndex + 1;
+ } else {
+ highIndex = sizeToTryIndex - 1;
+ bestSizeIndex = highIndex;
+ }
+ }
+
+ return mAutoSizeTextSizesInPx[bestSizeIndex];
+ }
+
+ private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) {
+ final CharSequence text = mTransformed != null
+ ? mTransformed
+ : getText();
+ final int maxLines = getMaxLines();
+ if (mTempTextPaint == null) {
+ mTempTextPaint = new TextPaint();
+ } else {
+ mTempTextPaint.reset();
+ }
+ mTempTextPaint.set(getPaint());
+ mTempTextPaint.setTextSize(suggestedSizeInPx);
+
+ final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
+ text, 0, text.length(), mTempTextPaint, Math.round(availableSpace.right));
+
+ layoutBuilder.setAlignment(getLayoutAlignment())
+ .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
+ .setIncludePad(getIncludeFontPadding())
+ .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+ .setBreakStrategy(getBreakStrategy())
+ .setHyphenationFrequency(getHyphenationFrequency())
+ .setJustificationMode(getJustificationMode())
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
+ .setTextDirection(getTextDirectionHeuristic());
+
+ final StaticLayout layout = layoutBuilder.build();
+
+ // Lines overflow.
+ if (maxLines != -1 && layout.getLineCount() > maxLines) {
+ return false;
+ }
+
+ // Height overflow.
+ if (layout.getHeight() > availableSpace.bottom) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private int getDesiredHeight() {
+ return Math.max(
+ getDesiredHeight(mLayout, true),
+ getDesiredHeight(mHintLayout, mEllipsize != null));
+ }
+
+ private int getDesiredHeight(Layout layout, boolean cap) {
+ if (layout == null) {
+ return 0;
+ }
+
+ /*
+ * Don't cap the hint to a certain number of lines.
+ * (Do cap it, though, if we have a maximum pixel height.)
+ */
+ int desired = layout.getHeight(cap);
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ desired = Math.max(desired, dr.mDrawableHeightLeft);
+ desired = Math.max(desired, dr.mDrawableHeightRight);
+ }
+
+ int linecount = layout.getLineCount();
+ final int padding = getCompoundPaddingTop() + getCompoundPaddingBottom();
+ desired += padding;
+
+ if (mMaxMode != LINES) {
+ desired = Math.min(desired, mMaximum);
+ } else if (cap && linecount > mMaximum && layout instanceof DynamicLayout) {
+ desired = layout.getLineTop(mMaximum);
+
+ if (dr != null) {
+ desired = Math.max(desired, dr.mDrawableHeightLeft);
+ desired = Math.max(desired, dr.mDrawableHeightRight);
+ }
+
+ desired += padding;
+ linecount = 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.MATCH_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);
+
+ if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
+ // In a fixed-height view, so use our new text layout.
+ if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
+ && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
+ autoSizeText();
+ 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)) {
+ autoSizeText();
+ 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();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mDeferScroll >= 0) {
+ int curs = mDeferScroll;
+ mDeferScroll = -1;
+ bringPointIntoView(Math.min(curs, mText.length()));
+ }
+ // Call auto-size after the width and height have been calculated.
+ autoSizeText();
+ }
+
+ private boolean isShowingHint() {
+ return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint);
+ }
+
+ /**
+ * Returns true if anything changed.
+ */
+ private boolean bringTextIntoView() {
+ Layout layout = isShowingHint() ? mHintLayout : mLayout;
+ int line = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ line = layout.getLineCount() - 1;
+ }
+
+ Layout.Alignment a = layout.getParagraphAlignment(line);
+ int dir = layout.getParagraphDirection(line);
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+ int ht = layout.getHeight();
+
+ int scrollx, scrolly;
+
+ // Convert to left, center, or right alignment.
+ if (a == Layout.Alignment.ALIGN_NORMAL) {
+ a = dir == Layout.DIR_LEFT_TO_RIGHT
+ ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
+ } else if (a == Layout.Alignment.ALIGN_OPPOSITE) {
+ a = dir == Layout.DIR_LEFT_TO_RIGHT
+ ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
+ }
+
+ 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) Math.floor(layout.getLineLeft(line));
+ int right = (int) Math.ceil(layout.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_RIGHT) {
+ int right = (int) Math.ceil(layout.getLineRight(line));
+ scrollx = right - hspace;
+ } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
+ scrollx = (int) Math.floor(layout.getLineLeft(line));
+ }
+
+ 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;
+ }
+ }
+
+ /**
+ * Move the point, specified by the offset, into the view if it is needed.
+ * This has to be called after layout. Returns true if anything changed.
+ */
+ public boolean bringPointIntoView(int offset) {
+ if (isLayoutRequested()) {
+ mDeferScroll = offset;
+ return false;
+ }
+ boolean changed = false;
+
+ Layout layout = isShowingHint() ? mHintLayout : mLayout;
+
+ if (layout == null) return changed;
+
+ int line = layout.getLineForOffset(offset);
+
+ int grav;
+
+ switch (layout.getParagraphAlignment(line)) {
+ case ALIGN_LEFT:
+ grav = 1;
+ break;
+ case ALIGN_RIGHT:
+ grav = -1;
+ break;
+ case ALIGN_NORMAL:
+ grav = layout.getParagraphDirection(line);
+ break;
+ case ALIGN_OPPOSITE:
+ grav = -layout.getParagraphDirection(line);
+ break;
+ case ALIGN_CENTER:
+ default:
+ grav = 0;
+ break;
+ }
+
+ // We only want to clamp the cursor to fit within the layout width
+ // in left-to-right modes, because in a right to left alignment,
+ // we want to scroll to keep the line-right on the screen, as other
+ // lines are likely to have text flush with the right margin, which
+ // we want to keep visible.
+ // A better long-term solution would probably be to measure both
+ // the full line and a blank-trimmed version, and, for example, use
+ // the latter measurement for centering and right alignment, but for
+ // the time being we only implement the cursor clamping in left to
+ // right where it is most likely to be annoying.
+ final boolean clamped = grav > 0;
+ // FIXME: Is it okay to truncate this, or should we round?
+ final int x = (int) layout.getPrimaryHorizontal(offset, clamped);
+ final int top = layout.getLineTop(line);
+ final int bottom = layout.getLineTop(line + 1);
+
+ int left = (int) Math.floor(layout.getLineLeft(line));
+ int right = (int) Math.ceil(layout.getLineRight(line));
+ int ht = layout.getHeight();
+
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+ if (!mHorizontallyScrolling && right - left > hspace && right > x) {
+ // If cursor has been clamped, make sure we don't scroll.
+ right = Math.max(x, left + hspace);
+ }
+
+ 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);
+ awakenScrollBars(mScroller.getDuration());
+ 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.
+
+ // The offsets here are to ensure the rectangle we are using is
+ // within our view bounds, in case the cursor is on the far left
+ // or right. If it isn't withing the bounds, then this request
+ // will be ignored.
+ if (mTempRect == null) mTempRect = new Rect();
+ mTempRect.set(x - 2, top, x + 2, bottom);
+ getInterestingRect(mTempRect, line);
+ mTempRect.offset(mScrollX, mScrollY);
+
+ if (requestRectangleOnScreen(mTempRect)) {
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Move the cursor, if needed, so that it is at an offset that is visible
+ * to the user. This will not move the cursor if it represents more than
+ * one character (a selection range). This will only work if the
+ * TextView contains spannable text; otherwise it will do nothing.
+ *
+ * @return True if the cursor was actually moved, false otherwise.
+ */
+ public boolean moveCursorToVisibleOffset() {
+ if (!(mText instanceof Spannable)) {
+ return false;
+ }
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start != end) {
+ return false;
+ }
+
+ // First: make sure the line is visible on screen:
+
+ int line = mLayout.getLineForOffset(start);
+
+ final int top = mLayout.getLineTop(line);
+ final int bottom = mLayout.getLineTop(line + 1);
+ final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+ int vslack = (bottom - top) / 2;
+ if (vslack > vspace / 4) {
+ vslack = vspace / 4;
+ }
+ final int vs = mScrollY;
+
+ if (top < (vs + vslack)) {
+ line = mLayout.getLineForVertical(vs + vslack + (bottom - top));
+ } else if (bottom > (vspace + vs - vslack)) {
+ line = mLayout.getLineForVertical(vspace + vs - vslack - (bottom - top));
+ }
+
+ // Next: make sure the character is visible on screen:
+
+ final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ final int hs = mScrollX;
+ final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
+ final int rightChar = mLayout.getOffsetForHorizontal(line, hspace + hs);
+
+ // line might contain bidirectional text
+ final int lowChar = leftChar < rightChar ? leftChar : rightChar;
+ final int highChar = leftChar > rightChar ? leftChar : rightChar;
+
+ int newStart = start;
+ if (newStart < lowChar) {
+ newStart = lowChar;
+ } else if (newStart > highChar) {
+ newStart = highChar;
+ }
+
+ if (newStart != start) {
+ Selection.setSelection((Spannable) mText, newStart);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller != null) {
+ if (mScroller.computeScrollOffset()) {
+ mScrollX = mScroller.getCurrX();
+ mScrollY = mScroller.getCurrY();
+ invalidateParentCaches();
+ postInvalidate(); // So we draw again
+ }
+ }
+ }
+
+ private void getInterestingRect(Rect r, int line) {
+ convertFromViewportToContentCoordinates(r);
+
+ // Rectangle can can be expanded on first and last line to take
+ // padding into account.
+ // TODO Take left/right padding into account too?
+ if (line == 0) r.top -= getExtendedPaddingTop();
+ if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
+ }
+
+ private void convertFromViewportToContentCoordinates(Rect r) {
+ final int horizontalOffset = viewportToContentHorizontalOffset();
+ r.left += horizontalOffset;
+ r.right += horizontalOffset;
+
+ final int verticalOffset = viewportToContentVerticalOffset();
+ r.top += verticalOffset;
+ r.bottom += verticalOffset;
+ }
+
+ int viewportToContentHorizontalOffset() {
+ return getCompoundPaddingLeft() - mScrollX;
+ }
+
+ int viewportToContentVerticalOffset() {
+ int offset = getExtendedPaddingTop() - mScrollY;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ offset += getVerticalOffset(false);
+ }
+ return offset;
+ }
+
+ @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}.
+ */
+ @ViewDebug.ExportedProperty(category = "text")
+ public int getSelectionStart() {
+ return Selection.getSelectionStart(getText());
+ }
+
+ /**
+ * Convenience for {@link Selection#getSelectionEnd}.
+ */
+ @ViewDebug.ExportedProperty(category = "text")
+ public int getSelectionEnd() {
+ return Selection.getSelectionEnd(getText());
+ }
+
+ /**
+ * Return true iff there is a selection inside this text view.
+ */
+ public boolean hasSelection() {
+ final int selectionStart = getSelectionStart();
+ final int selectionEnd = getSelectionEnd();
+
+ return selectionStart >= 0 && selectionStart != selectionEnd;
+ }
+
+ String getSelectedText() {
+ if (!hasSelection()) {
+ return null;
+ }
+
+ final int start = getSelectionStart();
+ final int end = getSelectionEnd();
+ return String.valueOf(
+ start > end ? mText.subSequence(end, start) : mText.subSequence(start, end));
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Sets the properties of this field to transform input to ALL CAPS
+ * display. This may use a "small caps" formatting if available.
+ * This setting will be ignored if this field is editable or selectable.
+ *
+ * This call replaces the current transformation method. Disabling this
+ * will not necessarily restore the previous behavior from before this
+ * was enabled.
+ *
+ * @see #setTransformationMethod(TransformationMethod)
+ * @attr ref android.R.styleable#TextView_textAllCaps
+ */
+ public void setAllCaps(boolean allCaps) {
+ if (allCaps) {
+ setTransformationMethod(new AllCapsTransformationMethod(getContext()));
+ } else {
+ setTransformationMethod(null);
+ }
+ }
+
+ /**
+ *
+ * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This
+ * settings is internally ignored if this field is editable or selectable.
+ * @return Whether the current transformation method is for ALL CAPS.
+ *
+ * @see #setAllCaps(boolean)
+ * @see #setTransformationMethod(TransformationMethod)
+ */
+ public boolean isAllCaps() {
+ final TransformationMethod method = getTransformationMethod();
+ return method != null && method instanceof AllCapsTransformationMethod;
+ }
+
+ /**
+ * If true, sets the properties of this field (number of lines, horizontally scrolling,
+ * transformation method) to be for a single-line input; if false, restores these to the default
+ * conditions.
+ *
+ * Note that the default conditions are not necessarily those that were in effect prior this
+ * method, and you may want to reset these properties to your custom values.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ @android.view.RemotableViewMethod
+ public void setSingleLine(boolean singleLine) {
+ // Could be used, but may break backward compatibility.
+ // if (mSingleLine == singleLine) return;
+ setInputTypeSingleLine(singleLine);
+ applySingleLine(singleLine, true, true);
+ }
+
+ /**
+ * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
+ * @param singleLine
+ */
+ private void setInputTypeSingleLine(boolean singleLine) {
+ if (mEditor != null
+ && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ if (singleLine) {
+ mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ } else {
+ mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ }
+ }
+
+ private void applySingleLine(boolean singleLine, boolean applyTransformation,
+ boolean changeMaxLines) {
+ mSingleLine = singleLine;
+ if (singleLine) {
+ setLines(1);
+ setHorizontallyScrolling(true);
+ if (applyTransformation) {
+ setTransformationMethod(SingleLineTransformationMethod.getInstance());
+ }
+ } else {
+ if (changeMaxLines) {
+ setMaxLines(Integer.MAX_VALUE);
+ }
+ setHorizontallyScrolling(false);
+ if (applyTransformation) {
+ setTransformationMethod(null);
+ }
+ }
+ }
+
+ /**
+ * Causes words in the text that are longer than the view's width
+ * to be ellipsized instead of broken in the middle. You may also
+ * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
+ * to constrain the text to a single line. Use <code>null</code>
+ * to turn off ellipsizing.
+ *
+ * If {@link #setMaxLines} has been used to set two or more lines,
+ * only {@link android.text.TextUtils.TruncateAt#END} and
+ * {@link android.text.TextUtils.TruncateAt#MARQUEE} are supported
+ * (other ellipsizing types will not do anything).
+ *
+ * @attr ref android.R.styleable#TextView_ellipsize
+ */
+ public void setEllipsize(TextUtils.TruncateAt where) {
+ // TruncateAt is an enum. != comparison is ok between these singleton objects.
+ if (mEllipsize != where) {
+ mEllipsize = where;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Sets how many times to repeat the marquee animation. Only applied if the
+ * TextView has marquee enabled. Set to -1 to repeat indefinitely.
+ *
+ * @see #getMarqueeRepeatLimit()
+ *
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+ public void setMarqueeRepeatLimit(int marqueeLimit) {
+ mMarqueeRepeatLimit = marqueeLimit;
+ }
+
+ /**
+ * Gets the number of times the marquee animation is repeated. Only meaningful if the
+ * TextView has marquee enabled.
+ *
+ * @return the number of times the marquee animation is repeated. -1 if the animation
+ * repeats indefinitely
+ *
+ * @see #setMarqueeRepeatLimit(int)
+ *
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+ public int getMarqueeRepeatLimit() {
+ return mMarqueeRepeatLimit;
+ }
+
+ /**
+ * Returns where, if anywhere, words that are longer than the view
+ * is wide should be ellipsized.
+ */
+ @ViewDebug.ExportedProperty
+ 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
+ */
+ @android.view.RemotableViewMethod
+ public void setSelectAllOnFocus(boolean selectAllOnFocus) {
+ createEditorIfNeeded();
+ mEditor.mSelectAllOnFocus = selectAllOnFocus;
+
+ if (selectAllOnFocus && !(mText instanceof Spannable)) {
+ setText(mText, BufferType.SPANNABLE);
+ }
+ }
+
+ /**
+ * Set whether the cursor is visible. The default is true. Note that this property only
+ * makes sense for editable TextView.
+ *
+ * @see #isCursorVisible()
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ @android.view.RemotableViewMethod
+ public void setCursorVisible(boolean visible) {
+ if (visible && mEditor == null) return; // visible is the default value with no edit data
+ createEditorIfNeeded();
+ if (mEditor.mCursorVisible != visible) {
+ mEditor.mCursorVisible = visible;
+ invalidate();
+
+ mEditor.makeBlink();
+
+ // InsertionPointCursorController depends on mCursorVisible
+ mEditor.prepareCursorControllers();
+ }
+ }
+
+ /**
+ * @return whether or not the cursor is visible (assuming this TextView is editable)
+ *
+ * @see #setCursorVisible(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ public boolean isCursorVisible() {
+ // true is the default value
+ return mEditor == null ? true : mEditor.mCursorVisible;
+ }
+
+ private boolean canMarquee() {
+ int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ return width > 0 && (mLayout.getLineWidth(0) > width
+ || (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null
+ && mSavedMarqueeModeLayout.getLineWidth(0) > width));
+ }
+
+ private void startMarquee() {
+ // Do not ellipsize EditText
+ if (getKeyListener() != null) return;
+
+ if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
+ return;
+ }
+
+ if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
+ && getLineCount() == 1 && canMarquee()) {
+
+ if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
+ mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
+ final Layout tmp = mLayout;
+ mLayout = mSavedMarqueeModeLayout;
+ mSavedMarqueeModeLayout = tmp;
+ setHorizontalFadingEdgeEnabled(true);
+ requestLayout();
+ invalidate();
+ }
+
+ if (mMarquee == null) mMarquee = new Marquee(this);
+ mMarquee.start(mMarqueeRepeatLimit);
+ }
+ }
+
+ private void stopMarquee() {
+ if (mMarquee != null && !mMarquee.isStopped()) {
+ mMarquee.stop();
+ }
+
+ if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
+ mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+ final Layout tmp = mSavedMarqueeModeLayout;
+ mSavedMarqueeModeLayout = mLayout;
+ mLayout = tmp;
+ setHorizontalFadingEdgeEnabled(false);
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ private void startStopMarquee(boolean start) {
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (start) {
+ startMarquee();
+ } else {
+ stopMarquee();
+ }
+ }
+ }
+
+ /**
+ * This method is called when the text is changed, in case any subclasses
+ * would like to know.
+ *
+ * Within <code>text</code>, the <code>lengthAfter</code> characters
+ * beginning at <code>start</code> have just replaced old text that had
+ * length <code>lengthBefore</code>. It is an error to attempt to make
+ * changes to <code>text</code> from this callback.
+ *
+ * @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 lengthBefore The length of the former text that has been replaced
+ * @param lengthAfter The length of the replacement modified text
+ */
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ // intentionally empty, template pattern method can be overridden by subclasses
+ }
+
+ /**
+ * This method is called when the selection has changed, in case any
+ * subclasses would like to know.
+ *
+ * @param selStart The new selection start location.
+ * @param selEnd The new selection end location.
+ */
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
+ }
+
+ /**
+ * Adds a TextWatcher to the list of those whose methods are called
+ * whenever this TextView's text changes.
+ * <p>
+ * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
+ * not called after {@link #setText} calls. Now, doing {@link #setText}
+ * if there are any text changed listeners forces the buffer type to
+ * Editable if it would not otherwise be and does call this method.
+ */
+ 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);
+ }
+ }
+
+ // The spans that are inside or intersect the modified region no longer make sense
+ removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
+ removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
+ }
+
+ // Removes all spans that are inside or actually overlap the start..end range
+ private <T> void removeIntersectingNonAdjacentSpans(int start, int end, Class<T> type) {
+ if (!(mText instanceof Editable)) return;
+ Editable text = (Editable) mText;
+
+ T[] spans = text.getSpans(start, end, type);
+ final int length = spans.length;
+ for (int i = 0; i < length; i++) {
+ final int spanStart = text.getSpanStart(spans[i]);
+ final int spanEnd = text.getSpanEnd(spans[i]);
+ if (spanEnd == start || spanStart == end) break;
+ text.removeSpan(spans[i]);
+ }
+ }
+
+ void removeAdjacentSuggestionSpans(final int pos) {
+ if (!(mText instanceof Editable)) return;
+ final Editable text = (Editable) mText;
+
+ final SuggestionSpan[] spans = text.getSpans(pos, pos, SuggestionSpan.class);
+ final int length = spans.length;
+ for (int i = 0; i < length; i++) {
+ final int spanStart = text.getSpanStart(spans[i]);
+ final int spanEnd = text.getSpanEnd(spans[i]);
+ if (spanEnd == pos || spanStart == pos) {
+ if (SpellChecker.haveWordBoundariesChanged(text, pos, pos, spanStart, spanEnd)) {
+ text.removeSpan(spans[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ 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);
+ }
+ }
+
+ if (mEditor != null) mEditor.sendOnTextChanged(start, before, after);
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ 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);
+ }
+ }
+
+ // Always notify AutoFillManager - it will return right away if autofill is disabled.
+ notifyAutoFillManagerAfterTextChangedIfNeeded();
+
+ hideErrorIfUnchanged();
+ }
+
+ private void notifyAutoFillManagerAfterTextChangedIfNeeded() {
+ // It is important to not check whether the view is important for autofill
+ // since the user can trigger autofill manually on not important views.
+ if (!isAutofillable()) {
+ return;
+ }
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ if (DEBUG_AUTOFILL) {
+ Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText);
+ }
+ afm.notifyValueChanged(TextView.this);
+ }
+ }
+
+ private boolean isAutofillable() {
+ // It is important to not check whether the view is important for autofill
+ // since the user can trigger autofill manually on not important views.
+ return getAutofillType() != AUTOFILL_TYPE_NONE;
+ }
+
+ void updateAfterEdit() {
+ invalidate();
+ int curs = getSelectionStart();
+
+ if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ registerForPreDraw();
+ }
+
+ checkForResize();
+
+ if (curs >= 0) {
+ mHighlightPathBogus = true;
+ if (mEditor != null) mEditor.makeBlink();
+ bringPointIntoView(curs);
+ }
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void handleTextChanged(CharSequence buffer, int start, int before, int after) {
+ sLastCutCopyOrTextChangedTime = 0;
+
+ final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ updateAfterEdit();
+ }
+ if (ims != null) {
+ ims.mContentChanged = true;
+ if (ims.mChangedStart < 0) {
+ ims.mChangedStart = start;
+ ims.mChangedEnd = start + before;
+ } else {
+ ims.mChangedStart = Math.min(ims.mChangedStart, start);
+ ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
+ }
+ ims.mChangedDelta += after - before;
+ }
+ resetErrorChangedFlag();
+ sendOnTextChanged(buffer, start, before, after);
+ onTextChanged(buffer, start, before, after);
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
+ // XXX Make the start and end move together if this ends up
+ // spending too much time invalidating.
+
+ boolean selChanged = false;
+ int newSelStart = -1, newSelEnd = -1;
+
+ final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
+
+ if (what == Selection.SELECTION_END) {
+ selChanged = true;
+ newSelEnd = newStart;
+
+ if (oldStart >= 0 || newStart >= 0) {
+ invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
+ checkForResize();
+ registerForPreDraw();
+ if (mEditor != null) mEditor.makeBlink();
+ }
+ }
+
+ if (what == Selection.SELECTION_START) {
+ selChanged = true;
+ newSelStart = newStart;
+
+ if (oldStart >= 0 || newStart >= 0) {
+ int end = Selection.getSelectionEnd(buf);
+ invalidateCursor(end, oldStart, newStart);
+ }
+ }
+
+ if (selChanged) {
+ mHighlightPathBogus = true;
+ if (mEditor != null && !isFocused()) mEditor.mSelectionMoved = true;
+
+ if ((buf.getSpanFlags(what) & Spanned.SPAN_INTERMEDIATE) == 0) {
+ if (newSelStart < 0) {
+ newSelStart = Selection.getSelectionStart(buf);
+ }
+ if (newSelEnd < 0) {
+ newSelEnd = Selection.getSelectionEnd(buf);
+ }
+
+ if (mEditor != null) {
+ mEditor.refreshTextActionMode();
+ if (!hasSelection()
+ && mEditor.getTextActionMode() == null && hasTransientState()) {
+ // User generated selection has been removed.
+ setHasTransientState(false);
+ }
+ }
+ onSelectionChanged(newSelStart, newSelEnd);
+ }
+ }
+
+ if (what instanceof UpdateAppearance || what instanceof ParagraphStyle
+ || what instanceof CharacterStyle) {
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ invalidate();
+ mHighlightPathBogus = true;
+ checkForResize();
+ } else {
+ ims.mContentChanged = true;
+ }
+ if (mEditor != null) {
+ if (oldStart >= 0) mEditor.invalidateTextDisplayList(mLayout, oldStart, oldEnd);
+ if (newStart >= 0) mEditor.invalidateTextDisplayList(mLayout, newStart, newEnd);
+ mEditor.invalidateHandlesAndActionMode();
+ }
+ }
+
+ if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
+ mHighlightPathBogus = true;
+ if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
+ ims.mSelectionModeChanged = true;
+ }
+
+ if (Selection.getSelectionStart(buf) >= 0) {
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ invalidateCursor();
+ } else {
+ ims.mCursorChanged = true;
+ }
+ }
+ }
+
+ if (what instanceof ParcelableSpan) {
+ // If this is a span that can be sent to a remote process,
+ // the current extract editor would be interested in it.
+ if (ims != null && ims.mExtractedTextRequest != null) {
+ if (ims.mBatchEditNesting != 0) {
+ if (oldStart >= 0) {
+ if (ims.mChangedStart > oldStart) {
+ ims.mChangedStart = oldStart;
+ }
+ if (ims.mChangedStart > oldEnd) {
+ ims.mChangedStart = oldEnd;
+ }
+ }
+ if (newStart >= 0) {
+ if (ims.mChangedStart > newStart) {
+ ims.mChangedStart = newStart;
+ }
+ if (ims.mChangedStart > newEnd) {
+ ims.mChangedStart = newEnd;
+ }
+ }
+ } else {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "Span change outside of batch: "
+ + oldStart + "-" + oldEnd + ","
+ + newStart + "-" + newEnd + " " + what);
+ }
+ ims.mContentChanged = true;
+ }
+ }
+ }
+
+ if (mEditor != null && mEditor.mSpellChecker != null && newStart < 0
+ && what instanceof SpellCheckSpan) {
+ mEditor.mSpellChecker.onSpellCheckSpanRemoved((SpellCheckSpan) what);
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ if (isTemporarilyDetached()) {
+ // If we are temporarily in the detach state, then do nothing.
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ return;
+ }
+
+ if (mEditor != null) mEditor.onFocusChanged(focused, direction);
+
+ if (focused) {
+ if (mText instanceof Spannable) {
+ Spannable sp = (Spannable) mText;
+ MetaKeyKeyListener.resetMetaState(sp);
+ }
+ }
+
+ startStopMarquee(focused);
+
+ if (mTransformation != null) {
+ mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
+ }
+
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ if (mEditor != null) mEditor.onWindowFocusChanged(hasWindowFocus);
+
+ startStopMarquee(hasWindowFocus);
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (mEditor != null && visibility != VISIBLE) {
+ mEditor.hideCursorAndSpanControllers();
+ stopTextActionMode();
+ }
+ }
+
+ /**
+ * Use {@link BaseInputConnection#removeComposingSpans
+ * BaseInputConnection.removeComposingSpans()} to remove any IME composing
+ * state from this text view.
+ */
+ public void clearComposingText() {
+ if (mText instanceof Spannable) {
+ BaseInputConnection.removeComposingSpans((Spannable) mText);
+ }
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ boolean wasSelected = isSelected();
+
+ super.setSelected(selected);
+
+ if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (selected) {
+ startMarquee();
+ } else {
+ stopMarquee();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (mEditor != null) {
+ mEditor.onTouchEvent(event);
+
+ if (mEditor.mSelectionModifierCursorController != null
+ && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
+ return true;
+ }
+ }
+
+ 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 the long press should have triggered an
+ * insertion action mode, we can now actually show it.
+ */
+ if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
+ mEditor.mDiscardNextActionUp = false;
+
+ if (mEditor.mIsInsertionActionModeStartPending) {
+ mEditor.startInsertionActionMode();
+ mEditor.mIsInsertionActionModeStartPending = false;
+ }
+ return superResult;
+ }
+
+ final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
+ && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
+
+ if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
+ && mText instanceof Spannable && mLayout != null) {
+ boolean handled = false;
+
+ if (mMovement != null) {
+ handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
+ }
+
+ final boolean textIsSelectable = isTextSelectable();
+ if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
+ // The LinkMovementMethod which should handle taps on links has not been installed
+ // on non editable text that support text selection.
+ // We reproduce its behavior here to open links for these.
+ ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
+ getSelectionEnd(), ClickableSpan.class);
+
+ if (links.length > 0) {
+ links[0].onClick(this);
+ handled = true;
+ }
+ }
+
+ if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
+ // Show the IME, except when selecting in read-only text.
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ viewClicked(imm);
+ if (isTextEditable() && mEditor.mShowSoftInputOnFocus && imm != null) {
+ imm.showSoftInput(this, 0);
+ }
+
+ // The above condition ensures that the mEditor is not null
+ mEditor.onTouchUpEvent(event);
+
+ handled = true;
+ }
+
+ if (handled) {
+ return true;
+ }
+ }
+
+ return superResult;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (mMovement != null && mText instanceof Spannable && mLayout != null) {
+ try {
+ if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
+ return true;
+ }
+ } catch (AbstractMethodError ex) {
+ // onGenericMotionEvent was added to the MovementMethod interface in API 12.
+ // Ignore its absence in case third party applications implemented the
+ // interface directly.
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ protected void onCreateContextMenu(ContextMenu menu) {
+ if (mEditor != null) {
+ mEditor.onCreateContextMenu(menu);
+ }
+ }
+
+ @Override
+ public boolean showContextMenu() {
+ if (mEditor != null) {
+ mEditor.setContextMenuAnchor(Float.NaN, Float.NaN);
+ }
+ return super.showContextMenu();
+ }
+
+ @Override
+ public boolean showContextMenu(float x, float y) {
+ if (mEditor != null) {
+ mEditor.setContextMenuAnchor(x, y);
+ }
+ return super.showContextMenu(x, y);
+ }
+
+ /**
+ * @return True iff this TextView contains a text that can be edited, or if this is
+ * a selectable TextView.
+ */
+ boolean isTextEditable() {
+ return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
+ }
+
+ /**
+ * Returns true, only while processing a touch gesture, if the initial
+ * touch down event caused focus to move to the text view and as a result
+ * its selection changed. Only valid while processing the touch gesture
+ * of interest, in an editable text view.
+ */
+ public boolean didTouchFocusSelect() {
+ return mEditor != null && mEditor.mTouchFocusSelected;
+ }
+
+ @Override
+ public void cancelLongPress() {
+ super.cancelLongPress();
+ if (mEditor != null) mEditor.mIgnoreActionUpEvent = true;
+ }
+
+ @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);
+ }
+
+ /**
+ * Sets the Scroller used for producing a scrolling animation
+ *
+ * @param s A Scroller instance
+ */
+ public void setScroller(Scroller s) {
+ mScroller = s;
+ }
+
+ @Override
+ protected float getLeftFadingEdgeStrength() {
+ if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
+ final Marquee marquee = mMarquee;
+ if (marquee.shouldDrawLeftFade()) {
+ return getHorizontalFadingEdgeStrength(marquee.getScroll(), 0.0f);
+ } else {
+ return 0.0f;
+ }
+ } else if (getLineCount() == 1) {
+ final float lineLeft = getLayout().getLineLeft(0);
+ if (lineLeft > mScrollX) return 0.0f;
+ return getHorizontalFadingEdgeStrength(mScrollX, lineLeft);
+ }
+ return super.getLeftFadingEdgeStrength();
+ }
+
+ @Override
+ protected float getRightFadingEdgeStrength() {
+ if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
+ final Marquee marquee = mMarquee;
+ return getHorizontalFadingEdgeStrength(marquee.getMaxFadeScroll(), marquee.getScroll());
+ } else if (getLineCount() == 1) {
+ final float rightEdge = mScrollX +
+ (getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight());
+ final float lineRight = getLayout().getLineRight(0);
+ if (lineRight < rightEdge) return 0.0f;
+ return getHorizontalFadingEdgeStrength(rightEdge, lineRight);
+ }
+ return super.getRightFadingEdgeStrength();
+ }
+
+ /**
+ * Calculates the fading edge strength as the ratio of the distance between two
+ * horizontal positions to {@link View#getHorizontalFadingEdgeLength()}. Uses the absolute
+ * value for the distance calculation.
+ *
+ * @param position1 A horizontal position.
+ * @param position2 A horizontal position.
+ * @return Fading edge strength between [0.0f, 1.0f].
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ private float getHorizontalFadingEdgeStrength(float position1, float position2) {
+ final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
+ if (horizontalFadingEdgeLength == 0) return 0.0f;
+ final float diff = Math.abs(position1 - position2);
+ if (diff > horizontalFadingEdgeLength) return 1.0f;
+ return diff / horizontalFadingEdgeLength;
+ }
+
+ private boolean isMarqueeFadeEnabled() {
+ return mEllipsize == TextUtils.TruncateAt.MARQUEE
+ && mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ if (mLayout != null) {
+ return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT
+ ? (int) mLayout.getLineWidth(0) : mLayout.getWidth();
+ }
+
+ return super.computeHorizontalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ if (mLayout != null) {
+ return mLayout.getHeight();
+ }
+ return super.computeVerticalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
+ }
+
+ @Override
+ public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
+ super.findViewsWithText(outViews, searched, flags);
+ if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
+ && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
+ String searchedLowerCase = searched.toString().toLowerCase();
+ String textLowerCase = mText.toString().toLowerCase();
+ if (textLowerCase.contains(searchedLowerCase)) {
+ outViews.add(this);
+ }
+ }
+ }
+
+ /**
+ * Type of the text buffer that defines the characteristics of the text such as static,
+ * styleable, or editable.
+ */
+ public enum BufferType {
+ NORMAL, SPANNABLE, EDITABLE
+ }
+
+ /**
+ * Returns the TextView_textColor attribute from the TypedArray, if set, or
+ * the TextAppearance_textColor from the TextView_textAppearance attribute,
+ * if TextView_textColor was not set directly.
+ *
+ * @removed
+ */
+ public static ColorStateList getTextColors(Context context, TypedArray attrs) {
+ if (attrs == null) {
+ // Preserve behavior prior to removal of this API.
+ throw new NullPointerException();
+ }
+
+ // It's not safe to use this method from apps. The parameter 'attrs'
+ // must have been obtained using the TextView filter array which is not
+ // available to the SDK. As such, we grab a default TypedArray with the
+ // right filter instead here.
+ final TypedArray a = context.obtainStyledAttributes(R.styleable.TextView);
+ ColorStateList colors = a.getColorStateList(R.styleable.TextView_textColor);
+ if (colors == null) {
+ final int ap = a.getResourceId(R.styleable.TextView_textAppearance, 0);
+ if (ap != 0) {
+ final TypedArray appearance = context.obtainStyledAttributes(
+ ap, R.styleable.TextAppearance);
+ colors = appearance.getColorStateList(R.styleable.TextAppearance_textColor);
+ appearance.recycle();
+ }
+ }
+ a.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.
+ *
+ * @removed
+ */
+ public static int getTextColor(Context context, TypedArray attrs, int def) {
+ final ColorStateList colors = getTextColors(context, attrs);
+ if (colors == null) {
+ return def;
+ } else {
+ return colors.getDefaultColor();
+ }
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
+ // Handle Ctrl-only shortcuts.
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_A:
+ if (canSelectText()) {
+ return onTextContextMenuItem(ID_SELECT_ALL);
+ }
+ break;
+ case KeyEvent.KEYCODE_Z:
+ if (canUndo()) {
+ return onTextContextMenuItem(ID_UNDO);
+ }
+ break;
+ case KeyEvent.KEYCODE_X:
+ if (canCut()) {
+ return onTextContextMenuItem(ID_CUT);
+ }
+ break;
+ case KeyEvent.KEYCODE_C:
+ if (canCopy()) {
+ return onTextContextMenuItem(ID_COPY);
+ }
+ break;
+ case KeyEvent.KEYCODE_V:
+ if (canPaste()) {
+ return onTextContextMenuItem(ID_PASTE);
+ }
+ break;
+ }
+ } else if (event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
+ // Handle Ctrl-Shift shortcuts.
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_Z:
+ if (canRedo()) {
+ return onTextContextMenuItem(ID_REDO);
+ }
+ break;
+ case KeyEvent.KEYCODE_V:
+ if (canPaste()) {
+ return onTextContextMenuItem(ID_PASTE_AS_PLAIN_TEXT);
+ }
+ }
+ }
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ /**
+ * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
+ * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
+ * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not
+ * sufficient.
+ */
+ boolean canSelectText() {
+ return mText.length() != 0 && mEditor != null && mEditor.hasSelectionController();
+ }
+
+ /**
+ * Test based on the <i>intrinsic</i> charateristics of the TextView.
+ * The text must be spannable and the movement method must allow for arbitary selection.
+ *
+ * See also {@link #canSelectText()}.
+ */
+ boolean textCanBeSelected() {
+ // prepareCursorController() relies on this method.
+ // If you change this condition, make sure prepareCursorController is called anywhere
+ // the value of this condition might be changed.
+ if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
+ return isTextEditable()
+ || (isTextSelectable() && mText instanceof Spannable && isEnabled());
+ }
+
+ private Locale getTextServicesLocale(boolean allowNullLocale) {
+ // Start fetching the text services locale asynchronously.
+ updateTextServicesLocaleAsync();
+ // If !allowNullLocale and there is no cached text services locale, just return the default
+ // locale.
+ return (mCurrentSpellCheckerLocaleCache == null && !allowNullLocale) ? Locale.getDefault()
+ : mCurrentSpellCheckerLocaleCache;
+ }
+
+ /**
+ * This is a temporary method. Future versions may support multi-locale text.
+ * Caveat: This method may not return the latest text services locale, but this should be
+ * acceptable and it's more important to make this method asynchronous.
+ *
+ * @return The locale that should be used for a word iterator
+ * in this TextView, based on the current spell checker settings,
+ * the current IME's locale, or the system default locale.
+ * Please note that a word iterator in this TextView is different from another word iterator
+ * used by SpellChecker.java of TextView. This method should be used for the former.
+ * @hide
+ */
+ // TODO: Support multi-locale
+ // TODO: Update the text services locale immediately after the keyboard locale is switched
+ // by catching intent of keyboard switch event
+ public Locale getTextServicesLocale() {
+ return getTextServicesLocale(false /* allowNullLocale */);
+ }
+
+ /**
+ * @return {@code true} if this TextView is specialized for showing and interacting with the
+ * extracted text in a full-screen input method.
+ * @hide
+ */
+ public boolean isInExtractedMode() {
+ return false;
+ }
+
+ /**
+ * @return {@code true} if this widget supports auto-sizing text and has been configured to
+ * auto-size.
+ */
+ private boolean isAutoSizeEnabled() {
+ return supportsAutoSizeText() && mAutoSizeTextType != AUTO_SIZE_TEXT_TYPE_NONE;
+ }
+
+ /**
+ * @return {@code true} if this TextView supports auto-sizing text to fit within its container.
+ * @hide
+ */
+ protected boolean supportsAutoSizeText() {
+ return true;
+ }
+
+ /**
+ * This is a temporary method. Future versions may support multi-locale text.
+ * Caveat: This method may not return the latest spell checker locale, but this should be
+ * acceptable and it's more important to make this method asynchronous.
+ *
+ * @return The locale that should be used for a spell checker in this TextView,
+ * based on the current spell checker settings, the current IME's locale, or the system default
+ * locale.
+ * @hide
+ */
+ public Locale getSpellCheckerLocale() {
+ return getTextServicesLocale(true /* allowNullLocale */);
+ }
+
+ private void updateTextServicesLocaleAsync() {
+ // AsyncTask.execute() uses a serial executor which means we don't have
+ // to lock around updateTextServicesLocaleLocked() to prevent it from
+ // being executed n times in parallel.
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ updateTextServicesLocaleLocked();
+ }
+ });
+ }
+
+ private void updateTextServicesLocaleLocked() {
+ final TextServicesManager textServicesManager = (TextServicesManager)
+ mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
+ final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true);
+ final Locale locale;
+ if (subtype != null) {
+ locale = subtype.getLocaleObject();
+ } else {
+ locale = null;
+ }
+ mCurrentSpellCheckerLocaleCache = locale;
+ }
+
+ void onLocaleChanged() {
+ mEditor.onLocaleChanged();
+ }
+
+ /**
+ * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
+ * Made available to achieve a consistent behavior.
+ * @hide
+ */
+ public WordIterator getWordIterator() {
+ if (mEditor != null) {
+ return mEditor.getWordIterator();
+ } else {
+ return null;
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEventInternal(event);
+
+ final CharSequence text = getTextForAccessibility();
+ if (!TextUtils.isEmpty(text)) {
+ event.getText().add(text);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TextView.class.getName();
+ }
+
+ @Override
+ public void onProvideStructure(ViewStructure structure) {
+ super.onProvideStructure(structure);
+ onProvideAutoStructureForAssistOrAutofill(structure, false);
+ }
+
+ @Override
+ public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+ super.onProvideAutofillStructure(structure, flags);
+ onProvideAutoStructureForAssistOrAutofill(structure, true);
+ }
+
+ private void onProvideAutoStructureForAssistOrAutofill(ViewStructure structure,
+ boolean forAutofill) {
+ final boolean isPassword = hasPasswordTransformationMethod()
+ || isPasswordInputType(getInputType());
+ if (forAutofill) {
+ structure.setDataIsSensitive(!mTextFromResource);
+ }
+
+ if (!isPassword || forAutofill) {
+ if (mLayout == null) {
+ assumeLayout();
+ }
+ Layout layout = mLayout;
+ final int lineCount = layout.getLineCount();
+ if (lineCount <= 1) {
+ // Simple case: this is a single line.
+ final CharSequence text = getText();
+ if (forAutofill) {
+ structure.setText(text);
+ } else {
+ structure.setText(text, getSelectionStart(), getSelectionEnd());
+ }
+ } else {
+ // Complex case: multi-line, could be scrolled or within a scroll container
+ // so some lines are not visible.
+ final int[] tmpCords = new int[2];
+ getLocationInWindow(tmpCords);
+ final int topWindowLocation = tmpCords[1];
+ View root = this;
+ ViewParent viewParent = getParent();
+ while (viewParent instanceof View) {
+ root = (View) viewParent;
+ viewParent = root.getParent();
+ }
+ final int windowHeight = root.getHeight();
+ final int topLine;
+ final int bottomLine;
+ if (topWindowLocation >= 0) {
+ // The top of the view is fully within its window; start text at line 0.
+ topLine = getLineAtCoordinateUnclamped(0);
+ bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1);
+ } else {
+ // The top of hte window has scrolled off the top of the window; figure out
+ // the starting line for this.
+ topLine = getLineAtCoordinateUnclamped(-topWindowLocation);
+ bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1 - topWindowLocation);
+ }
+ // We want to return some contextual lines above/below the lines that are
+ // actually visible.
+ int expandedTopLine = topLine - (bottomLine - topLine) / 2;
+ if (expandedTopLine < 0) {
+ expandedTopLine = 0;
+ }
+ int expandedBottomLine = bottomLine + (bottomLine - topLine) / 2;
+ if (expandedBottomLine >= lineCount) {
+ expandedBottomLine = lineCount - 1;
+ }
+
+ // Convert lines into character offsets.
+ int expandedTopChar = layout.getLineStart(expandedTopLine);
+ int expandedBottomChar = layout.getLineEnd(expandedBottomLine);
+
+ // Take into account selection -- if there is a selection, we need to expand
+ // the text we are returning to include that selection.
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+ if (selStart < selEnd) {
+ if (selStart < expandedTopChar) {
+ expandedTopChar = selStart;
+ }
+ if (selEnd > expandedBottomChar) {
+ expandedBottomChar = selEnd;
+ }
+ }
+
+ // Get the text and trim it to the range we are reporting.
+ CharSequence text = getText();
+ if (expandedTopChar > 0 || expandedBottomChar < text.length()) {
+ text = text.subSequence(expandedTopChar, expandedBottomChar);
+ }
+
+ if (forAutofill) {
+ structure.setText(text);
+ } else {
+ structure.setText(text, selStart - expandedTopChar, selEnd - expandedTopChar);
+
+ final int[] lineOffsets = new int[bottomLine - topLine + 1];
+ final int[] lineBaselines = new int[bottomLine - topLine + 1];
+ final int baselineOffset = getBaselineOffset();
+ for (int i = topLine; i <= bottomLine; i++) {
+ lineOffsets[i - topLine] = layout.getLineStart(i);
+ lineBaselines[i - topLine] = layout.getLineBaseline(i) + baselineOffset;
+ }
+ structure.setTextLines(lineOffsets, lineBaselines);
+ }
+ }
+
+ if (!forAutofill) {
+ // Extract style information that applies to the TextView as a whole.
+ int style = 0;
+ int typefaceStyle = getTypefaceStyle();
+ if ((typefaceStyle & Typeface.BOLD) != 0) {
+ style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
+ }
+ if ((typefaceStyle & Typeface.ITALIC) != 0) {
+ style |= AssistStructure.ViewNode.TEXT_STYLE_ITALIC;
+ }
+
+ // Global styles can also be set via TextView.setPaintFlags().
+ int paintFlags = mTextPaint.getFlags();
+ if ((paintFlags & Paint.FAKE_BOLD_TEXT_FLAG) != 0) {
+ style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
+ }
+ if ((paintFlags & Paint.UNDERLINE_TEXT_FLAG) != 0) {
+ style |= AssistStructure.ViewNode.TEXT_STYLE_UNDERLINE;
+ }
+ if ((paintFlags & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
+ style |= AssistStructure.ViewNode.TEXT_STYLE_STRIKE_THRU;
+ }
+
+ // TextView does not have its own text background color. A background is either part
+ // of the View (and can be any drawable) or a BackgroundColorSpan inside the text.
+ structure.setTextStyle(getTextSize(), getCurrentTextColor(),
+ AssistStructure.ViewNode.TEXT_COLOR_UNDEFINED /* bgColor */, style);
+ }
+ }
+ structure.setHint(getHint());
+ structure.setInputType(getInputType());
+ }
+
+ boolean canRequestAutofill() {
+ if (!isAutofillable()) {
+ return false;
+ }
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ return afm.isEnabled();
+ }
+ return false;
+ }
+
+ private void requestAutofill() {
+ final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.requestAutofill(this);
+ }
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!value.isText() || !isTextEditable()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ final CharSequence autofilledValue = value.getTextValue();
+
+ // First autofill it...
+ setText(autofilledValue, mBufferType, true, 0);
+
+ // ...then move cursor to the end.
+ final CharSequence text = getText();
+ if ((text instanceof Spannable)) {
+ Selection.setSelection((Spannable) text, text.length());
+ }
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isTextEditable() ? AUTOFILL_TYPE_TEXT : AUTOFILL_TYPE_NONE;
+ }
+
+ /**
+ * Gets the {@link TextView}'s current text for AutoFill. The value is trimmed to 100K
+ * {@code char}s if longer.
+ *
+ * @return current text, {@code null} if the text is not editable
+ *
+ * @see View#getAutofillValue()
+ */
+ @Override
+ @Nullable
+ public AutofillValue getAutofillValue() {
+ if (isTextEditable()) {
+ final CharSequence text = TextUtils.trimToParcelableSize(getText());
+ return AutofillValue.forText(text);
+ }
+ return null;
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+
+ final boolean isPassword = hasPasswordTransformationMethod();
+ event.setPassword(isPassword);
+
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
+ event.setFromIndex(Selection.getSelectionStart(mText));
+ event.setToIndex(Selection.getSelectionEnd(mText));
+ event.setItemCount(mText.length());
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ final boolean isPassword = hasPasswordTransformationMethod();
+ info.setPassword(isPassword);
+ info.setText(getTextForAccessibility());
+ info.setHintText(mHint);
+ info.setShowingHintText(isShowingHint());
+
+ if (mBufferType == BufferType.EDITABLE) {
+ info.setEditable(true);
+ if (isEnabled()) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+ }
+ }
+
+ if (mEditor != null) {
+ info.setInputType(mEditor.mInputType);
+
+ if (mEditor.mError != null) {
+ info.setContentInvalid(true);
+ info.setError(mEditor.mError);
+ }
+ }
+
+ if (!TextUtils.isEmpty(mText)) {
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+ info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
+ | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
+ info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+ info.setAvailableExtraData(
+ Arrays.asList(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
+ }
+
+ if (isFocused()) {
+ if (canCopy()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_COPY);
+ }
+ if (canPaste()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_PASTE);
+ }
+ if (canCut()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CUT);
+ }
+ if (canShare()) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ ACCESSIBILITY_ACTION_SHARE,
+ getResources().getString(com.android.internal.R.string.share)));
+ }
+ if (canProcessText()) { // also implies mEditor is not null.
+ mEditor.mProcessTextIntentActionsHandler.onInitializeAccessibilityNodeInfo(info);
+ }
+ }
+
+ // Check for known input filter types.
+ final int numFilters = mFilters.length;
+ for (int i = 0; i < numFilters; i++) {
+ final InputFilter filter = mFilters[i];
+ if (filter instanceof InputFilter.LengthFilter) {
+ info.setMaxTextLength(((InputFilter.LengthFilter) filter).getMax());
+ }
+ }
+
+ if (!isSingleLine()) {
+ info.setMultiLine(true);
+ }
+ }
+
+ @Override
+ public void addExtraDataToAccessibilityNodeInfo(
+ AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) {
+ // The only extra data we support requires arguments.
+ if (arguments == null) {
+ return;
+ }
+ if (extraDataKey.equals(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY)) {
+ int positionInfoStartIndex = arguments.getInt(
+ EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, -1);
+ int positionInfoLength = arguments.getInt(
+ EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, -1);
+ if ((positionInfoLength <= 0) || (positionInfoStartIndex < 0)
+ || (positionInfoStartIndex >= mText.length())) {
+ Log.e(LOG_TAG, "Invalid arguments for accessibility character locations");
+ return;
+ }
+ RectF[] boundingRects = new RectF[positionInfoLength];
+ final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
+ populateCharacterBounds(builder, positionInfoStartIndex,
+ positionInfoStartIndex + positionInfoLength,
+ viewportToContentHorizontalOffset(), viewportToContentVerticalOffset());
+ CursorAnchorInfo cursorAnchorInfo = builder.setMatrix(null).build();
+ for (int i = 0; i < positionInfoLength; i++) {
+ int flags = cursorAnchorInfo.getCharacterBoundsFlags(positionInfoStartIndex + i);
+ if ((flags & FLAG_HAS_VISIBLE_REGION) == FLAG_HAS_VISIBLE_REGION) {
+ RectF bounds = cursorAnchorInfo
+ .getCharacterBounds(positionInfoStartIndex + i);
+ if (bounds != null) {
+ mapRectFromViewToScreenCoords(bounds, true);
+ boundingRects[i] = bounds;
+ }
+ }
+ }
+ info.getExtras().putParcelableArray(extraDataKey, boundingRects);
+ }
+ }
+
+ /**
+ * Populate requested character bounds in a {@link CursorAnchorInfo.Builder}
+ *
+ * @param builder The builder to populate
+ * @param startIndex The starting character index to populate
+ * @param endIndex The ending character index to populate
+ * @param viewportToContentHorizontalOffset The horizontal offset from the viewport to the
+ * content
+ * @param viewportToContentVerticalOffset The vertical offset from the viewport to the content
+ * @hide
+ */
+ public void populateCharacterBounds(CursorAnchorInfo.Builder builder,
+ int startIndex, int endIndex, float viewportToContentHorizontalOffset,
+ float viewportToContentVerticalOffset) {
+ final int minLine = mLayout.getLineForOffset(startIndex);
+ final int maxLine = mLayout.getLineForOffset(endIndex - 1);
+ for (int line = minLine; line <= maxLine; ++line) {
+ final int lineStart = mLayout.getLineStart(line);
+ final int lineEnd = mLayout.getLineEnd(line);
+ final int offsetStart = Math.max(lineStart, startIndex);
+ final int offsetEnd = Math.min(lineEnd, endIndex);
+ final boolean ltrLine =
+ mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
+ final float[] widths = new float[offsetEnd - offsetStart];
+ mLayout.getPaint().getTextWidths(mText, offsetStart, offsetEnd, widths);
+ final float top = mLayout.getLineTop(line);
+ final float bottom = mLayout.getLineBottom(line);
+ for (int offset = offsetStart; offset < offsetEnd; ++offset) {
+ final float charWidth = widths[offset - offsetStart];
+ final boolean isRtl = mLayout.isRtlCharAt(offset);
+ final float primary = mLayout.getPrimaryHorizontal(offset);
+ final float secondary = mLayout.getSecondaryHorizontal(offset);
+ // TODO: This doesn't work perfectly for text with custom styles and
+ // TAB chars.
+ final float left;
+ final float right;
+ if (ltrLine) {
+ if (isRtl) {
+ left = secondary - charWidth;
+ right = secondary;
+ } else {
+ left = primary;
+ right = primary + charWidth;
+ }
+ } else {
+ if (!isRtl) {
+ left = secondary;
+ right = secondary + charWidth;
+ } else {
+ left = primary - charWidth;
+ right = primary;
+ }
+ }
+ // TODO: Check top-right and bottom-left as well.
+ final float localLeft = left + viewportToContentHorizontalOffset;
+ final float localRight = right + viewportToContentHorizontalOffset;
+ final float localTop = top + viewportToContentVerticalOffset;
+ final float localBottom = bottom + viewportToContentVerticalOffset;
+ final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
+ final boolean isBottomRightVisible =
+ isPositionVisible(localRight, localBottom);
+ int characterBoundsFlags = 0;
+ if (isTopLeftVisible || isBottomRightVisible) {
+ characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
+ }
+ if (!isTopLeftVisible || !isBottomRightVisible) {
+ characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
+ }
+ if (isRtl) {
+ characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
+ }
+ // Here offset is the index in Java chars.
+ builder.addCharacterBounds(offset, localLeft, localTop, localRight,
+ localBottom, characterBoundsFlags);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isPositionVisible(final float positionX, final float positionY) {
+ synchronized (TEMP_POSITION) {
+ final float[] position = TEMP_POSITION;
+ position[0] = positionX;
+ position[1] = positionY;
+ View view = this;
+
+ while (view != null) {
+ if (view != this) {
+ // Local scroll is already taken into account in positionX/Y
+ position[0] -= view.getScrollX();
+ position[1] -= view.getScrollY();
+ }
+
+ if (position[0] < 0 || position[1] < 0 || position[0] > view.getWidth()
+ || position[1] > view.getHeight()) {
+ return false;
+ }
+
+ if (!view.getMatrix().isIdentity()) {
+ view.getMatrix().mapPoints(position);
+ }
+
+ position[0] += view.getLeft();
+ position[1] += view.getTop();
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ view = (View) parent;
+ } else {
+ // We've reached the ViewRoot, stop iterating
+ view = null;
+ }
+ }
+ }
+
+ // We've been able to walk up the view hierarchy and the position was never clipped
+ return true;
+ }
+
+ /**
+ * Performs an accessibility action after it has been offered to the
+ * delegate.
+ *
+ * @hide
+ */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (mEditor != null
+ && mEditor.mProcessTextIntentActionsHandler.performAccessibilityAction(action)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLICK: {
+ return performAccessibilityActionClick(arguments);
+ }
+ case AccessibilityNodeInfo.ACTION_COPY: {
+ if (isFocused() && canCopy()) {
+ if (onTextContextMenuItem(ID_COPY)) {
+ return true;
+ }
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_PASTE: {
+ if (isFocused() && canPaste()) {
+ if (onTextContextMenuItem(ID_PASTE)) {
+ return true;
+ }
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_CUT: {
+ if (isFocused() && canCut()) {
+ if (onTextContextMenuItem(ID_CUT)) {
+ return true;
+ }
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
+ ensureIterableTextForAccessibilitySelectable();
+ CharSequence text = getIterableTextForAccessibility();
+ if (text == null) {
+ return false;
+ }
+ final int start = (arguments != null) ? arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1;
+ final int end = (arguments != null) ? arguments.getInt(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1;
+ if ((getSelectionStart() != start || getSelectionEnd() != end)) {
+ // No arguments clears the selection.
+ if (start == end && end == -1) {
+ Selection.removeSelection((Spannable) text);
+ return true;
+ }
+ if (start >= 0 && start <= end && end <= text.length()) {
+ Selection.setSelection((Spannable) text, start, end);
+ // Make sure selection mode is engaged.
+ if (mEditor != null) {
+ mEditor.startSelectionActionModeAsync(false);
+ }
+ return true;
+ }
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
+ case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
+ ensureIterableTextForAccessibilitySelectable();
+ return super.performAccessibilityActionInternal(action, arguments);
+ }
+ case ACCESSIBILITY_ACTION_SHARE: {
+ if (isFocused() && canShare()) {
+ if (onTextContextMenuItem(ID_SHARE)) {
+ return true;
+ }
+ }
+ } return false;
+ case AccessibilityNodeInfo.ACTION_SET_TEXT: {
+ if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) {
+ return false;
+ }
+ CharSequence text = (arguments != null) ? arguments.getCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;
+ setText(text);
+ if (mText != null) {
+ int updatedTextLength = mText.length();
+ if (updatedTextLength > 0) {
+ Selection.setSelection((Spannable) mText, updatedTextLength);
+ }
+ }
+ } return true;
+ default: {
+ return super.performAccessibilityActionInternal(action, arguments);
+ }
+ }
+ }
+
+ private boolean performAccessibilityActionClick(Bundle arguments) {
+ boolean handled = false;
+
+ if (!isEnabled()) {
+ return false;
+ }
+
+ if (isClickable() || isLongClickable()) {
+ // Simulate View.onTouchEvent for an ACTION_UP event
+ if (isFocusable() && !isFocused()) {
+ requestFocus();
+ }
+
+ performClick();
+ handled = true;
+ }
+
+ // Show the IME, except when selecting in read-only text.
+ if ((mMovement != null || onCheckIsTextEditor()) && hasSpannableText() && mLayout != null
+ && (isTextEditable() || isTextSelectable()) && isFocused()) {
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ viewClicked(imm);
+ if (!isTextSelectable() && mEditor.mShowSoftInputOnFocus && imm != null) {
+ handled |= imm.showSoftInput(this, 0);
+ }
+ }
+
+ return handled;
+ }
+
+ private boolean hasSpannableText() {
+ return mText != null && mText instanceof Spannable;
+ }
+
+ /** @hide */
+ @Override
+ public void sendAccessibilityEventInternal(int eventType) {
+ if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED && mEditor != null) {
+ mEditor.mProcessTextIntentActionsHandler.initializeAccessibilityActions();
+ }
+
+ super.sendAccessibilityEventInternal(eventType);
+ }
+
+ @Override
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+ // Do not send scroll events since first they are not interesting for
+ // accessibility and second such events a generated too frequently.
+ // For details see the implementation of bringTextIntoView().
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ return;
+ }
+ super.sendAccessibilityEventUnchecked(event);
+ }
+
+ /**
+ * Returns the text that should be exposed to accessibility services.
+ * <p>
+ * This approximates what is displayed visually. If the user has specified
+ * that accessibility services should speak passwords, this method will
+ * bypass any password transformation method and return unobscured text.
+ *
+ * @return the text that should be exposed to accessibility services, may
+ * be {@code null} if no text is set
+ */
+ @Nullable
+ private CharSequence getTextForAccessibility() {
+ // If the text is empty, we must be showing the hint text.
+ if (TextUtils.isEmpty(mText)) {
+ return mHint;
+ }
+
+ // Otherwise, return whatever text is being displayed.
+ return TextUtils.trimToParcelableSize(mTransformed);
+ }
+
+ void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
+ int fromIndex, int removedCount, int addedCount) {
+ AccessibilityEvent event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+ event.setFromIndex(fromIndex);
+ event.setRemovedCount(removedCount);
+ event.setAddedCount(addedCount);
+ event.setBeforeText(beforeText);
+ sendAccessibilityEventUnchecked(event);
+ }
+
+ /**
+ * Returns whether this text view is a current input method target. The
+ * default implementation just checks with {@link InputMethodManager}.
+ * @return True if the TextView is a current input method target; false otherwise.
+ */
+ public boolean isInputMethodTarget() {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ return imm != null && imm.isActive(this);
+ }
+
+ static final int ID_SELECT_ALL = android.R.id.selectAll;
+ static final int ID_UNDO = android.R.id.undo;
+ static final int ID_REDO = android.R.id.redo;
+ static final int ID_CUT = android.R.id.cut;
+ static final int ID_COPY = android.R.id.copy;
+ static final int ID_PASTE = android.R.id.paste;
+ static final int ID_SHARE = android.R.id.shareText;
+ static final int ID_PASTE_AS_PLAIN_TEXT = android.R.id.pasteAsPlainText;
+ static final int ID_REPLACE = android.R.id.replaceText;
+ static final int ID_ASSIST = android.R.id.textAssist;
+ static final int ID_AUTOFILL = android.R.id.autofill;
+
+ /**
+ * Called when a context menu option for the text view is selected. Currently
+ * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
+ * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
+ *
+ * @return true if the context menu item action was performed.
+ */
+ public boolean onTextContextMenuItem(int id) {
+ int min = 0;
+ int max = mText.length();
+
+ if (isFocused()) {
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+
+ min = Math.max(0, Math.min(selStart, selEnd));
+ max = Math.max(0, Math.max(selStart, selEnd));
+ }
+
+ switch (id) {
+ case ID_SELECT_ALL:
+ final boolean hadSelection = hasSelection();
+ selectAllText();
+ if (mEditor != null && hadSelection) {
+ mEditor.invalidateActionModeAsync();
+ }
+ return true;
+
+ case ID_UNDO:
+ if (mEditor != null) {
+ mEditor.undo();
+ }
+ return true; // Returns true even if nothing was undone.
+
+ case ID_REDO:
+ if (mEditor != null) {
+ mEditor.redo();
+ }
+ return true; // Returns true even if nothing was undone.
+
+ case ID_PASTE:
+ paste(min, max, true /* withFormatting */);
+ return true;
+
+ case ID_PASTE_AS_PLAIN_TEXT:
+ paste(min, max, false /* withFormatting */);
+ return true;
+
+ case ID_CUT:
+ final ClipData cutData = ClipData.newPlainText(null, getTransformedText(min, max));
+ if (setPrimaryClip(cutData)) {
+ deleteText_internal(min, max);
+ } else {
+ Toast.makeText(getContext(),
+ com.android.internal.R.string.failed_to_copy_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+
+ case ID_COPY:
+ final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
+ if (setPrimaryClip(copyData)) {
+ stopTextActionMode();
+ } else {
+ Toast.makeText(getContext(),
+ com.android.internal.R.string.failed_to_copy_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+
+ case ID_REPLACE:
+ if (mEditor != null) {
+ mEditor.replace();
+ }
+ return true;
+
+ case ID_SHARE:
+ shareSelectedText();
+ return true;
+
+ case ID_AUTOFILL:
+ requestAutofill();
+ stopTextActionMode();
+ return true;
+ }
+ return false;
+ }
+
+ CharSequence getTransformedText(int start, int end) {
+ return removeSuggestionSpans(mTransformed.subSequence(start, end));
+ }
+
+ @Override
+ public boolean performLongClick() {
+ boolean handled = false;
+ boolean performedHapticFeedback = false;
+
+ if (mEditor != null) {
+ mEditor.mIsBeingLongClicked = true;
+ }
+
+ if (super.performLongClick()) {
+ handled = true;
+ performedHapticFeedback = true;
+ }
+
+ if (mEditor != null) {
+ handled |= mEditor.performLongClick(handled);
+ mEditor.mIsBeingLongClicked = false;
+ }
+
+ if (handled) {
+ if (!performedHapticFeedback) {
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ if (mEditor != null) mEditor.mDiscardNextActionUp = true;
+ } else {
+ MetricsLogger.action(
+ mContext,
+ MetricsEvent.TEXT_LONGPRESS,
+ TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
+ }
+
+ return handled;
+ }
+
+ @Override
+ protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
+ super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
+ if (mEditor != null) {
+ mEditor.onScrollChanged();
+ }
+ }
+
+ /**
+ * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
+ * by the IME or by the spell checker as the user types. This is done by adding
+ * {@link SuggestionSpan}s to the text.
+ *
+ * When suggestions are enabled (default), this list of suggestions will be displayed when the
+ * user asks for them on these parts of the text. This value depends on the inputType of this
+ * TextView.
+ *
+ * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
+ *
+ * In addition, the type variation must be one of
+ * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
+ * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
+ * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
+ * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
+ * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
+ *
+ * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
+ *
+ * @return true if the suggestions popup window is enabled, based on the inputType.
+ */
+ public boolean isSuggestionsEnabled() {
+ if (mEditor == null) return false;
+ if ((mEditor.mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) {
+ return false;
+ }
+ if ((mEditor.mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
+
+ final int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+ return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+ }
+
+ /**
+ * If provided, this ActionMode.Callback will be used to create the ActionMode when text
+ * selection is initiated in this View.
+ *
+ * <p>The standard implementation populates the menu with a subset of Select All, Cut, Copy,
+ * Paste, Replace and Share actions, depending on what this View supports.
+ *
+ * <p>A custom implementation can add new entries in the default menu in its
+ * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, android.view.Menu)}
+ * method. The default actions can also be removed from the menu using
+ * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
+ * {@link android.R.id#cut}, {@link android.R.id#copy}, {@link android.R.id#paste},
+ * {@link android.R.id#replaceText} or {@link android.R.id#shareText} ids as parameters.
+ *
+ * <p>Returning false from
+ * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, android.view.Menu)}
+ * will prevent the action mode from being started.
+ *
+ * <p>Action click events should be handled by the custom implementation of
+ * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode,
+ * android.view.MenuItem)}.
+ *
+ * <p>Note that text selection mode is not started when a TextView receives focus and the
+ * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
+ * that case, to allow for quick replacement.
+ */
+ public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ createEditorIfNeeded();
+ mEditor.mCustomSelectionActionModeCallback = actionModeCallback;
+ }
+
+ /**
+ * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
+ *
+ * @return The current custom selection callback.
+ */
+ public ActionMode.Callback getCustomSelectionActionModeCallback() {
+ return mEditor == null ? null : mEditor.mCustomSelectionActionModeCallback;
+ }
+
+ /**
+ * If provided, this ActionMode.Callback will be used to create the ActionMode when text
+ * insertion is initiated in this View.
+ * The standard implementation populates the menu with a subset of Select All,
+ * Paste and Replace actions, depending on what this View supports.
+ *
+ * <p>A custom implementation can add new entries in the default menu in its
+ * {@link android.view.ActionMode.Callback#onPrepareActionMode(android.view.ActionMode,
+ * android.view.Menu)} method. The default actions can also be removed from the menu using
+ * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
+ * {@link android.R.id#paste} or {@link android.R.id#replaceText} ids as parameters.</p>
+ *
+ * <p>Returning false from
+ * {@link android.view.ActionMode.Callback#onCreateActionMode(android.view.ActionMode,
+ * android.view.Menu)} will prevent the action mode from being started.</p>
+ *
+ * <p>Action click events should be handled by the custom implementation of
+ * {@link android.view.ActionMode.Callback#onActionItemClicked(android.view.ActionMode,
+ * android.view.MenuItem)}.</p>
+ *
+ * <p>Note that text insertion mode is not started when a TextView receives focus and the
+ * {@link android.R.attr#selectAllOnFocus} flag has been set.</p>
+ */
+ public void setCustomInsertionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ createEditorIfNeeded();
+ mEditor.mCustomInsertionActionModeCallback = actionModeCallback;
+ }
+
+ /**
+ * Retrieves the value set in {@link #setCustomInsertionActionModeCallback}. Default is null.
+ *
+ * @return The current custom insertion callback.
+ */
+ public ActionMode.Callback getCustomInsertionActionModeCallback() {
+ return mEditor == null ? null : mEditor.mCustomInsertionActionModeCallback;
+ }
+
+ /**
+ * Sets the {@link TextClassifier} for this TextView.
+ */
+ public void setTextClassifier(@Nullable TextClassifier textClassifier) {
+ mTextClassifier = textClassifier;
+ }
+
+ /**
+ * Returns the {@link TextClassifier} used by this TextView.
+ * If no TextClassifier has been set, this TextView uses the default set by the
+ * {@link TextClassificationManager}.
+ */
+ @NonNull
+ public TextClassifier getTextClassifier() {
+ if (mTextClassifier == null) {
+ TextClassificationManager tcm =
+ mContext.getSystemService(TextClassificationManager.class);
+ if (tcm != null) {
+ mTextClassifier = tcm.getTextClassifier();
+ } else {
+ mTextClassifier = TextClassifier.NO_OP;
+ }
+ }
+ return mTextClassifier;
+ }
+
+ /**
+ * @hide
+ */
+ protected void stopTextActionMode() {
+ if (mEditor != null) {
+ mEditor.stopTextActionMode();
+ }
+ }
+
+ boolean canUndo() {
+ return mEditor != null && mEditor.canUndo();
+ }
+
+ boolean canRedo() {
+ return mEditor != null && mEditor.canRedo();
+ }
+
+ boolean canCut() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
+ }
+
+ if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null
+ && mEditor.mKeyListener != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canCopy() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
+ }
+
+ if (mText.length() > 0 && hasSelection() && mEditor != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canShare() {
+ if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()) {
+ return false;
+ }
+ return canCopy();
+ }
+
+ boolean isDeviceProvisioned() {
+ if (mDeviceProvisionedState == DEVICE_PROVISIONED_UNKNOWN) {
+ mDeviceProvisionedState = Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0
+ ? DEVICE_PROVISIONED_YES
+ : DEVICE_PROVISIONED_NO;
+ }
+ return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
+ }
+
+ boolean canPaste() {
+ return (mText instanceof Editable
+ && mEditor != null && mEditor.mKeyListener != null
+ && getSelectionStart() >= 0
+ && getSelectionEnd() >= 0
+ && ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
+ .hasPrimaryClip());
+ }
+
+ boolean canPasteAsPlainText() {
+ if (!canPaste()) {
+ return false;
+ }
+
+ final ClipData clipData =
+ ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
+ .getPrimaryClip();
+ final ClipDescription description = clipData.getDescription();
+ final boolean isPlainType = description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
+ final CharSequence text = clipData.getItemAt(0).getText();
+ if (isPlainType && (text instanceof Spanned)) {
+ Spanned spanned = (Spanned) text;
+ if (TextUtils.hasStyleSpan(spanned)) {
+ return true;
+ }
+ }
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+ }
+
+ boolean canProcessText() {
+ if (getId() == View.NO_ID) {
+ return false;
+ }
+ return canShare();
+ }
+
+ boolean canSelectAllText() {
+ return canSelectText() && !hasPasswordTransformationMethod()
+ && !(getSelectionStart() == 0 && getSelectionEnd() == mText.length());
+ }
+
+ boolean selectAllText() {
+ if (mEditor != null) {
+ // Hide the toolbar before changing the selection to avoid flickering.
+ mEditor.hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY);
+ }
+ final int length = mText.length();
+ Selection.setSelection((Spannable) mText, 0, length);
+ return length > 0;
+ }
+
+ void replaceSelectionWithText(CharSequence text) {
+ ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text);
+ }
+
+ /**
+ * Paste clipboard content between min and max positions.
+ */
+ private void paste(int min, int max, boolean withFormatting) {
+ ClipboardManager clipboard =
+ (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = clipboard.getPrimaryClip();
+ if (clip != null) {
+ boolean didFirst = false;
+ for (int i = 0; i < clip.getItemCount(); i++) {
+ final CharSequence paste;
+ if (withFormatting) {
+ paste = clip.getItemAt(i).coerceToStyledText(getContext());
+ } else {
+ // Get an item as text and remove all spans by toString().
+ final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
+ paste = (text instanceof Spanned) ? text.toString() : text;
+ }
+ if (paste != null) {
+ if (!didFirst) {
+ Selection.setSelection((Spannable) mText, max);
+ ((Editable) mText).replace(min, max, paste);
+ didFirst = true;
+ } else {
+ ((Editable) mText).insert(getSelectionEnd(), "\n");
+ ((Editable) mText).insert(getSelectionEnd(), paste);
+ }
+ }
+ }
+ sLastCutCopyOrTextChangedTime = 0;
+ }
+ }
+
+ private void shareSelectedText() {
+ String selectedText = getSelectedText();
+ if (selectedText != null && !selectedText.isEmpty()) {
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+ sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
+ selectedText = TextUtils.trimToParcelableSize(selectedText);
+ sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
+ getContext().startActivity(Intent.createChooser(sharingIntent, null));
+ Selection.setSelection((Spannable) mText, getSelectionEnd());
+ }
+ }
+
+ @CheckResult
+ private boolean setPrimaryClip(ClipData clip) {
+ ClipboardManager clipboard =
+ (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ clipboard.setPrimaryClip(clip);
+ } catch (Throwable t) {
+ return false;
+ }
+ sLastCutCopyOrTextChangedTime = SystemClock.uptimeMillis();
+ return true;
+ }
+
+ /**
+ * Get the character offset closest to the specified absolute position. A typical use case is to
+ * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
+ *
+ * @param x The horizontal absolute position of a point on screen
+ * @param y The vertical absolute position of a point on screen
+ * @return the character offset for the character whose position is closest to the specified
+ * position. Returns -1 if there is no layout.
+ */
+ public int getOffsetForPosition(float x, float y) {
+ if (getLayout() == null) return -1;
+ final int line = getLineAtCoordinate(y);
+ final int offset = getOffsetAtCoordinate(line, x);
+ return offset;
+ }
+
+ float convertToLocalHorizontalCoordinate(float x) {
+ x -= getTotalPaddingLeft();
+ // Clamp the position to inside of the view.
+ x = Math.max(0.0f, x);
+ x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
+ x += getScrollX();
+ return x;
+ }
+
+ int getLineAtCoordinate(float y) {
+ y -= getTotalPaddingTop();
+ // Clamp the position to inside of the view.
+ y = Math.max(0.0f, y);
+ y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
+ y += getScrollY();
+ return getLayout().getLineForVertical((int) y);
+ }
+
+ int getLineAtCoordinateUnclamped(float y) {
+ y -= getTotalPaddingTop();
+ y += getScrollY();
+ return getLayout().getLineForVertical((int) y);
+ }
+
+ int getOffsetAtCoordinate(int line, float x) {
+ x = convertToLocalHorizontalCoordinate(x);
+ return getLayout().getOffsetForHorizontal(line, x);
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ return mEditor != null && mEditor.hasInsertionController();
+
+ case DragEvent.ACTION_DRAG_ENTERED:
+ TextView.this.requestFocus();
+ return true;
+
+ case DragEvent.ACTION_DRAG_LOCATION:
+ if (mText instanceof Spannable) {
+ final int offset = getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection((Spannable) mText, offset);
+ }
+ return true;
+
+ case DragEvent.ACTION_DROP:
+ if (mEditor != null) mEditor.onDrop(event);
+ return true;
+
+ case DragEvent.ACTION_DRAG_ENDED:
+ case DragEvent.ACTION_DRAG_EXITED:
+ default:
+ return true;
+ }
+ }
+
+ boolean isInBatchEditMode() {
+ if (mEditor == null) return false;
+ final Editor.InputMethodState ims = mEditor.mInputMethodState;
+ if (ims != null) {
+ return ims.mBatchEditNesting > 0;
+ }
+ return mEditor.mInBatchEditControllers;
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+
+ final TextDirectionHeuristic newTextDir = getTextDirectionHeuristic();
+ if (mTextDir != newTextDir) {
+ mTextDir = newTextDir;
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected TextDirectionHeuristic getTextDirectionHeuristic() {
+ if (hasPasswordTransformationMethod()) {
+ // passwords fields should be LTR
+ return TextDirectionHeuristics.LTR;
+ }
+
+ if (mEditor != null
+ && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_PHONE) {
+ // Phone numbers must be in the direction of the locale's digits. Most locales have LTR
+ // digits, but some locales, such as those written in the Adlam or N'Ko scripts, have
+ // RTL digits.
+ final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(getTextLocale());
+ final String zero = symbols.getDigitStrings()[0];
+ // In case the zero digit is multi-codepoint, just use the first codepoint to determine
+ // direction.
+ final int firstCodepoint = zero.codePointAt(0);
+ final byte digitDirection = Character.getDirectionality(firstCodepoint);
+ if (digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT
+ || digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) {
+ return TextDirectionHeuristics.RTL;
+ } else {
+ return TextDirectionHeuristics.LTR;
+ }
+ }
+
+ // Always need to resolve layout direction first
+ final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+
+ // Now, we can select the heuristic
+ switch (getTextDirection()) {
+ default:
+ case TEXT_DIRECTION_FIRST_STRONG:
+ return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
+ TextDirectionHeuristics.FIRSTSTRONG_LTR);
+ case TEXT_DIRECTION_ANY_RTL:
+ return TextDirectionHeuristics.ANYRTL_LTR;
+ case TEXT_DIRECTION_LTR:
+ return TextDirectionHeuristics.LTR;
+ case TEXT_DIRECTION_RTL:
+ return TextDirectionHeuristics.RTL;
+ case TEXT_DIRECTION_LOCALE:
+ return TextDirectionHeuristics.LOCALE;
+ case TEXT_DIRECTION_FIRST_STRONG_LTR:
+ return TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ case TEXT_DIRECTION_FIRST_STRONG_RTL:
+ return TextDirectionHeuristics.FIRSTSTRONG_RTL;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void onResolveDrawables(int layoutDirection) {
+ // No need to resolve twice
+ if (mLastLayoutDirection == layoutDirection) {
+ return;
+ }
+ mLastLayoutDirection = layoutDirection;
+
+ // Resolve drawables
+ if (mDrawables != null) {
+ if (mDrawables.resolveWithLayoutDirection(layoutDirection)) {
+ prepareDrawableForDisplay(mDrawables.mShowing[Drawables.LEFT]);
+ prepareDrawableForDisplay(mDrawables.mShowing[Drawables.RIGHT]);
+ applyCompoundDrawableTint();
+ }
+ }
+ }
+
+ /**
+ * Prepares a drawable for display by propagating layout direction and
+ * drawable state.
+ *
+ * @param dr the drawable to prepare
+ */
+ private void prepareDrawableForDisplay(@Nullable Drawable dr) {
+ if (dr == null) {
+ return;
+ }
+
+ dr.setLayoutDirection(getLayoutDirection());
+
+ if (dr.isStateful()) {
+ dr.setState(getDrawableState());
+ dr.jumpToCurrentState();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected void resetResolvedDrawables() {
+ super.resetResolvedDrawables();
+ mLastLayoutDirection = -1;
+ }
+
+ /**
+ * @hide
+ */
+ protected void viewClicked(InputMethodManager imm) {
+ if (imm != null) {
+ imm.viewClicked(this);
+ }
+ }
+
+ /**
+ * Deletes the range of text [start, end[.
+ * @hide
+ */
+ protected void deleteText_internal(int start, int end) {
+ ((Editable) mText).delete(start, end);
+ }
+
+ /**
+ * Replaces the range of text [start, end[ by replacement text
+ * @hide
+ */
+ protected void replaceText_internal(int start, int end, CharSequence text) {
+ ((Editable) mText).replace(start, end, text);
+ }
+
+ /**
+ * Sets a span on the specified range of text
+ * @hide
+ */
+ protected void setSpan_internal(Object span, int start, int end, int flags) {
+ ((Editable) mText).setSpan(span, start, end, flags);
+ }
+
+ /**
+ * Moves the cursor to the specified offset position in text
+ * @hide
+ */
+ protected void setCursorPosition_internal(int start, int end) {
+ Selection.setSelection(((Editable) mText), start, end);
+ }
+
+ /**
+ * An Editor should be created as soon as any of the editable-specific fields (grouped
+ * inside the Editor object) is assigned to a non-default value.
+ * This method will create the Editor if needed.
+ *
+ * A standard TextView (as well as buttons, checkboxes...) should not qualify and hence will
+ * have a null Editor, unlike an EditText. Inconsistent in-between states will have an
+ * Editor for backward compatibility, as soon as one of these fields is assigned.
+ *
+ * Also note that for performance reasons, the mEditor is created when needed, but not
+ * reset when no more edit-specific fields are needed.
+ */
+ private void createEditorIfNeeded() {
+ if (mEditor == null) {
+ mEditor = new Editor(this);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public CharSequence getIterableTextForAccessibility() {
+ return mText;
+ }
+
+ private void ensureIterableTextForAccessibilitySelectable() {
+ if (!(mText instanceof Spannable)) {
+ setText(mText, BufferType.SPANNABLE);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public TextSegmentIterator getIteratorForGranularity(int granularity) {
+ switch (granularity) {
+ case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: {
+ Spannable text = (Spannable) getIterableTextForAccessibility();
+ if (!TextUtils.isEmpty(text) && getLayout() != null) {
+ AccessibilityIterators.LineTextSegmentIterator iterator =
+ AccessibilityIterators.LineTextSegmentIterator.getInstance();
+ iterator.initialize(text, getLayout());
+ return iterator;
+ }
+ } break;
+ case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: {
+ Spannable text = (Spannable) getIterableTextForAccessibility();
+ if (!TextUtils.isEmpty(text) && getLayout() != null) {
+ AccessibilityIterators.PageTextSegmentIterator iterator =
+ AccessibilityIterators.PageTextSegmentIterator.getInstance();
+ iterator.initialize(this);
+ return iterator;
+ }
+ } break;
+ }
+ return super.getIteratorForGranularity(granularity);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int getAccessibilitySelectionStart() {
+ return getSelectionStart();
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isAccessibilitySelectionExtendable() {
+ return true;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int getAccessibilitySelectionEnd() {
+ return getSelectionEnd();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void setAccessibilitySelection(int start, int end) {
+ if (getAccessibilitySelectionStart() == start
+ && getAccessibilitySelectionEnd() == end) {
+ return;
+ }
+ CharSequence text = getIterableTextForAccessibility();
+ if (Math.min(start, end) >= 0 && Math.max(start, end) <= text.length()) {
+ Selection.setSelection((Spannable) text, start, end);
+ } else {
+ Selection.removeSelection((Spannable) text);
+ }
+ // Hide all selection controllers used for adjusting selection
+ // since we are doing so explicitlty by other means and these
+ // controllers interact with how selection behaves.
+ if (mEditor != null) {
+ mEditor.hideCursorAndSpanControllers();
+ mEditor.stopTextActionMode();
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+ super.encodeProperties(stream);
+
+ TruncateAt ellipsize = getEllipsize();
+ stream.addProperty("text:ellipsize", ellipsize == null ? null : ellipsize.name());
+ stream.addProperty("text:textSize", getTextSize());
+ stream.addProperty("text:scaledTextSize", getScaledTextSize());
+ stream.addProperty("text:typefaceStyle", getTypefaceStyle());
+ stream.addProperty("text:selectionStart", getSelectionStart());
+ stream.addProperty("text:selectionEnd", getSelectionEnd());
+ stream.addProperty("text:curTextColor", mCurTextColor);
+ stream.addProperty("text:text", mText == null ? null : mText.toString());
+ stream.addProperty("text:gravity", mGravity);
+ }
+
+ /**
+ * User interface state that is stored by TextView for implementing
+ * {@link View#onSaveInstanceState}.
+ */
+ public static class SavedState extends BaseSavedState {
+ int selStart = -1;
+ int selEnd = -1;
+ CharSequence text;
+ boolean frozenWithFocus;
+ CharSequence error;
+ ParcelableParcel editorState; // Optional state from Editor.
+
+ 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);
+
+ if (error == null) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ TextUtils.writeToParcel(error, out, flags);
+ }
+
+ if (editorState == null) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ editorState.writeToParcel(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 + "}";
+ }
+
+ @SuppressWarnings("hiding")
+ 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);
+
+ if (in.readInt() != 0) {
+ error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+
+ if (in.readInt() != 0) {
+ editorState = ParcelableParcel.CREATOR.createFromParcel(in);
+ }
+ }
+ }
+
+ 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];
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ public void drawText(BaseCanvas c, int start, int end,
+ float x, float y, Paint p) {
+ c.drawText(mChars, start + mStart, end - start, x, y, p);
+ }
+
+ @Override
+ public void drawTextRun(BaseCanvas c, int start, int end,
+ int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint p) {
+ int count = end - start;
+ int contextCount = contextEnd - contextStart;
+ c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
+ contextCount, x, y, isRtl, 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);
+ }
+
+ public float getTextRunAdvances(int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, float[] advances, int advancesIndex,
+ Paint p) {
+ int count = end - start;
+ int contextCount = contextEnd - contextStart;
+ return p.getTextRunAdvances(mChars, start + mStart, count,
+ contextStart + mStart, contextCount, isRtl, advances,
+ advancesIndex);
+ }
+
+ public int getTextRunCursor(int contextStart, int contextEnd, int dir,
+ int offset, int cursorOpt, Paint p) {
+ int contextCount = contextEnd - contextStart;
+ return p.getTextRunCursor(mChars, contextStart + mStart,
+ contextCount, dir, offset + mStart, cursorOpt);
+ }
+ }
+
+ private static final class Marquee {
+ // TODO: Add an option to configure this
+ private static final float MARQUEE_DELTA_MAX = 0.07f;
+ private static final int MARQUEE_DELAY = 1200;
+ private static final int MARQUEE_DP_PER_SECOND = 30;
+
+ private static final byte MARQUEE_STOPPED = 0x0;
+ private static final byte MARQUEE_STARTING = 0x1;
+ private static final byte MARQUEE_RUNNING = 0x2;
+
+ private final WeakReference<TextView> mView;
+ private final Choreographer mChoreographer;
+
+ private byte mStatus = MARQUEE_STOPPED;
+ private final float mPixelsPerSecond;
+ private float mMaxScroll;
+ private float mMaxFadeScroll;
+ private float mGhostStart;
+ private float mGhostOffset;
+ private float mFadeStop;
+ private int mRepeatLimit;
+
+ private float mScroll;
+ private long mLastAnimationMs;
+
+ Marquee(TextView v) {
+ final float density = v.getContext().getResources().getDisplayMetrics().density;
+ mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
+ mView = new WeakReference<TextView>(v);
+ mChoreographer = Choreographer.getInstance();
+ }
+
+ private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ tick();
+ }
+ };
+
+ private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ mStatus = MARQUEE_RUNNING;
+ mLastAnimationMs = mChoreographer.getFrameTime();
+ tick();
+ }
+ };
+
+ private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (mStatus == MARQUEE_RUNNING) {
+ if (mRepeatLimit >= 0) {
+ mRepeatLimit--;
+ }
+ start(mRepeatLimit);
+ }
+ }
+ };
+
+ void tick() {
+ if (mStatus != MARQUEE_RUNNING) {
+ return;
+ }
+
+ mChoreographer.removeFrameCallback(mTickCallback);
+
+ final TextView textView = mView.get();
+ if (textView != null && (textView.isFocused() || textView.isSelected())) {
+ long currentMs = mChoreographer.getFrameTime();
+ long deltaMs = currentMs - mLastAnimationMs;
+ mLastAnimationMs = currentMs;
+ float deltaPx = deltaMs / 1000f * mPixelsPerSecond;
+ mScroll += deltaPx;
+ if (mScroll > mMaxScroll) {
+ mScroll = mMaxScroll;
+ mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
+ } else {
+ mChoreographer.postFrameCallback(mTickCallback);
+ }
+ textView.invalidate();
+ }
+ }
+
+ void stop() {
+ mStatus = MARQUEE_STOPPED;
+ mChoreographer.removeFrameCallback(mStartCallback);
+ mChoreographer.removeFrameCallback(mRestartCallback);
+ mChoreographer.removeFrameCallback(mTickCallback);
+ resetScroll();
+ }
+
+ private void resetScroll() {
+ mScroll = 0.0f;
+ final TextView textView = mView.get();
+ if (textView != null) textView.invalidate();
+ }
+
+ void start(int repeatLimit) {
+ if (repeatLimit == 0) {
+ stop();
+ return;
+ }
+ mRepeatLimit = repeatLimit;
+ final TextView textView = mView.get();
+ if (textView != null && textView.mLayout != null) {
+ mStatus = MARQUEE_STARTING;
+ mScroll = 0.0f;
+ final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
+ - textView.getCompoundPaddingRight();
+ final float lineWidth = textView.mLayout.getLineWidth(0);
+ final float gap = textWidth / 3.0f;
+ mGhostStart = lineWidth - textWidth + gap;
+ mMaxScroll = mGhostStart + textWidth;
+ mGhostOffset = lineWidth + gap;
+ mFadeStop = lineWidth + textWidth / 6.0f;
+ mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
+
+ textView.invalidate();
+ mChoreographer.postFrameCallback(mStartCallback);
+ }
+ }
+
+ float getGhostOffset() {
+ return mGhostOffset;
+ }
+
+ float getScroll() {
+ return mScroll;
+ }
+
+ float getMaxFadeScroll() {
+ return mMaxFadeScroll;
+ }
+
+ boolean shouldDrawLeftFade() {
+ return mScroll <= mFadeStop;
+ }
+
+ boolean shouldDrawGhost() {
+ return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
+ }
+
+ boolean isRunning() {
+ return mStatus == MARQUEE_RUNNING;
+ }
+
+ boolean isStopped() {
+ return mStatus == MARQUEE_STOPPED;
+ }
+ }
+
+ private class ChangeWatcher implements TextWatcher, SpanWatcher {
+
+ private CharSequence mBeforeText;
+
+ public void beforeTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "beforeTextChanged start=" + start
+ + " before=" + before + " after=" + after + ": " + buffer);
+ }
+
+ if (AccessibilityManager.getInstance(mContext).isEnabled()
+ && !isPasswordInputType(getInputType()) && !hasPasswordTransformationMethod()) {
+ mBeforeText = buffer.toString();
+ }
+
+ TextView.this.sendBeforeTextChanged(buffer, start, before, after);
+ }
+
+ public void onTextChanged(CharSequence buffer, int start, int before, int after) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "onTextChanged start=" + start
+ + " before=" + before + " after=" + after + ": " + buffer);
+ }
+ TextView.this.handleTextChanged(buffer, start, before, after);
+
+ if (AccessibilityManager.getInstance(mContext).isEnabled()
+ && (isFocused() || isSelected() && isShown())) {
+ sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
+ mBeforeText = null;
+ }
+ }
+
+ public void afterTextChanged(Editable buffer) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "afterTextChanged: " + buffer);
+ }
+ TextView.this.sendAfterTextChanged(buffer);
+
+ if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
+ MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
+ }
+ }
+
+ public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
+ + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
+ }
+ TextView.this.spanChange(buf, what, s, st, e, en);
+ }
+
+ public void onSpanAdded(Spannable buf, Object what, int s, int e) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf);
+ }
+ TextView.this.spanChange(buf, what, -1, s, -1, e);
+ }
+
+ public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
+ if (DEBUG_EXTRACT) {
+ Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf);
+ }
+ TextView.this.spanChange(buf, what, s, -1, e, -1);
+ }
+ }
+}
diff --git a/android/widget/TextViewAutoSizeLayoutPerfTest.java b/android/widget/TextViewAutoSizeLayoutPerfTest.java
new file mode 100644
index 0000000..c310166
--- /dev/null
+++ b/android/widget/TextViewAutoSizeLayoutPerfTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 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.os.Looper;
+import android.os.Bundle;
+import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
+import android.view.View;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.perftests.core.R;
+
+import java.util.Locale;
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewAutoSizeLayoutPerfTest {
+ @Parameters(name = "{0}")
+ public static Collection layouts() {
+ return Arrays.asList(new Object[][] {
+ { "Basic TextView - no autosize", R.layout.test_basic_textview_layout},
+ { "Autosize TextView 5 sizes", R.layout.test_autosize_textview_5},
+ { "Autosize TextView 10 sizes", R.layout.test_autosize_textview_10},
+ { "Autosize TextView 50 sizes", R.layout.test_autosize_textview_50},
+ { "Autosize TextView 100 sizes", R.layout.test_autosize_textview_100},
+ { "Autosize TextView 300 sizes", R.layout.test_autosize_textview_300},
+ { "Autosize TextView 500 sizes", R.layout.test_autosize_textview_500},
+ { "Autosize TextView 1000 sizes", R.layout.test_autosize_textview_1000},
+ { "Autosize TextView 10000 sizes", R.layout.test_autosize_textview_10000},
+ { "Autosize TextView 100000 sizes", R.layout.test_autosize_textview_100000}
+ });
+ }
+
+ private int mLayoutId;
+
+ public TextViewAutoSizeLayoutPerfTest(String key, int layoutId) {
+ mLayoutId = layoutId;
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule =
+ new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testConstruction() throws Throwable {
+ mActivityRule.runOnUiThread(() -> {
+ assertTrue("We should be running on the main thread",
+ Looper.getMainLooper().getThread() == Thread.currentThread());
+ assertTrue("We should be running on the main thread",
+ Looper.myLooper() == Looper.getMainLooper());
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Activity activity = mActivityRule.getActivity();
+ activity.setContentView(mLayoutId);
+
+ while (state.keepRunning()) {
+ TextView textView = new TextView(activity);
+ // TextView#onLayout() gets called, which triggers TextView#autoSizeText()
+ // which is the method we want to benchmark.
+ textView.requestLayout();
+ }
+ });
+ }
+}
diff --git a/android/widget/TextViewFontFamilyLayoutPerfTest.java b/android/widget/TextViewFontFamilyLayoutPerfTest.java
new file mode 100644
index 0000000..4b6da6b
--- /dev/null
+++ b/android/widget/TextViewFontFamilyLayoutPerfTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 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.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.LayoutInflater;
+
+import com.android.perftests.core.R;
+
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewFontFamilyLayoutPerfTest {
+ @Parameters(name = "{0}")
+ public static Collection layouts() {
+ return Arrays.asList(new Object[][] {
+ { "String fontFamily attribute", R.layout.test_textview_font_family_string},
+ { "File fontFamily attribute", R.layout.test_textview_font_family_file},
+ { "XML fontFamily attribute", R.layout.test_textview_font_family_xml},
+ });
+ }
+
+ private int mLayoutId;
+
+ public TextViewFontFamilyLayoutPerfTest(String key, int layoutId) {
+ mLayoutId = layoutId;
+ }
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testConstruction() throws Throwable {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final LayoutInflater inflator =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ while (state.keepRunning()) {
+ inflator.inflate(mLayoutId, null, false);
+ }
+ }
+}
diff --git a/android/widget/TextViewMetrics.java b/android/widget/TextViewMetrics.java
new file mode 100644
index 0000000..96d1794
--- /dev/null
+++ b/android/widget/TextViewMetrics.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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;
+
+/**
+ * {@link com.android.internal.logging.MetricsLogger} values for TextView.
+ *
+ * @hide
+ */
+public final class TextViewMetrics {
+
+ private TextViewMetrics() {}
+
+ /**
+ * Long press on TextView - no special classification.
+ */
+ public static final int SUBTYPE_LONG_PRESS_OTHER = 0;
+ /**
+ * Long press on TextView - selection started.
+ */
+ public static final int SUBTYPE_LONG_PRESS_SELECTION = 1;
+ /**
+ * Long press on TextView - drag and drop started.
+ */
+ public static final int SUBTYPE_LONG_PRESS_DRAG_AND_DROP = 2;
+
+ /**
+ * Assist menu item (shown or clicked) - classification: other.
+ */
+ public static final int SUBTYPE_ASSIST_MENU_ITEM_OTHER = 0;
+
+ /**
+ * Assist menu item (shown or clicked) - classification: email.
+ */
+ public static final int SUBTYPE_ASSIST_MENU_ITEM_EMAIL = 1;
+
+ /**
+ * Assist menu item (shown or clicked) - classification: phone.
+ */
+ public static final int SUBTYPE_ASSIST_MENU_ITEM_PHONE = 2;
+
+ /**
+ * Assist menu item (shown or clicked) - classification: address.
+ */
+ public static final int SUBTYPE_ASSIST_MENU_ITEM_ADDRESS = 3;
+
+ /**
+ * Assist menu item (shown or clicked) - classification: url.
+ */
+ public static final int SUBTYPE_ASSIST_MENU_ITEM_URL = 4;
+}
diff --git a/android/widget/TextViewOnMeasurePerfTest.java b/android/widget/TextViewOnMeasurePerfTest.java
new file mode 100644
index 0000000..a14dd25
--- /dev/null
+++ b/android/widget/TextViewOnMeasurePerfTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017 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 static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Typeface;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.TextAppearanceSpan;
+import android.view.LayoutInflater;
+
+import com.android.perftests.core.R;
+
+import java.util.Random;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TextViewOnMeasurePerfTest {
+ private static final String MULTILINE_TEXT =
+ "Lorem ipsum dolor sit amet, \n"+
+ "consectetur adipiscing elit, \n" +
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n" +
+ "Ut enim ad minim veniam, \n" +
+ "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat " +
+ "nulla pariatur.\n" +
+ "Excepteur sint occaecat cupidatat non proident, \n" +
+ "sunt in culpa qui officia deserunt mollit anim id est laborum.\n";
+
+ private static final int VIEW_WIDTH = 1000;
+ private static final int VIEW_HEIGHT = 1000;
+ private static final CharSequence COMPLEX_MULTILINE_TEXT;
+ static {
+ final SpannableStringBuilder ssb = new SpannableStringBuilder();
+
+ // To emphasize, append multiline text 10 times.
+ for (int i = 0; i < 10; ++i) {
+ ssb.append(MULTILINE_TEXT);
+ }
+
+ final ColorStateList[] COLORS = {
+ ColorStateList.valueOf(0xFFFF0000), // RED
+ ColorStateList.valueOf(0xFF00FF00), // GREEN
+ ColorStateList.valueOf(0xFF0000FF), // BLUE
+ };
+
+ final int[] STYLES = {
+ Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC
+ };
+
+ final String[] FAMILIES = { "sans-serif", "serif", "monospace" };
+
+ // Append random span to text.
+ final Random random = new Random(0);
+ for (int pos = 0; pos < ssb.length();) {
+ final TextAppearanceSpan span = new TextAppearanceSpan(
+ FAMILIES[random.nextInt(FAMILIES.length)],
+ STYLES[random.nextInt(STYLES.length)],
+ 24 + random.nextInt(32), // text size. minimum 24
+ COLORS[random.nextInt(COLORS.length)],
+ COLORS[random.nextInt(COLORS.length)]);
+ int spanLength = 1 + random.nextInt(9); // Up to 9 span length.
+ if (pos + spanLength > ssb.length()) {
+ spanLength = ssb.length() - pos;
+ }
+ ssb.setSpan(span, pos, pos + spanLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ pos += spanLength;
+ }
+ COMPLEX_MULTILINE_TEXT = ssb;
+ }
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testMeasure_AtMost() throws Throwable {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ final TextView textView = new TextView(context);
+ textView.setText(COMPLEX_MULTILINE_TEXT);
+
+ while (state.keepRunning()) {
+ // Changing locale to invalidate internal layout.
+ textView.setTextLocale(Locale.UK);
+ textView.setTextLocale(Locale.US);
+
+ textView.measure(AT_MOST | VIEW_WIDTH, AT_MOST | VIEW_HEIGHT);
+ }
+ }
+
+ @Test
+ public void testMeasure_Exactly() throws Throwable {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ final TextView textView = new TextView(context);
+ textView.setText(COMPLEX_MULTILINE_TEXT);
+
+ while (state.keepRunning()) {
+ // Changing locale to invalidate internal layout.
+ textView.setTextLocale(Locale.UK);
+ textView.setTextLocale(Locale.US);
+
+ textView.measure(EXACTLY | VIEW_WIDTH, EXACTLY | VIEW_HEIGHT);
+ }
+ }
+
+ @Test
+ public void testMeasure_Unspecified() throws Throwable {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ final TextView textView = new TextView(context);
+ textView.setText(COMPLEX_MULTILINE_TEXT);
+
+ while (state.keepRunning()) {
+ // Changing locale to invalidate internal layout.
+ textView.setTextLocale(Locale.UK);
+ textView.setTextLocale(Locale.US);
+
+ textView.measure(UNSPECIFIED, UNSPECIFIED);
+ }
+ }
+}
diff --git a/android/widget/TextViewSetTextLocalePerfTest.java b/android/widget/TextViewSetTextLocalePerfTest.java
new file mode 100644
index 0000000..7fc5e4f
--- /dev/null
+++ b/android/widget/TextViewSetTextLocalePerfTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 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.os.Bundle;
+import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+import java.util.Locale;
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewSetTextLocalePerfTest {
+ @Parameters(name = "{0}")
+ public static Collection locales() {
+ return Arrays.asList(new Object[][] {
+ { "SameLocale", "en-US", "en-US" },
+ { "DifferentLocale", "en-US", "ja-JP"}
+ });
+ }
+
+ private String mMetricKey;
+ private Locale mFirstLocale;
+ private Locale mSecondLocale;
+
+ public TextViewSetTextLocalePerfTest(
+ String metricKey, String firstLocale, String secondLocale) {
+ mMetricKey = metricKey;
+ mFirstLocale = Locale.forLanguageTag(firstLocale);
+ mSecondLocale = Locale.forLanguageTag(secondLocale);
+ }
+
+ @Rule
+ public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testSetTextLocale() {
+ TextView textView = new TextView(mActivityRule.getActivity());
+
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ textView.setTextLocale(mFirstLocale);
+ textView.setTextLocale(mSecondLocale);
+ }
+ }
+}
diff --git a/android/widget/ThemedSpinnerAdapter.java b/android/widget/ThemedSpinnerAdapter.java
new file mode 100644
index 0000000..6d92620
--- /dev/null
+++ b/android/widget/ThemedSpinnerAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An extension of SpinnerAdapter that is capable of inflating drop-down views
+ * against a different theme than normal views.
+ * <p>
+ * Classes that implement this interface should use the theme provided to
+ * {@link #setDropDownViewTheme(Theme)} when creating views in
+ * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}.
+ */
+public interface ThemedSpinnerAdapter extends SpinnerAdapter {
+ /**
+ * Sets the {@link Resources.Theme} against which drop-down views are
+ * inflated.
+ *
+ * @param theme the context against which to inflate drop-down views, or
+ * {@code null} to use the default theme
+ * @see SpinnerAdapter#getDropDownView(int, View, ViewGroup)
+ */
+ void setDropDownViewTheme(@Nullable Resources.Theme theme);
+
+ /**
+ * Returns the value previously set by a call to
+ * {@link #setDropDownViewTheme(Theme)}.
+ *
+ * @return the {@link Resources.Theme} against which drop-down views are
+ * inflated, or {@code null} if one has not been explicitly set
+ */
+ @Nullable
+ Resources.Theme getDropDownViewTheme();
+}
diff --git a/android/widget/TimePicker.java b/android/widget/TimePicker.java
new file mode 100644
index 0000000..ae6881e
--- /dev/null
+++ b/android/widget/TimePicker.java
@@ -0,0 +1,577 @@
+/*
+ * 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.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.icu.util.Calendar;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * A widget for selecting the time of day, in either 24-hour or AM/PM mode.
+ * <p>
+ * For a dialog using this view, see {@link android.app.TimePickerDialog}. See
+ * the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide for more information.
+ *
+ * @attr ref android.R.styleable#TimePicker_timePickerMode
+ */
+@Widget
+public class TimePicker extends FrameLayout {
+ private static final String LOG_TAG = TimePicker.class.getSimpleName();
+
+ /**
+ * Presentation mode for the Holo-style time picker that uses a set of
+ * {@link android.widget.NumberPicker}s.
+ *
+ * @see #getMode()
+ * @hide Visible for testing only.
+ */
+ @TestApi
+ public static final int MODE_SPINNER = 1;
+
+ /**
+ * Presentation mode for the Material-style time picker that uses a clock
+ * face.
+ *
+ * @see #getMode()
+ * @hide Visible for testing only.
+ */
+ @TestApi
+ public static final int MODE_CLOCK = 2;
+
+ /** @hide */
+ @IntDef({MODE_SPINNER, MODE_CLOCK})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TimePickerMode {}
+
+ private final TimePickerDelegate mDelegate;
+
+ @TimePickerMode
+ private final int mMode;
+
+ /**
+ * 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, R.attr.timePickerStyle);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // DatePicker is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+ final boolean isDialogMode = a.getBoolean(R.styleable.TimePicker_dialogMode, false);
+ final int requestedMode = a.getInt(R.styleable.TimePicker_timePickerMode, MODE_SPINNER);
+ a.recycle();
+
+ if (requestedMode == MODE_CLOCK && isDialogMode) {
+ // You want MODE_CLOCK? YOU CAN'T HANDLE MODE_CLOCK! Well, maybe
+ // you can depending on your screen size. Let's check...
+ mMode = context.getResources().getInteger(R.integer.time_picker_mode);
+ } else {
+ mMode = requestedMode;
+ }
+
+ switch (mMode) {
+ case MODE_CLOCK:
+ mDelegate = new TimePickerClockDelegate(
+ this, context, attrs, defStyleAttr, defStyleRes);
+ break;
+ case MODE_SPINNER:
+ default:
+ mDelegate = new TimePickerSpinnerDelegate(
+ this, context, attrs, defStyleAttr, defStyleRes);
+ break;
+ }
+ mDelegate.setAutoFillChangeListener((v, h, m) -> {
+ final AutofillManager afm = context.getSystemService(AutofillManager.class);
+ if (afm != null) {
+ afm.notifyValueChanged(this);
+ }
+ });
+ }
+
+ /**
+ * @return the picker's presentation mode, one of {@link #MODE_CLOCK} or
+ * {@link #MODE_SPINNER}
+ * @attr ref android.R.styleable#TimePicker_timePickerMode
+ * @hide Visible for testing only.
+ */
+ @TimePickerMode
+ @TestApi
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * Sets the currently selected hour using 24-hour time.
+ *
+ * @param hour the hour to set, in the range (0-23)
+ * @see #getHour()
+ */
+ public void setHour(@IntRange(from = 0, to = 23) int hour) {
+ mDelegate.setHour(MathUtils.constrain(hour, 0, 23));
+ }
+
+ /**
+ * Returns the currently selected hour using 24-hour time.
+ *
+ * @return the currently selected hour, in the range (0-23)
+ * @see #setHour(int)
+ */
+ public int getHour() {
+ return mDelegate.getHour();
+ }
+
+ /**
+ * Sets the currently selected minute.
+ *
+ * @param minute the minute to set, in the range (0-59)
+ * @see #getMinute()
+ */
+ public void setMinute(@IntRange(from = 0, to = 59) int minute) {
+ mDelegate.setMinute(MathUtils.constrain(minute, 0, 59));
+ }
+
+ /**
+ * Returns the currently selected minute.
+ *
+ * @return the currently selected minute, in the range (0-59)
+ * @see #setMinute(int)
+ */
+ public int getMinute() {
+ return mDelegate.getMinute();
+ }
+
+ /**
+ * Sets the currently selected hour using 24-hour time.
+ *
+ * @param currentHour the hour to set, in the range (0-23)
+ * @deprecated Use {@link #setHour(int)}
+ */
+ @Deprecated
+ public void setCurrentHour(@NonNull Integer currentHour) {
+ setHour(currentHour);
+ }
+
+ /**
+ * @return the currently selected hour, in the range (0-23)
+ * @deprecated Use {@link #getHour()}
+ */
+ @NonNull
+ @Deprecated
+ public Integer getCurrentHour() {
+ return getHour();
+ }
+
+ /**
+ * Sets the currently selected minute.
+ *
+ * @param currentMinute the minute to set, in the range (0-59)
+ * @deprecated Use {@link #setMinute(int)}
+ */
+ @Deprecated
+ public void setCurrentMinute(@NonNull Integer currentMinute) {
+ setMinute(currentMinute);
+ }
+
+ /**
+ * @return the currently selected minute, in the range (0-59)
+ * @deprecated Use {@link #getMinute()}
+ */
+ @NonNull
+ @Deprecated
+ public Integer getCurrentMinute() {
+ return getMinute();
+ }
+
+ /**
+ * Sets whether this widget displays time in 24-hour mode or 12-hour mode
+ * with an AM/PM picker.
+ *
+ * @param is24HourView {@code true} to display in 24-hour mode,
+ * {@code false} for 12-hour mode with AM/PM
+ * @see #is24HourView()
+ */
+ public void setIs24HourView(@NonNull Boolean is24HourView) {
+ if (is24HourView == null) {
+ return;
+ }
+
+ mDelegate.setIs24Hour(is24HourView);
+ }
+
+ /**
+ * @return {@code true} if this widget displays time in 24-hour mode,
+ * {@code false} otherwise}
+ * @see #setIs24HourView(Boolean)
+ */
+ public boolean is24HourView() {
+ return mDelegate.is24Hour();
+ }
+
+ /**
+ * 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) {
+ mDelegate.setOnTimeChangedListener(onTimeChangedListener);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mDelegate.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mDelegate.isEnabled();
+ }
+
+ @Override
+ public int getBaseline() {
+ return mDelegate.getBaseline();
+ }
+
+ /**
+ * Validates whether current input by the user is a valid time based on the locale. TimePicker
+ * will show an error message to the user if the time is not valid.
+ *
+ * @return {@code true} if the input is valid, {@code false} otherwise
+ */
+ public boolean validateInput() {
+ return mDelegate.validateInput();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return mDelegate.onSaveInstanceState(superState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ BaseSavedState ss = (BaseSavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDelegate.onRestoreInstanceState(ss);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TimePicker.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ return mDelegate.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ /** @hide */
+ @TestApi
+ public View getHourView() {
+ return mDelegate.getHourView();
+ }
+
+ /** @hide */
+ @TestApi
+ public View getMinuteView() {
+ return mDelegate.getMinuteView();
+ }
+
+ /** @hide */
+ @TestApi
+ public View getAmView() {
+ return mDelegate.getAmView();
+ }
+
+ /** @hide */
+ @TestApi
+ public View getPmView() {
+ return mDelegate.getPmView();
+ }
+
+ /**
+ * A delegate interface that defined the public API of the TimePicker. Allows different
+ * TimePicker implementations. This would need to be implemented by the TimePicker delegates
+ * for the real behavior.
+ */
+ interface TimePickerDelegate {
+ void setHour(@IntRange(from = 0, to = 23) int hour);
+ int getHour();
+
+ void setMinute(@IntRange(from = 0, to = 59) int minute);
+ int getMinute();
+
+ void setDate(@IntRange(from = 0, to = 23) int hour,
+ @IntRange(from = 0, to = 59) int minute);
+
+ void autofill(AutofillValue value);
+ AutofillValue getAutofillValue();
+
+ void setIs24Hour(boolean is24Hour);
+ boolean is24Hour();
+
+ boolean validateInput();
+
+ void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener);
+ void setAutoFillChangeListener(OnTimeChangedListener autoFillChangeListener);
+
+ void setEnabled(boolean enabled);
+ boolean isEnabled();
+
+ int getBaseline();
+
+ Parcelable onSaveInstanceState(Parcelable superState);
+ void onRestoreInstanceState(Parcelable state);
+
+ boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+ void onPopulateAccessibilityEvent(AccessibilityEvent event);
+
+ /** @hide */
+ @TestApi View getHourView();
+
+ /** @hide */
+ @TestApi View getMinuteView();
+
+ /** @hide */
+ @TestApi View getAmView();
+
+ /** @hide */
+ @TestApi View getPmView();
+ }
+
+ static String[] getAmPmStrings(Context context) {
+ final Locale locale = context.getResources().getConfiguration().locale;
+ final LocaleData d = LocaleData.get(locale);
+
+ final String[] result = new String[2];
+ result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
+ result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
+ return result;
+ }
+
+ /**
+ * An abstract class which can be used as a start for TimePicker implementations
+ */
+ abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
+ protected final TimePicker mDelegator;
+ protected final Context mContext;
+ protected final Locale mLocale;
+
+ protected OnTimeChangedListener mOnTimeChangedListener;
+ protected OnTimeChangedListener mAutoFillChangeListener;
+
+ // The value that was passed to autofill() - it must be stored because it getAutofillValue()
+ // must return the exact same value that was autofilled, otherwise the widget will not be
+ // properly highlighted after autofill().
+ private long mAutofilledValue;
+
+ public AbstractTimePickerDelegate(@NonNull TimePicker delegator, @NonNull Context context) {
+ mDelegator = delegator;
+ mContext = context;
+ mLocale = context.getResources().getConfiguration().locale;
+ }
+
+ @Override
+ public void setOnTimeChangedListener(OnTimeChangedListener callback) {
+ mOnTimeChangedListener = callback;
+ }
+
+ @Override
+ public void setAutoFillChangeListener(OnTimeChangedListener callback) {
+ mAutoFillChangeListener = callback;
+ }
+
+ @Override
+ public final void autofill(AutofillValue value) {
+ if (value == null || !value.isDate()) {
+ Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ return;
+ }
+
+ final long time = value.getDateValue();
+
+ final Calendar cal = Calendar.getInstance(mLocale);
+ cal.setTimeInMillis(time);
+ setDate(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
+
+ // Must set mAutofilledValue *after* calling subclass method to make sure the value
+ // returned by getAutofillValue() matches it.
+ mAutofilledValue = time;
+ }
+
+ @Override
+ public final AutofillValue getAutofillValue() {
+ if (mAutofilledValue != 0) {
+ return AutofillValue.forDate(mAutofilledValue);
+ }
+
+ final Calendar cal = Calendar.getInstance(mLocale);
+ cal.set(Calendar.HOUR_OF_DAY, getHour());
+ cal.set(Calendar.MINUTE, getMinute());
+ return AutofillValue.forDate(cal.getTimeInMillis());
+ }
+
+ /**
+ * This method must be called every time the value of the hour and/or minute is changed by
+ * a subclass method.
+ */
+ protected void resetAutofilledValue() {
+ mAutofilledValue = 0;
+ }
+
+ protected static class SavedState extends View.BaseSavedState {
+ private final int mHour;
+ private final int mMinute;
+ private final boolean mIs24HourMode;
+ private final int mCurrentItemShowing;
+
+ public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) {
+ this(superState, hour, minute, is24HourMode, 0);
+ }
+
+ public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
+ int currentItemShowing) {
+ super(superState);
+ mHour = hour;
+ mMinute = minute;
+ mIs24HourMode = is24HourMode;
+ mCurrentItemShowing = currentItemShowing;
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ mHour = in.readInt();
+ mMinute = in.readInt();
+ mIs24HourMode = (in.readInt() == 1);
+ mCurrentItemShowing = in.readInt();
+ }
+
+ public int getHour() {
+ return mHour;
+ }
+
+ public int getMinute() {
+ return mMinute;
+ }
+
+ public boolean is24HourMode() {
+ return mIs24HourMode;
+ }
+
+ public int getCurrentItemShowing() {
+ return mCurrentItemShowing;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mHour);
+ dest.writeInt(mMinute);
+ dest.writeInt(mIs24HourMode ? 1 : 0);
+ dest.writeInt(mCurrentItemShowing);
+ }
+
+ @SuppressWarnings({"unused", "hiding"})
+ public static final 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
+ public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
+ // This view is self-sufficient for autofill, so it needs to call
+ // onProvideAutoFillStructure() to fill itself, but it does not need to call
+ // dispatchProvideAutoFillStructure() to fill its children.
+ structure.setAutofillId(getAutofillId());
+ onProvideAutofillStructure(structure, flags);
+ }
+
+ @Override
+ public void autofill(AutofillValue value) {
+ if (!isEnabled()) return;
+
+ mDelegate.autofill(value);
+ }
+
+ @Override
+ public @AutofillType int getAutofillType() {
+ return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
+ }
+
+ @Override
+ public AutofillValue getAutofillValue() {
+ return isEnabled() ? mDelegate.getAutofillValue() : null;
+ }
+}
diff --git a/android/widget/TimePickerClockDelegate.java b/android/widget/TimePickerClockDelegate.java
new file mode 100644
index 0000000..706b0ce
--- /dev/null
+++ b/android/widget/TimePickerClockDelegate.java
@@ -0,0 +1,1087 @@
+/*
+ * Copyright (C) 2013 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.IntDef;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.icu.text.DecimalFormatSymbols;
+import android.os.Parcelable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.style.TtsSpan;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.RadialTimePickerView.OnValueSelectedListener;
+import android.widget.TextInputTimePickerView.OnValueTypedListener;
+
+import com.android.internal.R;
+import com.android.internal.widget.NumericTextView;
+import com.android.internal.widget.NumericTextView.OnValueChangedListener;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+
+/**
+ * A delegate implementing the radial clock-based TimePicker.
+ */
+class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
+ /**
+ * Delay in milliseconds before valid but potentially incomplete, for
+ * example "1" but not "12", keyboard edits are propagated from the
+ * hour / minute fields to the radial picker.
+ */
+ private static final long DELAY_COMMIT_MILLIS = 2000;
+
+ @IntDef({FROM_EXTERNAL_API, FROM_RADIAL_PICKER, FROM_INPUT_PICKER})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ChangeSource {}
+ private static final int FROM_EXTERNAL_API = 0;
+ private static final int FROM_RADIAL_PICKER = 1;
+ private static final int FROM_INPUT_PICKER = 2;
+
+ // Index used by RadialPickerLayout
+ private static final int HOUR_INDEX = RadialTimePickerView.HOURS;
+ private static final int MINUTE_INDEX = RadialTimePickerView.MINUTES;
+
+ private static final int[] ATTRS_TEXT_COLOR = new int[] {R.attr.textColor};
+ private static final int[] ATTRS_DISABLED_ALPHA = new int[] {R.attr.disabledAlpha};
+
+ private static final int AM = 0;
+ private static final int PM = 1;
+
+ private static final int HOURS_IN_HALF_DAY = 12;
+
+ private final NumericTextView mHourView;
+ private final NumericTextView mMinuteView;
+ private final View mAmPmLayout;
+ private final RadioButton mAmLabel;
+ private final RadioButton mPmLabel;
+ private final RadialTimePickerView mRadialTimePickerView;
+ private final TextView mSeparatorView;
+
+ private boolean mRadialPickerModeEnabled = true;
+ private final ImageButton mRadialTimePickerModeButton;
+ private final String mRadialTimePickerModeEnabledDescription;
+ private final String mTextInputPickerModeEnabledDescription;
+ private final View mRadialTimePickerHeader;
+ private final View mTextInputPickerHeader;
+
+ private final TextInputTimePickerView mTextInputPickerView;
+
+ private final Calendar mTempCalendar;
+
+ // Accessibility strings.
+ private final String mSelectHours;
+ private final String mSelectMinutes;
+
+ private boolean mIsEnabled = true;
+ private boolean mAllowAutoAdvance;
+ private int mCurrentHour;
+ private int mCurrentMinute;
+ private boolean mIs24Hour;
+
+ // The portrait layout puts AM/PM at the right by default.
+ private boolean mIsAmPmAtLeft = false;
+ // The landscape layouts put AM/PM at the bottom by default.
+ private boolean mIsAmPmAtTop = false;
+
+ // Localization data.
+ private boolean mHourFormatShowLeadingZero;
+ private boolean mHourFormatStartsAtZero;
+
+ // Most recent time announcement values for accessibility.
+ private CharSequence mLastAnnouncedText;
+ private boolean mLastAnnouncedIsHour;
+
+ public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ // process style attributes
+ final TypedArray a = mContext.obtainStyledAttributes(attrs,
+ R.styleable.TimePicker, defStyleAttr, defStyleRes);
+ final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final Resources res = mContext.getResources();
+
+ mSelectHours = res.getString(R.string.select_hours);
+ mSelectMinutes = res.getString(R.string.select_minutes);
+
+ final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout,
+ R.layout.time_picker_material);
+ final View mainView = inflater.inflate(layoutResourceId, delegator);
+ mainView.setSaveFromParentEnabled(false);
+ mRadialTimePickerHeader = mainView.findViewById(R.id.time_header);
+ mRadialTimePickerHeader.setOnTouchListener(new NearestTouchDelegate());
+
+ // Set up hour/minute labels.
+ mHourView = (NumericTextView) mainView.findViewById(R.id.hours);
+ mHourView.setOnClickListener(mClickListener);
+ mHourView.setOnFocusChangeListener(mFocusListener);
+ mHourView.setOnDigitEnteredListener(mDigitEnteredListener);
+ mHourView.setAccessibilityDelegate(
+ new ClickActionDelegate(context, R.string.select_hours));
+ mSeparatorView = (TextView) mainView.findViewById(R.id.separator);
+ mMinuteView = (NumericTextView) mainView.findViewById(R.id.minutes);
+ mMinuteView.setOnClickListener(mClickListener);
+ mMinuteView.setOnFocusChangeListener(mFocusListener);
+ mMinuteView.setOnDigitEnteredListener(mDigitEnteredListener);
+ mMinuteView.setAccessibilityDelegate(
+ new ClickActionDelegate(context, R.string.select_minutes));
+ mMinuteView.setRange(0, 59);
+
+ // Set up AM/PM labels.
+ mAmPmLayout = mainView.findViewById(R.id.ampm_layout);
+ mAmPmLayout.setOnTouchListener(new NearestTouchDelegate());
+
+ final String[] amPmStrings = TimePicker.getAmPmStrings(context);
+ mAmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.am_label);
+ mAmLabel.setText(obtainVerbatim(amPmStrings[0]));
+ mAmLabel.setOnClickListener(mClickListener);
+ ensureMinimumTextWidth(mAmLabel);
+
+ mPmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.pm_label);
+ mPmLabel.setText(obtainVerbatim(amPmStrings[1]));
+ mPmLabel.setOnClickListener(mClickListener);
+ ensureMinimumTextWidth(mPmLabel);
+
+ // For the sake of backwards compatibility, attempt to extract the text
+ // color from the header time text appearance. If it's set, we'll let
+ // that override the "real" header text color.
+ ColorStateList headerTextColor = null;
+
+ @SuppressWarnings("deprecation")
+ final int timeHeaderTextAppearance = a.getResourceId(
+ R.styleable.TimePicker_headerTimeTextAppearance, 0);
+ if (timeHeaderTextAppearance != 0) {
+ final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
+ ATTRS_TEXT_COLOR, 0, timeHeaderTextAppearance);
+ final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
+ headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
+ textAppearance.recycle();
+ }
+
+ if (headerTextColor == null) {
+ headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor);
+ }
+
+ mTextInputPickerHeader = mainView.findViewById(R.id.input_header);
+
+ if (headerTextColor != null) {
+ mHourView.setTextColor(headerTextColor);
+ mSeparatorView.setTextColor(headerTextColor);
+ mMinuteView.setTextColor(headerTextColor);
+ mAmLabel.setTextColor(headerTextColor);
+ mPmLabel.setTextColor(headerTextColor);
+ }
+
+ // Set up header background, if available.
+ if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) {
+ mRadialTimePickerHeader.setBackground(a.getDrawable(
+ R.styleable.TimePicker_headerBackground));
+ mTextInputPickerHeader.setBackground(a.getDrawable(
+ R.styleable.TimePicker_headerBackground));
+ }
+
+ a.recycle();
+
+ mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById(R.id.radial_picker);
+ mRadialTimePickerView.applyAttributes(attrs, defStyleAttr, defStyleRes);
+ mRadialTimePickerView.setOnValueSelectedListener(mOnValueSelectedListener);
+
+ mTextInputPickerView = (TextInputTimePickerView) mainView.findViewById(R.id.input_mode);
+ mTextInputPickerView.setListener(mOnValueTypedListener);
+
+ mRadialTimePickerModeButton =
+ (ImageButton) mainView.findViewById(R.id.toggle_mode);
+ mRadialTimePickerModeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleRadialPickerMode();
+ }
+ });
+ mRadialTimePickerModeEnabledDescription = context.getResources().getString(
+ R.string.time_picker_radial_mode_description);
+ mTextInputPickerModeEnabledDescription = context.getResources().getString(
+ R.string.time_picker_text_input_mode_description);
+
+ mAllowAutoAdvance = true;
+
+ updateHourFormat();
+
+ // Initialize with current time.
+ mTempCalendar = Calendar.getInstance(mLocale);
+ final int currentHour = mTempCalendar.get(Calendar.HOUR_OF_DAY);
+ final int currentMinute = mTempCalendar.get(Calendar.MINUTE);
+ initialize(currentHour, currentMinute, mIs24Hour, HOUR_INDEX);
+ }
+
+ private void toggleRadialPickerMode() {
+ if (mRadialPickerModeEnabled) {
+ mRadialTimePickerView.setVisibility(View.GONE);
+ mRadialTimePickerHeader.setVisibility(View.GONE);
+ mTextInputPickerHeader.setVisibility(View.VISIBLE);
+ mTextInputPickerView.setVisibility(View.VISIBLE);
+ mRadialTimePickerModeButton.setImageResource(R.drawable.btn_clock_material);
+ mRadialTimePickerModeButton.setContentDescription(
+ mRadialTimePickerModeEnabledDescription);
+ mRadialPickerModeEnabled = false;
+ } else {
+ mRadialTimePickerView.setVisibility(View.VISIBLE);
+ mRadialTimePickerHeader.setVisibility(View.VISIBLE);
+ mTextInputPickerHeader.setVisibility(View.GONE);
+ mTextInputPickerView.setVisibility(View.GONE);
+ mRadialTimePickerModeButton.setImageResource(R.drawable.btn_keyboard_key_material);
+ mRadialTimePickerModeButton.setContentDescription(
+ mTextInputPickerModeEnabledDescription);
+ updateTextInputPicker();
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ }
+ mRadialPickerModeEnabled = true;
+ }
+ }
+
+ @Override
+ public boolean validateInput() {
+ return mTextInputPickerView.validateInput();
+ }
+
+ /**
+ * Ensures that a TextView is wide enough to contain its text without
+ * wrapping or clipping. Measures the specified view and sets the minimum
+ * width to the view's desired width.
+ *
+ * @param v the text view to measure
+ */
+ private static void ensureMinimumTextWidth(TextView v) {
+ v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ // Set both the TextView and the View version of minimum
+ // width because they are subtly different.
+ final int minWidth = v.getMeasuredWidth();
+ v.setMinWidth(minWidth);
+ v.setMinimumWidth(minWidth);
+ }
+
+ /**
+ * Updates hour formatting based on the current locale and 24-hour mode.
+ * <p>
+ * Determines how the hour should be formatted, sets member variables for
+ * leading zero and starting hour, and sets the hour view's presentation.
+ */
+ private void updateHourFormat() {
+ final String bestDateTimePattern = DateFormat.getBestDateTimePattern(
+ mLocale, mIs24Hour ? "Hm" : "hm");
+ final int lengthPattern = bestDateTimePattern.length();
+ boolean showLeadingZero = false;
+ char hourFormat = '\0';
+
+ for (int i = 0; i < lengthPattern; i++) {
+ final char c = bestDateTimePattern.charAt(i);
+ if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+ hourFormat = c;
+ if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+ showLeadingZero = true;
+ }
+ break;
+ }
+ }
+
+ mHourFormatShowLeadingZero = showLeadingZero;
+ mHourFormatStartsAtZero = hourFormat == 'K' || hourFormat == 'H';
+
+ // Update hour text field.
+ final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+ final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
+ mHourView.setRange(minHour, maxHour);
+ mHourView.setShowLeadingZeroes(mHourFormatShowLeadingZero);
+
+ final String[] digits = DecimalFormatSymbols.getInstance(mLocale).getDigitStrings();
+ int maxCharLength = 0;
+ for (int i = 0; i < 10; i++) {
+ maxCharLength = Math.max(maxCharLength, digits[i].length());
+ }
+ mTextInputPickerView.setHourFormat(maxCharLength * 2);
+ }
+
+ static final CharSequence obtainVerbatim(String text) {
+ return new SpannableStringBuilder().append(text,
+ new TtsSpan.VerbatimBuilder(text).build(), 0);
+ }
+
+ /**
+ * The legacy text color might have been poorly defined. Ensures that it
+ * has an appropriate activated state, using the selected state if one
+ * exists or modifying the default text color otherwise.
+ *
+ * @param color a legacy text color, or {@code null}
+ * @return a color state list with an appropriate activated state, or
+ * {@code null} if a valid activated state could not be generated
+ */
+ @Nullable
+ private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
+ if (color == null || color.hasState(R.attr.state_activated)) {
+ return color;
+ }
+
+ final int activatedColor;
+ final int defaultColor;
+ if (color.hasState(R.attr.state_selected)) {
+ activatedColor = color.getColorForState(StateSet.get(
+ StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
+ defaultColor = color.getColorForState(StateSet.get(
+ StateSet.VIEW_STATE_ENABLED), 0);
+ } else {
+ activatedColor = color.getDefaultColor();
+
+ // Generate a non-activated color using the disabled alpha.
+ final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
+ final float disabledAlpha = ta.getFloat(0, 0.30f);
+ defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
+ }
+
+ if (activatedColor == 0 || defaultColor == 0) {
+ // We somehow failed to obtain the colors.
+ return null;
+ }
+
+ final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
+ final int[] colors = new int[] { activatedColor, defaultColor };
+ return new ColorStateList(stateSet, colors);
+ }
+
+ private int multiplyAlphaComponent(int color, float alphaMod) {
+ final int srcRgb = color & 0xFFFFFF;
+ final int srcAlpha = (color >> 24) & 0xFF;
+ final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
+ return srcRgb | (dstAlpha << 24);
+ }
+
+ private static class ClickActionDelegate extends AccessibilityDelegate {
+ private final AccessibilityAction mClickAction;
+
+ public ClickActionDelegate(Context context, int resId) {
+ mClickAction = new AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId));
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ info.addAction(mClickAction);
+ }
+ }
+
+ private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) {
+ mCurrentHour = hourOfDay;
+ mCurrentMinute = minute;
+ mIs24Hour = is24HourView;
+ updateUI(index);
+ }
+
+ private void updateUI(int index) {
+ updateHeaderAmPm();
+ updateHeaderHour(mCurrentHour, false);
+ updateHeaderSeparator();
+ updateHeaderMinute(mCurrentMinute, false);
+ updateRadialPicker(index);
+ updateTextInputPicker();
+
+ mDelegator.invalidate();
+ }
+
+ private void updateTextInputPicker() {
+ mTextInputPickerView.updateTextInputValues(getLocalizedHour(mCurrentHour), mCurrentMinute,
+ mCurrentHour < 12 ? AM : PM, mIs24Hour, mHourFormatStartsAtZero);
+ }
+
+ private void updateRadialPicker(int index) {
+ mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour);
+ setCurrentItemShowing(index, false, true);
+ }
+
+ private void updateHeaderAmPm() {
+ if (mIs24Hour) {
+ mAmPmLayout.setVisibility(View.GONE);
+ } else {
+ // Find the location of AM/PM based on locale information.
+ final String dateTimePattern = DateFormat.getBestDateTimePattern(mLocale, "hm");
+ final boolean isAmPmAtStart = dateTimePattern.startsWith("a");
+ setAmPmStart(isAmPmAtStart);
+ updateAmPmLabelStates(mCurrentHour < 12 ? AM : PM);
+ }
+ }
+
+ private void setAmPmStart(boolean isAmPmAtStart) {
+ final RelativeLayout.LayoutParams params =
+ (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams();
+ if (params.getRule(RelativeLayout.RIGHT_OF) != 0
+ || params.getRule(RelativeLayout.LEFT_OF) != 0) {
+ // Horizontal mode, with AM/PM appearing to left/right of hours and minutes.
+ final boolean isAmPmAtLeft;
+ if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_LTR) {
+ isAmPmAtLeft = isAmPmAtStart;
+ } else {
+ isAmPmAtLeft = !isAmPmAtStart;
+ }
+ if (mIsAmPmAtLeft == isAmPmAtLeft) {
+ // AM/PM is already at the correct location. No change needed.
+ return;
+ }
+
+ if (isAmPmAtLeft) {
+ params.removeRule(RelativeLayout.RIGHT_OF);
+ params.addRule(RelativeLayout.LEFT_OF, mHourView.getId());
+ } else {
+ params.removeRule(RelativeLayout.LEFT_OF);
+ params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId());
+ }
+ mIsAmPmAtLeft = isAmPmAtLeft;
+ } else if (params.getRule(RelativeLayout.BELOW) != 0
+ || params.getRule(RelativeLayout.ABOVE) != 0) {
+ // Vertical mode, with AM/PM appearing to top/bottom of hours and minutes.
+ if (mIsAmPmAtTop == isAmPmAtStart) {
+ // AM/PM is already at the correct location. No change needed.
+ return;
+ }
+
+ final int otherViewId;
+ if (isAmPmAtStart) {
+ otherViewId = params.getRule(RelativeLayout.BELOW);
+ params.removeRule(RelativeLayout.BELOW);
+ params.addRule(RelativeLayout.ABOVE, otherViewId);
+ } else {
+ otherViewId = params.getRule(RelativeLayout.ABOVE);
+ params.removeRule(RelativeLayout.ABOVE);
+ params.addRule(RelativeLayout.BELOW, otherViewId);
+ }
+
+ // Switch the top and bottom paddings on the other view.
+ final View otherView = mRadialTimePickerHeader.findViewById(otherViewId);
+ final int top = otherView.getPaddingTop();
+ final int bottom = otherView.getPaddingBottom();
+ final int left = otherView.getPaddingLeft();
+ final int right = otherView.getPaddingRight();
+ otherView.setPadding(left, bottom, right, top);
+
+ mIsAmPmAtTop = isAmPmAtStart;
+ }
+
+ mAmPmLayout.setLayoutParams(params);
+ }
+
+ @Override
+ public void setDate(int hour, int minute) {
+ setHourInternal(hour, FROM_EXTERNAL_API, true, false);
+ setMinuteInternal(minute, FROM_EXTERNAL_API, false);
+
+ onTimeChanged();
+ }
+
+ /**
+ * Set the current hour.
+ */
+ @Override
+ public void setHour(int hour) {
+ setHourInternal(hour, FROM_EXTERNAL_API, true, true);
+ }
+
+ private void setHourInternal(int hour, @ChangeSource int source, boolean announce,
+ boolean notify) {
+ if (mCurrentHour == hour) {
+ return;
+ }
+
+ resetAutofilledValue();
+ mCurrentHour = hour;
+ updateHeaderHour(hour, announce);
+ updateHeaderAmPm();
+
+ if (source != FROM_RADIAL_PICKER) {
+ mRadialTimePickerView.setCurrentHour(hour);
+ mRadialTimePickerView.setAmOrPm(hour < 12 ? AM : PM);
+ }
+ if (source != FROM_INPUT_PICKER) {
+ updateTextInputPicker();
+ }
+
+ mDelegator.invalidate();
+ if (notify) {
+ onTimeChanged();
+ }
+ }
+
+ /**
+ * @return the current hour in the range (0-23)
+ */
+ @Override
+ public int getHour() {
+ final int currentHour = mRadialTimePickerView.getCurrentHour();
+ if (mIs24Hour) {
+ return currentHour;
+ }
+
+ if (mRadialTimePickerView.getAmOrPm() == PM) {
+ return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+ } else {
+ return currentHour % HOURS_IN_HALF_DAY;
+ }
+ }
+
+ /**
+ * Set the current minute (0-59).
+ */
+ @Override
+ public void setMinute(int minute) {
+ setMinuteInternal(minute, FROM_EXTERNAL_API, true);
+ }
+
+ private void setMinuteInternal(int minute, @ChangeSource int source, boolean notify) {
+ if (mCurrentMinute == minute) {
+ return;
+ }
+
+ resetAutofilledValue();
+ mCurrentMinute = minute;
+ updateHeaderMinute(minute, true);
+
+ if (source != FROM_RADIAL_PICKER) {
+ mRadialTimePickerView.setCurrentMinute(minute);
+ }
+ if (source != FROM_INPUT_PICKER) {
+ updateTextInputPicker();
+ }
+
+ mDelegator.invalidate();
+ if (notify) {
+ onTimeChanged();
+ }
+ }
+
+ /**
+ * @return The current minute.
+ */
+ @Override
+ public int getMinute() {
+ return mRadialTimePickerView.getCurrentMinute();
+ }
+
+ /**
+ * Sets whether time is displayed in 24-hour mode or 12-hour mode with
+ * AM/PM indicators.
+ *
+ * @param is24Hour {@code true} to display time in 24-hour mode or
+ * {@code false} for 12-hour mode with AM/PM
+ */
+ public void setIs24Hour(boolean is24Hour) {
+ if (mIs24Hour != is24Hour) {
+ mIs24Hour = is24Hour;
+ mCurrentHour = getHour();
+
+ updateHourFormat();
+ updateUI(mRadialTimePickerView.getCurrentItemShowing());
+ }
+ }
+
+ /**
+ * @return {@code true} if time is displayed in 24-hour mode, or
+ * {@code false} if time is displayed in 12-hour mode with AM/PM
+ * indicators
+ */
+ @Override
+ public boolean is24Hour() {
+ return mIs24Hour;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mHourView.setEnabled(enabled);
+ mMinuteView.setEnabled(enabled);
+ mAmLabel.setEnabled(enabled);
+ mPmLabel.setEnabled(enabled);
+ mRadialTimePickerView.setEnabled(enabled);
+ mIsEnabled = enabled;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ @Override
+ public int getBaseline() {
+ // does not support baseline alignment
+ return -1;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState(Parcelable superState) {
+ return new SavedState(superState, getHour(), getMinute(),
+ is24Hour(), getCurrentItemShowing());
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ final SavedState ss = (SavedState) state;
+ initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing());
+ mRadialTimePickerView.invalidate();
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ int flags = DateUtils.FORMAT_SHOW_TIME;
+ if (mIs24Hour) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ } else {
+ flags |= DateUtils.FORMAT_12HOUR;
+ }
+
+ mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
+ mTempCalendar.set(Calendar.MINUTE, getMinute());
+
+ final String selectedTime = DateUtils.formatDateTime(mContext,
+ mTempCalendar.getTimeInMillis(), flags);
+ final String selectionMode = mRadialTimePickerView.getCurrentItemShowing() == HOUR_INDEX ?
+ mSelectHours : mSelectMinutes;
+ event.getText().add(selectedTime + " " + selectionMode);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getHourView() {
+ return mHourView;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getMinuteView() {
+ return mMinuteView;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getAmView() {
+ return mAmLabel;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getPmView() {
+ return mPmLabel;
+ }
+
+ /**
+ * @return the index of the current item showing
+ */
+ private int getCurrentItemShowing() {
+ return mRadialTimePickerView.getCurrentItemShowing();
+ }
+
+ /**
+ * Propagate the time change
+ */
+ private void onTimeChanged() {
+ mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ if (mOnTimeChangedListener != null) {
+ mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+ }
+ if (mAutoFillChangeListener != null) {
+ mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
+ }
+ }
+
+ private void tryVibrate() {
+ mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ }
+
+ private void updateAmPmLabelStates(int amOrPm) {
+ final boolean isAm = amOrPm == AM;
+ mAmLabel.setActivated(isAm);
+ mAmLabel.setChecked(isAm);
+
+ final boolean isPm = amOrPm == PM;
+ mPmLabel.setActivated(isPm);
+ mPmLabel.setChecked(isPm);
+ }
+
+ /**
+ * Converts hour-of-day (0-23) time into a localized hour number.
+ * <p>
+ * The localized value may be in the range (0-23), (1-24), (0-11), or
+ * (1-12) depending on the locale. This method does not handle leading
+ * zeroes.
+ *
+ * @param hourOfDay the hour-of-day (0-23)
+ * @return a localized hour number
+ */
+ private int getLocalizedHour(int hourOfDay) {
+ if (!mIs24Hour) {
+ // Convert to hour-of-am-pm.
+ hourOfDay %= 12;
+ }
+
+ if (!mHourFormatStartsAtZero && hourOfDay == 0) {
+ // Convert to clock-hour (either of-day or of-am-pm).
+ hourOfDay = mIs24Hour ? 24 : 12;
+ }
+
+ return hourOfDay;
+ }
+
+ private void updateHeaderHour(int hourOfDay, boolean announce) {
+ final int localizedHour = getLocalizedHour(hourOfDay);
+ mHourView.setValue(localizedHour);
+
+ if (announce) {
+ tryAnnounceForAccessibility(mHourView.getText(), true);
+ }
+ }
+
+ private void updateHeaderMinute(int minuteOfHour, boolean announce) {
+ mMinuteView.setValue(minuteOfHour);
+
+ if (announce) {
+ tryAnnounceForAccessibility(mMinuteView.getText(), false);
+ }
+ }
+
+ /**
+ * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+ *
+ * See http://unicode.org/cldr/trac/browser/trunk/common/main
+ *
+ * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+ * separator as the character which is just after the hour marker in the returned pattern.
+ */
+ private void updateHeaderSeparator() {
+ final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+ (mIs24Hour) ? "Hm" : "hm");
+ final String separatorText;
+ // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
+ final char[] hourFormats = {'H', 'h', 'K', 'k'};
+ int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats);
+ if (hIndex == -1) {
+ // Default case
+ separatorText = ":";
+ } else {
+ separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
+ }
+ mSeparatorView.setText(separatorText);
+ mTextInputPickerView.updateSeparator(separatorText);
+ }
+
+ static private int lastIndexOfAny(String str, char[] any) {
+ final int lengthAny = any.length;
+ if (lengthAny > 0) {
+ for (int i = str.length() - 1; i >= 0; i--) {
+ char c = str.charAt(i);
+ for (int j = 0; j < lengthAny; j++) {
+ if (c == any[j]) {
+ return i;
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) {
+ if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) {
+ // TODO: Find a better solution, potentially live regions?
+ mDelegator.announceForAccessibility(text);
+ mLastAnnouncedText = text;
+ mLastAnnouncedIsHour = isHour;
+ }
+ }
+
+ /**
+ * Show either Hours or Minutes.
+ */
+ private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) {
+ mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
+
+ if (index == HOUR_INDEX) {
+ if (announce) {
+ mDelegator.announceForAccessibility(mSelectHours);
+ }
+ } else {
+ if (announce) {
+ mDelegator.announceForAccessibility(mSelectMinutes);
+ }
+ }
+
+ mHourView.setActivated(index == HOUR_INDEX);
+ mMinuteView.setActivated(index == MINUTE_INDEX);
+ }
+
+ private void setAmOrPm(int amOrPm) {
+ updateAmPmLabelStates(amOrPm);
+
+ if (mRadialTimePickerView.setAmOrPm(amOrPm)) {
+ mCurrentHour = getHour();
+ updateTextInputPicker();
+ if (mOnTimeChangedListener != null) {
+ mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+ }
+ }
+ }
+
+ /** Listener for RadialTimePickerView interaction. */
+ private final OnValueSelectedListener mOnValueSelectedListener = new OnValueSelectedListener() {
+ @Override
+ public void onValueSelected(int pickerType, int newValue, boolean autoAdvance) {
+ boolean valueChanged = false;
+ switch (pickerType) {
+ case RadialTimePickerView.HOURS:
+ if (getHour() != newValue) {
+ valueChanged = true;
+ }
+ final boolean isTransition = mAllowAutoAdvance && autoAdvance;
+ setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition, true);
+ if (isTransition) {
+ setCurrentItemShowing(MINUTE_INDEX, true, false);
+
+ final int localizedHour = getLocalizedHour(newValue);
+ mDelegator.announceForAccessibility(localizedHour + ". " + mSelectMinutes);
+ }
+ break;
+ case RadialTimePickerView.MINUTES:
+ if (getMinute() != newValue) {
+ valueChanged = true;
+ }
+ setMinuteInternal(newValue, FROM_RADIAL_PICKER, true);
+ break;
+ }
+
+ if (mOnTimeChangedListener != null && valueChanged) {
+ mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+ }
+ }
+ };
+
+ private final OnValueTypedListener mOnValueTypedListener = new OnValueTypedListener() {
+ @Override
+ public void onValueChanged(int pickerType, int newValue) {
+ switch (pickerType) {
+ case TextInputTimePickerView.HOURS:
+ setHourInternal(newValue, FROM_INPUT_PICKER, false, true);
+ break;
+ case TextInputTimePickerView.MINUTES:
+ setMinuteInternal(newValue, FROM_INPUT_PICKER, true);
+ break;
+ case TextInputTimePickerView.AMPM:
+ setAmOrPm(newValue);
+ break;
+ }
+ }
+ };
+
+ /** Listener for keyboard interaction. */
+ private final OnValueChangedListener mDigitEnteredListener = new OnValueChangedListener() {
+ @Override
+ public void onValueChanged(NumericTextView view, int value,
+ boolean isValid, boolean isFinished) {
+ final Runnable commitCallback;
+ final View nextFocusTarget;
+ if (view == mHourView) {
+ commitCallback = mCommitHour;
+ nextFocusTarget = view.isFocused() ? mMinuteView : null;
+ } else if (view == mMinuteView) {
+ commitCallback = mCommitMinute;
+ nextFocusTarget = null;
+ } else {
+ return;
+ }
+
+ view.removeCallbacks(commitCallback);
+
+ if (isValid) {
+ if (isFinished) {
+ // Done with hours entry, make visual updates
+ // immediately and move to next focus if needed.
+ commitCallback.run();
+
+ if (nextFocusTarget != null) {
+ nextFocusTarget.requestFocus();
+ }
+ } else {
+ // May still be making changes. Postpone visual
+ // updates to prevent distracting the user.
+ view.postDelayed(commitCallback, DELAY_COMMIT_MILLIS);
+ }
+ }
+ }
+ };
+
+ private final Runnable mCommitHour = new Runnable() {
+ @Override
+ public void run() {
+ setHour(mHourView.getValue());
+ }
+ };
+
+ private final Runnable mCommitMinute = new Runnable() {
+ @Override
+ public void run() {
+ setMinute(mMinuteView.getValue());
+ }
+ };
+
+ private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean focused) {
+ if (focused) {
+ switch (v.getId()) {
+ case R.id.am_label:
+ setAmOrPm(AM);
+ break;
+ case R.id.pm_label:
+ setAmOrPm(PM);
+ break;
+ case R.id.hours:
+ setCurrentItemShowing(HOUR_INDEX, true, true);
+ break;
+ case R.id.minutes:
+ setCurrentItemShowing(MINUTE_INDEX, true, true);
+ break;
+ default:
+ // Failed to handle this click, don't vibrate.
+ return;
+ }
+
+ tryVibrate();
+ }
+ }
+ };
+
+ private final View.OnClickListener mClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ final int amOrPm;
+ switch (v.getId()) {
+ case R.id.am_label:
+ setAmOrPm(AM);
+ break;
+ case R.id.pm_label:
+ setAmOrPm(PM);
+ break;
+ case R.id.hours:
+ setCurrentItemShowing(HOUR_INDEX, true, true);
+ break;
+ case R.id.minutes:
+ setCurrentItemShowing(MINUTE_INDEX, true, true);
+ break;
+ default:
+ // Failed to handle this click, don't vibrate.
+ return;
+ }
+
+ tryVibrate();
+ }
+ };
+
+ /**
+ * Delegates unhandled touches in a view group to the nearest child view.
+ */
+ private static class NearestTouchDelegate implements View.OnTouchListener {
+ private View mInitialTouchTarget;
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ final int actionMasked = motionEvent.getActionMasked();
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ if (view instanceof ViewGroup) {
+ mInitialTouchTarget = findNearestChild((ViewGroup) view,
+ (int) motionEvent.getX(), (int) motionEvent.getY());
+ } else {
+ mInitialTouchTarget = null;
+ }
+ }
+
+ final View child = mInitialTouchTarget;
+ if (child == null) {
+ return false;
+ }
+
+ final float offsetX = view.getScrollX() - child.getLeft();
+ final float offsetY = view.getScrollY() - child.getTop();
+ motionEvent.offsetLocation(offsetX, offsetY);
+ final boolean handled = child.dispatchTouchEvent(motionEvent);
+ motionEvent.offsetLocation(-offsetX, -offsetY);
+
+ if (actionMasked == MotionEvent.ACTION_UP
+ || actionMasked == MotionEvent.ACTION_CANCEL) {
+ mInitialTouchTarget = null;
+ }
+
+ return handled;
+ }
+
+ private View findNearestChild(ViewGroup v, int x, int y) {
+ View bestChild = null;
+ int bestDist = Integer.MAX_VALUE;
+
+ for (int i = 0, count = v.getChildCount(); i < count; i++) {
+ final View child = v.getChildAt(i);
+ final int dX = x - (child.getLeft() + child.getWidth() / 2);
+ final int dY = y - (child.getTop() + child.getHeight() / 2);
+ final int dist = dX * dX + dY * dY;
+ if (bestDist > dist) {
+ bestChild = child;
+ bestDist = dist;
+ }
+ }
+
+ return bestChild;
+ }
+ }
+}
diff --git a/android/widget/TimePickerSpinnerDelegate.java b/android/widget/TimePickerSpinnerDelegate.java
new file mode 100644
index 0000000..cc79b9c
--- /dev/null
+++ b/android/widget/TimePickerSpinnerDelegate.java
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2013 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 static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcelable;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Calendar;
+
+/**
+ * A delegate implementing the basic spinner-based TimePicker.
+ */
+class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
+ private static final boolean DEFAULT_ENABLED_STATE = true;
+ private static final int HOURS_IN_HALF_DAY = 12;
+
+ private final NumberPicker mHourSpinner;
+ private final NumberPicker mMinuteSpinner;
+ private final NumberPicker mAmPmSpinner;
+ private final EditText mHourSpinnerInput;
+ private final EditText mMinuteSpinnerInput;
+ private final EditText mAmPmSpinnerInput;
+ private final TextView mDivider;
+
+ // Note that the legacy implementation of the TimePicker is
+ // using a button for toggling between AM/PM while the new
+ // version uses a NumberPicker spinner. Therefore the code
+ // accommodates these two cases to be backwards compatible.
+ private final Button mAmPmButton;
+
+ private final String[] mAmPmStrings;
+
+ private final Calendar mTempCalendar;
+
+ private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+ private boolean mHourWithTwoDigit;
+ private char mHourFormat;
+
+ private boolean mIs24HourView;
+ private boolean mIsAm;
+
+ public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(delegator, context);
+
+ // process style attributes
+ final TypedArray a = mContext.obtainStyledAttributes(
+ attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+ final int layoutResourceId = a.getResourceId(
+ R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
+ a.recycle();
+
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ final View view = inflater.inflate(layoutResourceId, mDelegator, true);
+ view.setSaveFromParentEnabled(false);
+
+ // hour
+ mHourSpinner = delegator.findViewById(R.id.hour);
+ mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+ public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+ updateInputState();
+ if (!is24Hour()) {
+ if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
+ (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
+ mIsAm = !mIsAm;
+ updateAmPmControl();
+ }
+ }
+ onTimeChanged();
+ }
+ });
+ mHourSpinnerInput = mHourSpinner.findViewById(R.id.numberpicker_input);
+ mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+ // divider (only for the new widget style)
+ mDivider = mDelegator.findViewById(R.id.divider);
+ if (mDivider != null) {
+ setDividerText();
+ }
+
+ // minute
+ mMinuteSpinner = mDelegator.findViewById(R.id.minute);
+ mMinuteSpinner.setMinValue(0);
+ mMinuteSpinner.setMaxValue(59);
+ mMinuteSpinner.setOnLongPressUpdateInterval(100);
+ mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
+ mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+ public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+ updateInputState();
+ int minValue = mMinuteSpinner.getMinValue();
+ int maxValue = mMinuteSpinner.getMaxValue();
+ if (oldVal == maxValue && newVal == minValue) {
+ int newHour = mHourSpinner.getValue() + 1;
+ if (!is24Hour() && newHour == HOURS_IN_HALF_DAY) {
+ mIsAm = !mIsAm;
+ updateAmPmControl();
+ }
+ mHourSpinner.setValue(newHour);
+ } else if (oldVal == minValue && newVal == maxValue) {
+ int newHour = mHourSpinner.getValue() - 1;
+ if (!is24Hour() && newHour == HOURS_IN_HALF_DAY - 1) {
+ mIsAm = !mIsAm;
+ updateAmPmControl();
+ }
+ mHourSpinner.setValue(newHour);
+ }
+ onTimeChanged();
+ }
+ });
+ mMinuteSpinnerInput = mMinuteSpinner.findViewById(R.id.numberpicker_input);
+ mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+ // Get the localized am/pm strings and use them in the spinner.
+ mAmPmStrings = getAmPmStrings(context);
+
+ // am/pm
+ final View amPmView = mDelegator.findViewById(R.id.amPm);
+ if (amPmView instanceof Button) {
+ mAmPmSpinner = null;
+ mAmPmSpinnerInput = null;
+ mAmPmButton = (Button) amPmView;
+ mAmPmButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View button) {
+ button.requestFocus();
+ mIsAm = !mIsAm;
+ updateAmPmControl();
+ onTimeChanged();
+ }
+ });
+ } else {
+ mAmPmButton = null;
+ mAmPmSpinner = (NumberPicker) amPmView;
+ mAmPmSpinner.setMinValue(0);
+ mAmPmSpinner.setMaxValue(1);
+ mAmPmSpinner.setDisplayedValues(mAmPmStrings);
+ mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+ public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
+ updateInputState();
+ picker.requestFocus();
+ mIsAm = !mIsAm;
+ updateAmPmControl();
+ onTimeChanged();
+ }
+ });
+ mAmPmSpinnerInput = mAmPmSpinner.findViewById(R.id.numberpicker_input);
+ mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ }
+
+ if (isAmPmAtStart()) {
+ // Move the am/pm view to the beginning
+ ViewGroup amPmParent = delegator.findViewById(R.id.timePickerLayout);
+ amPmParent.removeView(amPmView);
+ amPmParent.addView(amPmView, 0);
+ // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
+ // for example and not for Holo Theme)
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
+ final int startMargin = lp.getMarginStart();
+ final int endMargin = lp.getMarginEnd();
+ if (startMargin != endMargin) {
+ lp.setMarginStart(endMargin);
+ lp.setMarginEnd(startMargin);
+ }
+ }
+
+ getHourFormatData();
+
+ // update controls to initial state
+ updateHourControl();
+ updateMinuteControl();
+ updateAmPmControl();
+
+ // set to current time
+ mTempCalendar = Calendar.getInstance(mLocale);
+ setHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
+ setMinute(mTempCalendar.get(Calendar.MINUTE));
+
+ if (!isEnabled()) {
+ setEnabled(false);
+ }
+
+ // set the content descriptions
+ setContentDescriptions();
+
+ // If not explicitly specified this view is important for accessibility.
+ if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ @Override
+ public boolean validateInput() {
+ return true;
+ }
+
+ private void getHourFormatData() {
+ final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+ (mIs24HourView) ? "Hm" : "hm");
+ final int lengthPattern = bestDateTimePattern.length();
+ mHourWithTwoDigit = false;
+ char hourFormat = '\0';
+ // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
+ // the hour format that we found.
+ for (int i = 0; i < lengthPattern; i++) {
+ final char c = bestDateTimePattern.charAt(i);
+ if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+ mHourFormat = c;
+ if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+ mHourWithTwoDigit = true;
+ }
+ break;
+ }
+ }
+ }
+
+ private boolean isAmPmAtStart() {
+ final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+ "hm" /* skeleton */);
+
+ return bestDateTimePattern.startsWith("a");
+ }
+
+ /**
+ * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+ *
+ * See http://unicode.org/cldr/trac/browser/trunk/common/main
+ *
+ * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+ * separator as the character which is just after the hour marker in the returned pattern.
+ */
+ private void setDividerText() {
+ final String skeleton = (mIs24HourView) ? "Hm" : "hm";
+ final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+ skeleton);
+ final String separatorText;
+ int hourIndex = bestDateTimePattern.lastIndexOf('H');
+ if (hourIndex == -1) {
+ hourIndex = bestDateTimePattern.lastIndexOf('h');
+ }
+ if (hourIndex == -1) {
+ // Default case
+ separatorText = ":";
+ } else {
+ int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
+ if (minuteIndex == -1) {
+ separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
+ } else {
+ separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
+ }
+ }
+ mDivider.setText(separatorText);
+ }
+
+ @Override
+ public void setDate(int hour, int minute) {
+ setCurrentHour(hour, false);
+ setCurrentMinute(minute, false);
+
+ onTimeChanged();
+ }
+
+ @Override
+ public void setHour(int hour) {
+ setCurrentHour(hour, true);
+ }
+
+ private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
+ // why was Integer used in the first place?
+ if (currentHour == getHour()) {
+ return;
+ }
+ resetAutofilledValue();
+ if (!is24Hour()) {
+ // convert [0,23] ordinal to wall clock display
+ if (currentHour >= HOURS_IN_HALF_DAY) {
+ mIsAm = false;
+ if (currentHour > HOURS_IN_HALF_DAY) {
+ currentHour = currentHour - HOURS_IN_HALF_DAY;
+ }
+ } else {
+ mIsAm = true;
+ if (currentHour == 0) {
+ currentHour = HOURS_IN_HALF_DAY;
+ }
+ }
+ updateAmPmControl();
+ }
+ mHourSpinner.setValue(currentHour);
+ if (notifyTimeChanged) {
+ onTimeChanged();
+ }
+ }
+
+ @Override
+ public int getHour() {
+ int currentHour = mHourSpinner.getValue();
+ if (is24Hour()) {
+ return currentHour;
+ } else if (mIsAm) {
+ return currentHour % HOURS_IN_HALF_DAY;
+ } else {
+ return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+ }
+ }
+
+ @Override
+ public void setMinute(int minute) {
+ setCurrentMinute(minute, true);
+ }
+
+ private void setCurrentMinute(int minute, boolean notifyTimeChanged) {
+ if (minute == getMinute()) {
+ return;
+ }
+ resetAutofilledValue();
+ mMinuteSpinner.setValue(minute);
+ if (notifyTimeChanged) {
+ onTimeChanged();
+ }
+ }
+
+ @Override
+ public int getMinute() {
+ return mMinuteSpinner.getValue();
+ }
+
+ public void setIs24Hour(boolean is24Hour) {
+ if (mIs24HourView == is24Hour) {
+ return;
+ }
+ // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
+ int currentHour = getHour();
+ // Order is important here.
+ mIs24HourView = is24Hour;
+ getHourFormatData();
+ updateHourControl();
+ // set value after spinner range is updated
+ setCurrentHour(currentHour, false);
+ updateMinuteControl();
+ updateAmPmControl();
+ }
+
+ @Override
+ public boolean is24Hour() {
+ return mIs24HourView;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mMinuteSpinner.setEnabled(enabled);
+ if (mDivider != null) {
+ mDivider.setEnabled(enabled);
+ }
+ mHourSpinner.setEnabled(enabled);
+ if (mAmPmSpinner != null) {
+ mAmPmSpinner.setEnabled(enabled);
+ } else {
+ mAmPmButton.setEnabled(enabled);
+ }
+ mIsEnabled = enabled;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ @Override
+ public int getBaseline() {
+ return mHourSpinner.getBaseline();
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState(Parcelable superState) {
+ return new SavedState(superState, getHour(), getMinute(), is24Hour());
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ final SavedState ss = (SavedState) state;
+ setHour(ss.getHour());
+ setMinute(ss.getMinute());
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ int flags = DateUtils.FORMAT_SHOW_TIME;
+ if (mIs24HourView) {
+ flags |= DateUtils.FORMAT_24HOUR;
+ } else {
+ flags |= DateUtils.FORMAT_12HOUR;
+ }
+ mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
+ mTempCalendar.set(Calendar.MINUTE, getMinute());
+ String selectedDateUtterance = DateUtils.formatDateTime(mContext,
+ mTempCalendar.getTimeInMillis(), flags);
+ event.getText().add(selectedDateUtterance);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getHourView() {
+ return mHourSpinnerInput;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getMinuteView() {
+ return mMinuteSpinnerInput;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getAmView() {
+ return mAmPmSpinnerInput;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public View getPmView() {
+ return mAmPmSpinnerInput;
+ }
+
+ private void updateInputState() {
+ // Make sure that if the user changes the value and the IME is active
+ // for one of the inputs if this widget, the IME is closed. If the user
+ // changed the value via the IME and there is a next input the IME will
+ // be shown, otherwise the user chose another means of changing the
+ // value and having the IME up makes no sense.
+ InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+ if (inputMethodManager != null) {
+ if (inputMethodManager.isActive(mHourSpinnerInput)) {
+ mHourSpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
+ mMinuteSpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
+ mAmPmSpinnerInput.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+ }
+ }
+ }
+
+ private void updateAmPmControl() {
+ if (is24Hour()) {
+ if (mAmPmSpinner != null) {
+ mAmPmSpinner.setVisibility(View.GONE);
+ } else {
+ mAmPmButton.setVisibility(View.GONE);
+ }
+ } else {
+ int index = mIsAm ? Calendar.AM : Calendar.PM;
+ if (mAmPmSpinner != null) {
+ mAmPmSpinner.setValue(index);
+ mAmPmSpinner.setVisibility(View.VISIBLE);
+ } else {
+ mAmPmButton.setText(mAmPmStrings[index]);
+ mAmPmButton.setVisibility(View.VISIBLE);
+ }
+ }
+ mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ }
+
+ private void onTimeChanged() {
+ mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ if (mOnTimeChangedListener != null) {
+ mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(),
+ getMinute());
+ }
+ if (mAutoFillChangeListener != null) {
+ mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
+ }
+ }
+
+ private void updateHourControl() {
+ if (is24Hour()) {
+ // 'k' means 1-24 hour
+ if (mHourFormat == 'k') {
+ mHourSpinner.setMinValue(1);
+ mHourSpinner.setMaxValue(24);
+ } else {
+ mHourSpinner.setMinValue(0);
+ mHourSpinner.setMaxValue(23);
+ }
+ } else {
+ // 'K' means 0-11 hour
+ if (mHourFormat == 'K') {
+ mHourSpinner.setMinValue(0);
+ mHourSpinner.setMaxValue(11);
+ } else {
+ mHourSpinner.setMinValue(1);
+ mHourSpinner.setMaxValue(12);
+ }
+ }
+ mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
+ }
+
+ private void updateMinuteControl() {
+ if (is24Hour()) {
+ mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ } else {
+ mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+ }
+ }
+
+ private void setContentDescriptions() {
+ // Minute
+ trySetContentDescription(mMinuteSpinner, R.id.increment,
+ R.string.time_picker_increment_minute_button);
+ trySetContentDescription(mMinuteSpinner, R.id.decrement,
+ R.string.time_picker_decrement_minute_button);
+ // Hour
+ trySetContentDescription(mHourSpinner, R.id.increment,
+ R.string.time_picker_increment_hour_button);
+ trySetContentDescription(mHourSpinner, R.id.decrement,
+ R.string.time_picker_decrement_hour_button);
+ // AM/PM
+ if (mAmPmSpinner != null) {
+ trySetContentDescription(mAmPmSpinner, R.id.increment,
+ R.string.time_picker_increment_set_pm_button);
+ trySetContentDescription(mAmPmSpinner, R.id.decrement,
+ R.string.time_picker_decrement_set_am_button);
+ }
+ }
+
+ private void trySetContentDescription(View root, int viewId, int contDescResId) {
+ View target = root.findViewById(viewId);
+ if (target != null) {
+ target.setContentDescription(mContext.getString(contDescResId));
+ }
+ }
+
+ public static String[] getAmPmStrings(Context context) {
+ String[] result = new String[2];
+ LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
+ result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
+ result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
+ return result;
+ }
+}
diff --git a/android/widget/Toast.java b/android/widget/Toast.java
new file mode 100644
index 0000000..d807120
--- /dev/null
+++ b/android/widget/Toast.java
@@ -0,0 +1,535 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.app.INotificationManager;
+import android.app.ITransientNotification;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+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.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * 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.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about creating Toast notifications, read the
+ * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer
+ * guide.</p>
+ * </div>
+ */
+public class Toast {
+ static final String TAG = "Toast";
+ static final boolean localLOGV = false;
+
+ /** @hide */
+ @IntDef({LENGTH_SHORT, LENGTH_LONG})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Duration {}
+
+ /**
+ * 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;
+ 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) {
+ this(context, null);
+ }
+
+ /**
+ * Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
+ * @hide
+ */
+ public Toast(@NonNull Context context, @Nullable Looper looper) {
+ mContext = context;
+ mTN = new TN(context.getPackageName(), looper);
+ mTN.mY = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.toast_y_offset);
+ mTN.mGravity = context.getResources().getInteger(
+ com.android.internal.R.integer.config_toastDefaultGravity);
+ }
+
+ /**
+ * 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.getOpPackageName();
+ TN tn = mTN;
+ tn.mNextView = mNextView;
+
+ 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.cancel();
+ }
+
+ /**
+ * 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(@Duration int duration) {
+ mDuration = duration;
+ mTN.mDuration = duration;
+ }
+
+ /**
+ * Return the duration.
+ * @see #setDuration
+ */
+ @Duration
+ 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) {
+ mTN.mHorizontalMargin = horizontalMargin;
+ mTN.mVerticalMargin = verticalMargin;
+ }
+
+ /**
+ * Return the horizontal margin.
+ */
+ public float getHorizontalMargin() {
+ return mTN.mHorizontalMargin;
+ }
+
+ /**
+ * Return the vertical margin.
+ */
+ public float getVerticalMargin() {
+ return mTN.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) {
+ mTN.mGravity = gravity;
+ mTN.mX = xOffset;
+ mTN.mY = yOffset;
+ }
+
+ /**
+ * Get the location at which the notification should appear on the screen.
+ * @see android.view.Gravity
+ * @see #getGravity
+ */
+ public int getGravity() {
+ return mTN.mGravity;
+ }
+
+ /**
+ * Return the X offset in pixels to apply to the gravity's location.
+ */
+ public int getXOffset() {
+ return mTN.mX;
+ }
+
+ /**
+ * Return the Y offset in pixels to apply to the gravity's location.
+ */
+ public int getYOffset() {
+ return mTN.mY;
+ }
+
+ /**
+ * Gets the LayoutParams for the Toast window.
+ * @hide
+ */
+ public WindowManager.LayoutParams getWindowParams() {
+ return mTN.mParams;
+ }
+
+ /**
+ * 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, @Duration int duration) {
+ return makeText(context, null, text, duration);
+ }
+
+ /**
+ * Make a standard toast to display using the specified looper.
+ * If looper is null, Looper.myLooper() is used.
+ * @hide
+ */
+ public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
+ @NonNull CharSequence text, @Duration int duration) {
+ Toast result = new Toast(context, looper);
+
+ 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, @StringRes int resId, @Duration 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(@StringRes 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 = 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 static class TN extends ITransientNotification.Stub {
+ private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+
+ private static final int SHOW = 0;
+ private static final int HIDE = 1;
+ private static final int CANCEL = 2;
+ final Handler mHandler;
+
+ int mGravity;
+ int mX, mY;
+ float mHorizontalMargin;
+ float mVerticalMargin;
+
+
+ View mView;
+ View mNextView;
+ int mDuration;
+
+ WindowManager mWM;
+
+ String mPackageName;
+
+ static final long SHORT_DURATION_TIMEOUT = 4000;
+ static final long LONG_DURATION_TIMEOUT = 7000;
+
+ TN(String packageName, @Nullable Looper looper) {
+ // XXX This should be changed to use a Dialog, with a Theme.Toast
+ // defined that sets up the layout params appropriately.
+ final WindowManager.LayoutParams params = mParams;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.format = PixelFormat.TRANSLUCENT;
+ params.windowAnimations = com.android.internal.R.style.Animation_Toast;
+ params.type = WindowManager.LayoutParams.TYPE_TOAST;
+ params.setTitle("Toast");
+ params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+
+ mPackageName = packageName;
+
+ if (looper == null) {
+ // Use Looper.myLooper() if looper is not specified.
+ looper = Looper.myLooper();
+ if (looper == null) {
+ throw new RuntimeException(
+ "Can't toast on a thread that has not called Looper.prepare()");
+ }
+ }
+ mHandler = new Handler(looper, null) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW: {
+ IBinder token = (IBinder) msg.obj;
+ handleShow(token);
+ break;
+ }
+ case HIDE: {
+ handleHide();
+ // Don't do this in handleHide() because it is also invoked by
+ // handleShow()
+ mNextView = null;
+ break;
+ }
+ case CANCEL: {
+ handleHide();
+ // Don't do this in handleHide() because it is also invoked by
+ // handleShow()
+ mNextView = null;
+ try {
+ getService().cancelToast(mPackageName, TN.this);
+ } catch (RemoteException e) {
+ }
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * schedule handleShow into the right thread
+ */
+ @Override
+ public void show(IBinder windowToken) {
+ if (localLOGV) Log.v(TAG, "SHOW: " + this);
+ mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
+ }
+
+ /**
+ * schedule handleHide into the right thread
+ */
+ @Override
+ public void hide() {
+ if (localLOGV) Log.v(TAG, "HIDE: " + this);
+ mHandler.obtainMessage(HIDE).sendToTarget();
+ }
+
+ public void cancel() {
+ if (localLOGV) Log.v(TAG, "CANCEL: " + this);
+ mHandler.obtainMessage(CANCEL).sendToTarget();
+ }
+
+ public void handleShow(IBinder windowToken) {
+ if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ + " mNextView=" + mNextView);
+ // If a cancel/hide is pending - no need to show - at this point
+ // the window token is already invalid and no need to do any work.
+ if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
+ return;
+ }
+ if (mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ Context context = mView.getContext().getApplicationContext();
+ String packageName = mView.getContext().getOpPackageName();
+ if (context == null) {
+ context = mView.getContext();
+ }
+ mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ // We can resolve the Gravity here by using the Locale for getting
+ // the layout direction
+ final Configuration config = mView.getContext().getResources().getConfiguration();
+ final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
+ 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;
+ mParams.packageName = packageName;
+ mParams.hideTimeoutMilliseconds = mDuration ==
+ Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
+ mParams.token = windowToken;
+ 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);
+ // Since the notification manager service cancels the token right
+ // after it notifies us to cancel the toast there is an inherent
+ // race and we may attempt to add a window after the token has been
+ // invalidated. Let us hedge against that.
+ try {
+ mWM.addView(mView, mParams);
+ trySendAccessibilityEvent();
+ } catch (WindowManager.BadTokenException e) {
+ /* ignore */
+ }
+ }
+ }
+
+ private void trySendAccessibilityEvent() {
+ AccessibilityManager accessibilityManager =
+ AccessibilityManager.getInstance(mView.getContext());
+ if (!accessibilityManager.isEnabled()) {
+ return;
+ }
+ // treat toasts as notifications since they are used to
+ // announce a transient piece of information to the user
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ event.setClassName(getClass().getName());
+ event.setPackageName(mView.getContext().getPackageName());
+ mView.dispatchPopulateAccessibilityEvent(event);
+ accessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ 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.removeViewImmediate(mView);
+ }
+
+ mView = null;
+ }
+ }
+ }
+}
diff --git a/android/widget/ToggleButton.java b/android/widget/ToggleButton.java
new file mode 100644
index 0000000..6a8449e
--- /dev/null
+++ b/android/widget/ToggleButton.java
@@ -0,0 +1,158 @@
+/*
+ * 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".
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
+ * guide.</p>
+ *
+ * @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 defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.ToggleButton, defStyleAttr, defStyleRes);
+ 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, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ 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);
+ } else {
+ mIndicatorDrawable = null;
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mIndicatorDrawable != null) {
+ mIndicatorDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ToggleButton.class.getName();
+ }
+}
diff --git a/android/widget/Toolbar.java b/android/widget/Toolbar.java
new file mode 100644
index 0000000..bf3085d
--- /dev/null
+++ b/android/widget/Toolbar.java
@@ -0,0 +1,2400 @@
+/*
+ * Copyright (C) 2014 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.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.MenuRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.annotation.TestApi;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.CollapsibleActionView;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.SubMenuBuilder;
+import com.android.internal.widget.DecorToolbar;
+import com.android.internal.widget.ToolbarWidgetWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A standard toolbar for use within application content.
+ *
+ * <p>A Toolbar is a generalization of {@link android.app.ActionBar action bars} for use
+ * within application layouts. While an action bar is traditionally part of an
+ * {@link android.app.Activity Activity's} opaque window decor controlled by the framework,
+ * a Toolbar may be placed at any arbitrary level of nesting within a view hierarchy.
+ * An application may choose to designate a Toolbar as the action bar for an Activity
+ * using the {@link android.app.Activity#setActionBar(Toolbar) setActionBar()} method.</p>
+ *
+ * <p>Toolbar supports a more focused feature set than ActionBar. From start to end, a toolbar
+ * may contain a combination of the following optional elements:
+ *
+ * <ul>
+ * <li><em>A navigation button.</em> This may be an Up arrow, navigation menu toggle, close,
+ * collapse, done or another glyph of the app's choosing. This button should always be used
+ * to access other navigational destinations within the container of the Toolbar and
+ * its signified content or otherwise leave the current context signified by the Toolbar.
+ * The navigation button is vertically aligned within the Toolbar's
+ * {@link android.R.styleable#View_minHeight minimum height}, if set.</li>
+ * <li><em>A branded logo image.</em> This may extend to the height of the bar and can be
+ * arbitrarily wide.</li>
+ * <li><em>A title and subtitle.</em> The title should be a signpost for the Toolbar's current
+ * position in the navigation hierarchy and the content contained there. The subtitle,
+ * if present should indicate any extended information about the current content.
+ * If an app uses a logo image it should strongly consider omitting a title and subtitle.</li>
+ * <li><em>One or more custom views.</em> The application may add arbitrary child views
+ * to the Toolbar. They will appear at this position within the layout. If a child view's
+ * {@link LayoutParams} indicates a {@link Gravity} value of
+ * {@link Gravity#CENTER_HORIZONTAL CENTER_HORIZONTAL} the view will attempt to center
+ * within the available space remaining in the Toolbar after all other elements have been
+ * measured.</li>
+ * <li><em>An {@link ActionMenuView action menu}.</em> The menu of actions will pin to the
+ * end of the Toolbar offering a few
+ * <a href="http://developer.android.com/design/patterns/actionbar.html#ActionButtons">
+ * frequent, important or typical</a> actions along with an optional overflow menu for
+ * additional actions. Action buttons are vertically aligned within the Toolbar's
+ * {@link android.R.styleable#View_minHeight minimum height}, if set.</li>
+ * </ul>
+ * </p>
+ *
+ * <p>In modern Android UIs developers should lean more on a visually distinct color scheme for
+ * toolbars than on their application icon. The use of application icon plus title as a standard
+ * layout is discouraged on API 21 devices and newer.</p>
+ *
+ * @attr ref android.R.styleable#Toolbar_buttonGravity
+ * @attr ref android.R.styleable#Toolbar_collapseContentDescription
+ * @attr ref android.R.styleable#Toolbar_collapseIcon
+ * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+ * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+ * @attr ref android.R.styleable#Toolbar_contentInsetRight
+ * @attr ref android.R.styleable#Toolbar_contentInsetStart
+ * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+ * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+ * @attr ref android.R.styleable#Toolbar_gravity
+ * @attr ref android.R.styleable#Toolbar_logo
+ * @attr ref android.R.styleable#Toolbar_logoDescription
+ * @attr ref android.R.styleable#Toolbar_maxButtonHeight
+ * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+ * @attr ref android.R.styleable#Toolbar_navigationIcon
+ * @attr ref android.R.styleable#Toolbar_popupTheme
+ * @attr ref android.R.styleable#Toolbar_subtitle
+ * @attr ref android.R.styleable#Toolbar_subtitleTextAppearance
+ * @attr ref android.R.styleable#Toolbar_subtitleTextColor
+ * @attr ref android.R.styleable#Toolbar_title
+ * @attr ref android.R.styleable#Toolbar_titleMargin
+ * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+ * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+ * @attr ref android.R.styleable#Toolbar_titleMarginStart
+ * @attr ref android.R.styleable#Toolbar_titleMarginTop
+ * @attr ref android.R.styleable#Toolbar_titleTextAppearance
+ * @attr ref android.R.styleable#Toolbar_titleTextColor
+ */
+public class Toolbar extends ViewGroup {
+ private static final String TAG = "Toolbar";
+
+ private ActionMenuView mMenuView;
+ private TextView mTitleTextView;
+ private TextView mSubtitleTextView;
+ private ImageButton mNavButtonView;
+ private ImageView mLogoView;
+
+ private Drawable mCollapseIcon;
+ private CharSequence mCollapseDescription;
+ private ImageButton mCollapseButtonView;
+ View mExpandedActionView;
+
+ /** Context against which to inflate popup menus. */
+ private Context mPopupContext;
+
+ /** Theme resource against which to inflate popup menus. */
+ private int mPopupTheme;
+
+ private int mTitleTextAppearance;
+ private int mSubtitleTextAppearance;
+ private int mNavButtonStyle;
+
+ private int mButtonGravity;
+
+ private int mMaxButtonHeight;
+
+ private int mTitleMarginStart;
+ private int mTitleMarginEnd;
+ private int mTitleMarginTop;
+ private int mTitleMarginBottom;
+
+ private RtlSpacingHelper mContentInsets;
+ private int mContentInsetStartWithNavigation;
+ private int mContentInsetEndWithActions;
+
+ private int mGravity = Gravity.START | Gravity.CENTER_VERTICAL;
+
+ private CharSequence mTitleText;
+ private CharSequence mSubtitleText;
+
+ private int mTitleTextColor;
+ private int mSubtitleTextColor;
+
+ private boolean mEatingTouch;
+
+ // Clear me after use.
+ private final ArrayList<View> mTempViews = new ArrayList<View>();
+
+ // Used to hold views that will be removed while we have an expanded action view.
+ private final ArrayList<View> mHiddenViews = new ArrayList<>();
+
+ private final int[] mTempMargins = new int[2];
+
+ private OnMenuItemClickListener mOnMenuItemClickListener;
+
+ private final ActionMenuView.OnMenuItemClickListener mMenuViewItemClickListener =
+ new ActionMenuView.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mOnMenuItemClickListener != null) {
+ return mOnMenuItemClickListener.onMenuItemClick(item);
+ }
+ return false;
+ }
+ };
+
+ private ToolbarWidgetWrapper mWrapper;
+ private ActionMenuPresenter mOuterActionMenuPresenter;
+ private ExpandedActionViewMenuPresenter mExpandedMenuPresenter;
+ private MenuPresenter.Callback mActionMenuPresenterCallback;
+ private MenuBuilder.Callback mMenuBuilderCallback;
+
+ private boolean mCollapsible;
+
+ private final Runnable mShowOverflowMenuRunnable = new Runnable() {
+ @Override public void run() {
+ showOverflowMenu();
+ }
+ };
+
+ public Toolbar(Context context) {
+ this(context, null);
+ }
+
+ public Toolbar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.toolbarStyle);
+ }
+
+ public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar,
+ defStyleAttr, defStyleRes);
+
+ mTitleTextAppearance = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, 0);
+ mSubtitleTextAppearance = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, 0);
+ mNavButtonStyle = a.getResourceId(R.styleable.Toolbar_navigationButtonStyle, 0);
+ mGravity = a.getInteger(R.styleable.Toolbar_gravity, mGravity);
+ mButtonGravity = a.getInteger(R.styleable.Toolbar_buttonGravity, Gravity.TOP);
+ mTitleMarginStart = mTitleMarginEnd = mTitleMarginTop = mTitleMarginBottom =
+ a.getDimensionPixelOffset(R.styleable.Toolbar_titleMargin, 0);
+
+ final int marginStart = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginStart, -1);
+ if (marginStart >= 0) {
+ mTitleMarginStart = marginStart;
+ }
+
+ final int marginEnd = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginEnd, -1);
+ if (marginEnd >= 0) {
+ mTitleMarginEnd = marginEnd;
+ }
+
+ final int marginTop = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginTop, -1);
+ if (marginTop >= 0) {
+ mTitleMarginTop = marginTop;
+ }
+
+ final int marginBottom = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginBottom,
+ -1);
+ if (marginBottom >= 0) {
+ mTitleMarginBottom = marginBottom;
+ }
+
+ mMaxButtonHeight = a.getDimensionPixelSize(R.styleable.Toolbar_maxButtonHeight, -1);
+
+ final int contentInsetStart =
+ a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetStart,
+ RtlSpacingHelper.UNDEFINED);
+ final int contentInsetEnd =
+ a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetEnd,
+ RtlSpacingHelper.UNDEFINED);
+ final int contentInsetLeft =
+ a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetLeft, 0);
+ final int contentInsetRight =
+ a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetRight, 0);
+
+ ensureContentInsets();
+ mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight);
+
+ if (contentInsetStart != RtlSpacingHelper.UNDEFINED ||
+ contentInsetEnd != RtlSpacingHelper.UNDEFINED) {
+ mContentInsets.setRelative(contentInsetStart, contentInsetEnd);
+ }
+
+ mContentInsetStartWithNavigation = a.getDimensionPixelOffset(
+ R.styleable.Toolbar_contentInsetStartWithNavigation, RtlSpacingHelper.UNDEFINED);
+ mContentInsetEndWithActions = a.getDimensionPixelOffset(
+ R.styleable.Toolbar_contentInsetEndWithActions, RtlSpacingHelper.UNDEFINED);
+
+ mCollapseIcon = a.getDrawable(R.styleable.Toolbar_collapseIcon);
+ mCollapseDescription = a.getText(R.styleable.Toolbar_collapseContentDescription);
+
+ final CharSequence title = a.getText(R.styleable.Toolbar_title);
+ if (!TextUtils.isEmpty(title)) {
+ setTitle(title);
+ }
+
+ final CharSequence subtitle = a.getText(R.styleable.Toolbar_subtitle);
+ if (!TextUtils.isEmpty(subtitle)) {
+ setSubtitle(subtitle);
+ }
+
+ // Set the default context, since setPopupTheme() may be a no-op.
+ mPopupContext = mContext;
+ setPopupTheme(a.getResourceId(R.styleable.Toolbar_popupTheme, 0));
+
+ final Drawable navIcon = a.getDrawable(R.styleable.Toolbar_navigationIcon);
+ if (navIcon != null) {
+ setNavigationIcon(navIcon);
+ }
+
+ final CharSequence navDesc = a.getText(
+ R.styleable.Toolbar_navigationContentDescription);
+ if (!TextUtils.isEmpty(navDesc)) {
+ setNavigationContentDescription(navDesc);
+ }
+
+ final Drawable logo = a.getDrawable(R.styleable.Toolbar_logo);
+ if (logo != null) {
+ setLogo(logo);
+ }
+
+ final CharSequence logoDesc = a.getText(R.styleable.Toolbar_logoDescription);
+ if (!TextUtils.isEmpty(logoDesc)) {
+ setLogoDescription(logoDesc);
+ }
+
+ if (a.hasValue(R.styleable.Toolbar_titleTextColor)) {
+ setTitleTextColor(a.getColor(R.styleable.Toolbar_titleTextColor, 0xffffffff));
+ }
+
+ if (a.hasValue(R.styleable.Toolbar_subtitleTextColor)) {
+ setSubtitleTextColor(a.getColor(R.styleable.Toolbar_subtitleTextColor, 0xffffffff));
+ }
+ a.recycle();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // If the container is a cluster, unmark itself as a cluster to avoid having nested
+ // clusters.
+ ViewParent parent = getParent();
+ while (parent != null && parent instanceof ViewGroup) {
+ final ViewGroup vgParent = (ViewGroup) parent;
+ if (vgParent.isKeyboardNavigationCluster()) {
+ setKeyboardNavigationCluster(false);
+ if (vgParent.getTouchscreenBlocksFocus()) {
+ setTouchscreenBlocksFocus(false);
+ }
+ break;
+ }
+ parent = vgParent.getParent();
+ }
+ }
+
+ /**
+ * Specifies the theme to use when inflating popup menus. By default, uses
+ * the same theme as the toolbar itself.
+ *
+ * @param resId theme used to inflate popup menus
+ * @see #getPopupTheme()
+ */
+ public void setPopupTheme(@StyleRes int resId) {
+ if (mPopupTheme != resId) {
+ mPopupTheme = resId;
+ if (resId == 0) {
+ mPopupContext = mContext;
+ } else {
+ mPopupContext = new ContextThemeWrapper(mContext, resId);
+ }
+ }
+ }
+
+ /**
+ * @return resource identifier of the theme used to inflate popup menus, or
+ * 0 if menus are inflated against the toolbar theme
+ * @see #setPopupTheme(int)
+ */
+ public int getPopupTheme() {
+ return mPopupTheme;
+ }
+
+ /**
+ * Sets the title margin.
+ *
+ * @param start the starting title margin in pixels
+ * @param top the top title margin in pixels
+ * @param end the ending title margin in pixels
+ * @param bottom the bottom title margin in pixels
+ * @see #getTitleMarginStart()
+ * @see #getTitleMarginTop()
+ * @see #getTitleMarginEnd()
+ * @see #getTitleMarginBottom()
+ * @attr ref android.R.styleable#Toolbar_titleMargin
+ */
+ public void setTitleMargin(int start, int top, int end, int bottom) {
+ mTitleMarginStart = start;
+ mTitleMarginTop = top;
+ mTitleMarginEnd = end;
+ mTitleMarginBottom = bottom;
+
+ requestLayout();
+ }
+
+ /**
+ * @return the starting title margin in pixels
+ * @see #setTitleMarginStart(int)
+ * @attr ref android.R.styleable#Toolbar_titleMarginStart
+ */
+ public int getTitleMarginStart() {
+ return mTitleMarginStart;
+ }
+
+ /**
+ * Sets the starting title margin in pixels.
+ *
+ * @param margin the starting title margin in pixels
+ * @see #getTitleMarginStart()
+ * @attr ref android.R.styleable#Toolbar_titleMarginStart
+ */
+ public void setTitleMarginStart(int margin) {
+ mTitleMarginStart = margin;
+
+ requestLayout();
+ }
+
+ /**
+ * @return the top title margin in pixels
+ * @see #setTitleMarginTop(int)
+ * @attr ref android.R.styleable#Toolbar_titleMarginTop
+ */
+ public int getTitleMarginTop() {
+ return mTitleMarginTop;
+ }
+
+ /**
+ * Sets the top title margin in pixels.
+ *
+ * @param margin the top title margin in pixels
+ * @see #getTitleMarginTop()
+ * @attr ref android.R.styleable#Toolbar_titleMarginTop
+ */
+ public void setTitleMarginTop(int margin) {
+ mTitleMarginTop = margin;
+
+ requestLayout();
+ }
+
+ /**
+ * @return the ending title margin in pixels
+ * @see #setTitleMarginEnd(int)
+ * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+ */
+ public int getTitleMarginEnd() {
+ return mTitleMarginEnd;
+ }
+
+ /**
+ * Sets the ending title margin in pixels.
+ *
+ * @param margin the ending title margin in pixels
+ * @see #getTitleMarginEnd()
+ * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+ */
+ public void setTitleMarginEnd(int margin) {
+ mTitleMarginEnd = margin;
+
+ requestLayout();
+ }
+
+ /**
+ * @return the bottom title margin in pixels
+ * @see #setTitleMarginBottom(int)
+ * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+ */
+ public int getTitleMarginBottom() {
+ return mTitleMarginBottom;
+ }
+
+ /**
+ * Sets the bottom title margin in pixels.
+ *
+ * @param margin the bottom title margin in pixels
+ * @see #getTitleMarginBottom()
+ * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+ */
+ public void setTitleMarginBottom(int margin) {
+ mTitleMarginBottom = margin;
+ requestLayout();
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+ super.onRtlPropertiesChanged(layoutDirection);
+ ensureContentInsets();
+ mContentInsets.setDirection(layoutDirection == LAYOUT_DIRECTION_RTL);
+ }
+
+ /**
+ * Set a logo drawable from a resource id.
+ *
+ * <p>This drawable should generally take the place of title text. The logo cannot be
+ * clicked. Apps using a logo should also supply a description using
+ * {@link #setLogoDescription(int)}.</p>
+ *
+ * @param resId ID of a drawable resource
+ */
+ public void setLogo(@DrawableRes int resId) {
+ setLogo(getContext().getDrawable(resId));
+ }
+
+ /** @hide */
+ public boolean canShowOverflowMenu() {
+ return getVisibility() == VISIBLE && mMenuView != null && mMenuView.isOverflowReserved();
+ }
+
+ /**
+ * Check whether the overflow menu is currently showing. This may not reflect
+ * a pending show operation in progress.
+ *
+ * @return true if the overflow menu is currently showing
+ */
+ public boolean isOverflowMenuShowing() {
+ return mMenuView != null && mMenuView.isOverflowMenuShowing();
+ }
+
+ /** @hide */
+ public boolean isOverflowMenuShowPending() {
+ return mMenuView != null && mMenuView.isOverflowMenuShowPending();
+ }
+
+ /**
+ * Show the overflow items from the associated menu.
+ *
+ * @return true if the menu was able to be shown, false otherwise
+ */
+ public boolean showOverflowMenu() {
+ return mMenuView != null && mMenuView.showOverflowMenu();
+ }
+
+ /**
+ * Hide the overflow items from the associated menu.
+ *
+ * @return true if the menu was able to be hidden, false otherwise
+ */
+ public boolean hideOverflowMenu() {
+ return mMenuView != null && mMenuView.hideOverflowMenu();
+ }
+
+ /** @hide */
+ public void setMenu(MenuBuilder menu, ActionMenuPresenter outerPresenter) {
+ if (menu == null && mMenuView == null) {
+ return;
+ }
+
+ ensureMenuView();
+ final MenuBuilder oldMenu = mMenuView.peekMenu();
+ if (oldMenu == menu) {
+ return;
+ }
+
+ if (oldMenu != null) {
+ oldMenu.removeMenuPresenter(mOuterActionMenuPresenter);
+ oldMenu.removeMenuPresenter(mExpandedMenuPresenter);
+ }
+
+ if (mExpandedMenuPresenter == null) {
+ mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+ }
+
+ outerPresenter.setExpandedActionViewsExclusive(true);
+ if (menu != null) {
+ menu.addMenuPresenter(outerPresenter, mPopupContext);
+ menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+ } else {
+ outerPresenter.initForMenu(mPopupContext, null);
+ mExpandedMenuPresenter.initForMenu(mPopupContext, null);
+ outerPresenter.updateMenuView(true);
+ mExpandedMenuPresenter.updateMenuView(true);
+ }
+ mMenuView.setPopupTheme(mPopupTheme);
+ mMenuView.setPresenter(outerPresenter);
+ mOuterActionMenuPresenter = outerPresenter;
+ }
+
+ /**
+ * Dismiss all currently showing popup menus, including overflow or submenus.
+ */
+ public void dismissPopupMenus() {
+ if (mMenuView != null) {
+ mMenuView.dismissPopupMenus();
+ }
+ }
+
+ /** @hide */
+ public boolean isTitleTruncated() {
+ if (mTitleTextView == null) {
+ return false;
+ }
+
+ final Layout titleLayout = mTitleTextView.getLayout();
+ if (titleLayout == null) {
+ return false;
+ }
+
+ final int lineCount = titleLayout.getLineCount();
+ for (int i = 0; i < lineCount; i++) {
+ if (titleLayout.getEllipsisCount(i) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Set a logo drawable.
+ *
+ * <p>This drawable should generally take the place of title text. The logo cannot be
+ * clicked. Apps using a logo should also supply a description using
+ * {@link #setLogoDescription(int)}.</p>
+ *
+ * @param drawable Drawable to use as a logo
+ */
+ public void setLogo(Drawable drawable) {
+ if (drawable != null) {
+ ensureLogoView();
+ if (!isChildOrHidden(mLogoView)) {
+ addSystemView(mLogoView, true);
+ }
+ } else if (mLogoView != null && isChildOrHidden(mLogoView)) {
+ removeView(mLogoView);
+ mHiddenViews.remove(mLogoView);
+ }
+ if (mLogoView != null) {
+ mLogoView.setImageDrawable(drawable);
+ }
+ }
+
+ /**
+ * Return the current logo drawable.
+ *
+ * @return The current logo drawable
+ * @see #setLogo(int)
+ * @see #setLogo(android.graphics.drawable.Drawable)
+ */
+ public Drawable getLogo() {
+ return mLogoView != null ? mLogoView.getDrawable() : null;
+ }
+
+ /**
+ * Set a description of the toolbar's logo.
+ *
+ * <p>This description will be used for accessibility or other similar descriptions
+ * of the UI.</p>
+ *
+ * @param resId String resource id
+ */
+ public void setLogoDescription(@StringRes int resId) {
+ setLogoDescription(getContext().getText(resId));
+ }
+
+ /**
+ * Set a description of the toolbar's logo.
+ *
+ * <p>This description will be used for accessibility or other similar descriptions
+ * of the UI.</p>
+ *
+ * @param description Description to set
+ */
+ public void setLogoDescription(CharSequence description) {
+ if (!TextUtils.isEmpty(description)) {
+ ensureLogoView();
+ }
+ if (mLogoView != null) {
+ mLogoView.setContentDescription(description);
+ }
+ }
+
+ /**
+ * Return the description of the toolbar's logo.
+ *
+ * @return A description of the logo
+ */
+ public CharSequence getLogoDescription() {
+ return mLogoView != null ? mLogoView.getContentDescription() : null;
+ }
+
+ private void ensureLogoView() {
+ if (mLogoView == null) {
+ mLogoView = new ImageView(getContext());
+ }
+ }
+
+ /**
+ * Check whether this Toolbar is currently hosting an expanded action view.
+ *
+ * <p>An action view may be expanded either directly from the
+ * {@link android.view.MenuItem MenuItem} it belongs to or by user action. If the Toolbar
+ * has an expanded action view it can be collapsed using the {@link #collapseActionView()}
+ * method.</p>
+ *
+ * @return true if the Toolbar has an expanded action view
+ */
+ public boolean hasExpandedActionView() {
+ return mExpandedMenuPresenter != null &&
+ mExpandedMenuPresenter.mCurrentExpandedItem != null;
+ }
+
+ /**
+ * Collapse a currently expanded action view. If this Toolbar does not have an
+ * expanded action view this method has no effect.
+ *
+ * <p>An action view may be expanded either directly from the
+ * {@link android.view.MenuItem MenuItem} it belongs to or by user action.</p>
+ *
+ * @see #hasExpandedActionView()
+ */
+ public void collapseActionView() {
+ final MenuItemImpl item = mExpandedMenuPresenter == null ? null :
+ mExpandedMenuPresenter.mCurrentExpandedItem;
+ if (item != null) {
+ item.collapseActionView();
+ }
+ }
+
+ /**
+ * Returns the title of this toolbar.
+ *
+ * @return The current title.
+ */
+ public CharSequence getTitle() {
+ return mTitleText;
+ }
+
+ /**
+ * Set the title of this toolbar.
+ *
+ * <p>A title should be used as the anchor for a section of content. It should
+ * describe or name the content being viewed.</p>
+ *
+ * @param resId Resource ID of a string to set as the title
+ */
+ public void setTitle(@StringRes int resId) {
+ setTitle(getContext().getText(resId));
+ }
+
+ /**
+ * Set the title of this toolbar.
+ *
+ * <p>A title should be used as the anchor for a section of content. It should
+ * describe or name the content being viewed.</p>
+ *
+ * @param title Title to set
+ */
+ public void setTitle(CharSequence title) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mTitleTextView == null) {
+ final Context context = getContext();
+ mTitleTextView = new TextView(context);
+ mTitleTextView.setSingleLine();
+ mTitleTextView.setEllipsize(TextUtils.TruncateAt.END);
+ if (mTitleTextAppearance != 0) {
+ mTitleTextView.setTextAppearance(mTitleTextAppearance);
+ }
+ if (mTitleTextColor != 0) {
+ mTitleTextView.setTextColor(mTitleTextColor);
+ }
+ }
+ if (!isChildOrHidden(mTitleTextView)) {
+ addSystemView(mTitleTextView, true);
+ }
+ } else if (mTitleTextView != null && isChildOrHidden(mTitleTextView)) {
+ removeView(mTitleTextView);
+ mHiddenViews.remove(mTitleTextView);
+ }
+ if (mTitleTextView != null) {
+ mTitleTextView.setText(title);
+ }
+ mTitleText = title;
+ }
+
+ /**
+ * Return the subtitle of this toolbar.
+ *
+ * @return The current subtitle
+ */
+ public CharSequence getSubtitle() {
+ return mSubtitleText;
+ }
+
+ /**
+ * Set the subtitle of this toolbar.
+ *
+ * <p>Subtitles should express extended information about the current content.</p>
+ *
+ * @param resId String resource ID
+ */
+ public void setSubtitle(@StringRes int resId) {
+ setSubtitle(getContext().getText(resId));
+ }
+
+ /**
+ * Set the subtitle of this toolbar.
+ *
+ * <p>Subtitles should express extended information about the current content.</p>
+ *
+ * @param subtitle Subtitle to set
+ */
+ public void setSubtitle(CharSequence subtitle) {
+ if (!TextUtils.isEmpty(subtitle)) {
+ if (mSubtitleTextView == null) {
+ final Context context = getContext();
+ mSubtitleTextView = new TextView(context);
+ mSubtitleTextView.setSingleLine();
+ mSubtitleTextView.setEllipsize(TextUtils.TruncateAt.END);
+ if (mSubtitleTextAppearance != 0) {
+ mSubtitleTextView.setTextAppearance(mSubtitleTextAppearance);
+ }
+ if (mSubtitleTextColor != 0) {
+ mSubtitleTextView.setTextColor(mSubtitleTextColor);
+ }
+ }
+ if (!isChildOrHidden(mSubtitleTextView)) {
+ addSystemView(mSubtitleTextView, true);
+ }
+ } else if (mSubtitleTextView != null && isChildOrHidden(mSubtitleTextView)) {
+ removeView(mSubtitleTextView);
+ mHiddenViews.remove(mSubtitleTextView);
+ }
+ if (mSubtitleTextView != null) {
+ mSubtitleTextView.setText(subtitle);
+ }
+ mSubtitleText = subtitle;
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setTitleTextAppearance(Context context, @StyleRes int resId) {
+ mTitleTextAppearance = resId;
+ if (mTitleTextView != null) {
+ mTitleTextView.setTextAppearance(resId);
+ }
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setSubtitleTextAppearance(Context context, @StyleRes int resId) {
+ mSubtitleTextAppearance = resId;
+ if (mSubtitleTextView != null) {
+ mSubtitleTextView.setTextAppearance(resId);
+ }
+ }
+
+ /**
+ * Sets the text color of the title, if present.
+ *
+ * @param color The new text color in 0xAARRGGBB format
+ */
+ public void setTitleTextColor(@ColorInt int color) {
+ mTitleTextColor = color;
+ if (mTitleTextView != null) {
+ mTitleTextView.setTextColor(color);
+ }
+ }
+
+ /**
+ * Sets the text color of the subtitle, if present.
+ *
+ * @param color The new text color in 0xAARRGGBB format
+ */
+ public void setSubtitleTextColor(@ColorInt int color) {
+ mSubtitleTextColor = color;
+ if (mSubtitleTextView != null) {
+ mSubtitleTextView.setTextColor(color);
+ }
+ }
+
+ /**
+ * Retrieve the currently configured content description for the navigation button view.
+ * This will be used to describe the navigation action to users through mechanisms such
+ * as screen readers or tooltips.
+ *
+ * @return The navigation button's content description
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+ */
+ @Nullable
+ public CharSequence getNavigationContentDescription() {
+ return mNavButtonView != null ? mNavButtonView.getContentDescription() : null;
+ }
+
+ /**
+ * Set a content description for the navigation button if one is present. The content
+ * description will be read via screen readers or other accessibility systems to explain
+ * the action of the navigation button.
+ *
+ * @param resId Resource ID of a content description string to set, or 0 to
+ * clear the description
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+ */
+ public void setNavigationContentDescription(@StringRes int resId) {
+ setNavigationContentDescription(resId != 0 ? getContext().getText(resId) : null);
+ }
+
+ /**
+ * Set a content description for the navigation button if one is present. The content
+ * description will be read via screen readers or other accessibility systems to explain
+ * the action of the navigation button.
+ *
+ * @param description Content description to set, or <code>null</code> to
+ * clear the content description
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+ */
+ public void setNavigationContentDescription(@Nullable CharSequence description) {
+ if (!TextUtils.isEmpty(description)) {
+ ensureNavButtonView();
+ }
+ if (mNavButtonView != null) {
+ mNavButtonView.setContentDescription(description);
+ }
+ }
+
+ /**
+ * Set the icon to use for the toolbar's navigation button.
+ *
+ * <p>The navigation button appears at the start of the toolbar if present. Setting an icon
+ * will make the navigation button visible.</p>
+ *
+ * <p>If you use a navigation icon you should also set a description for its action using
+ * {@link #setNavigationContentDescription(int)}. This is used for accessibility and
+ * tooltips.</p>
+ *
+ * @param resId Resource ID of a drawable to set
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationIcon
+ */
+ public void setNavigationIcon(@DrawableRes int resId) {
+ setNavigationIcon(getContext().getDrawable(resId));
+ }
+
+ /**
+ * Set the icon to use for the toolbar's navigation button.
+ *
+ * <p>The navigation button appears at the start of the toolbar if present. Setting an icon
+ * will make the navigation button visible.</p>
+ *
+ * <p>If you use a navigation icon you should also set a description for its action using
+ * {@link #setNavigationContentDescription(int)}. This is used for accessibility and
+ * tooltips.</p>
+ *
+ * @param icon Drawable to set, may be null to clear the icon
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationIcon
+ */
+ public void setNavigationIcon(@Nullable Drawable icon) {
+ if (icon != null) {
+ ensureNavButtonView();
+ if (!isChildOrHidden(mNavButtonView)) {
+ addSystemView(mNavButtonView, true);
+ }
+ } else if (mNavButtonView != null && isChildOrHidden(mNavButtonView)) {
+ removeView(mNavButtonView);
+ mHiddenViews.remove(mNavButtonView);
+ }
+ if (mNavButtonView != null) {
+ mNavButtonView.setImageDrawable(icon);
+ }
+ }
+
+ /**
+ * Return the current drawable used as the navigation icon.
+ *
+ * @return The navigation icon drawable
+ *
+ * @attr ref android.R.styleable#Toolbar_navigationIcon
+ */
+ @Nullable
+ public Drawable getNavigationIcon() {
+ return mNavButtonView != null ? mNavButtonView.getDrawable() : null;
+ }
+
+ /**
+ * Set a listener to respond to navigation events.
+ *
+ * <p>This listener will be called whenever the user clicks the navigation button
+ * at the start of the toolbar. An icon must be set for the navigation button to appear.</p>
+ *
+ * @param listener Listener to set
+ * @see #setNavigationIcon(android.graphics.drawable.Drawable)
+ */
+ public void setNavigationOnClickListener(OnClickListener listener) {
+ ensureNavButtonView();
+ mNavButtonView.setOnClickListener(listener);
+ }
+
+ /**
+ * @hide
+ */
+ @Nullable
+ @TestApi
+ public View getNavigationView() {
+ return mNavButtonView;
+ }
+
+ /**
+ * Return the Menu shown in the toolbar.
+ *
+ * <p>Applications that wish to populate the toolbar's menu can do so from here. To use
+ * an XML menu resource, use {@link #inflateMenu(int)}.</p>
+ *
+ * @return The toolbar's Menu
+ */
+ public Menu getMenu() {
+ ensureMenu();
+ return mMenuView.getMenu();
+ }
+
+ /**
+ * Set the icon to use for the overflow button.
+ *
+ * @param icon Drawable to set, may be null to clear the icon
+ */
+ public void setOverflowIcon(@Nullable Drawable icon) {
+ ensureMenu();
+ mMenuView.setOverflowIcon(icon);
+ }
+
+ /**
+ * Return the current drawable used as the overflow icon.
+ *
+ * @return The overflow icon drawable
+ */
+ @Nullable
+ public Drawable getOverflowIcon() {
+ ensureMenu();
+ return mMenuView.getOverflowIcon();
+ }
+
+ private void ensureMenu() {
+ ensureMenuView();
+ if (mMenuView.peekMenu() == null) {
+ // Initialize a new menu for the first time.
+ final MenuBuilder menu = (MenuBuilder) mMenuView.getMenu();
+ if (mExpandedMenuPresenter == null) {
+ mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+ }
+ mMenuView.setExpandedActionViewsExclusive(true);
+ menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+ }
+ }
+
+ private void ensureMenuView() {
+ if (mMenuView == null) {
+ mMenuView = new ActionMenuView(getContext());
+ mMenuView.setPopupTheme(mPopupTheme);
+ mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener);
+ mMenuView.setMenuCallbacks(mActionMenuPresenterCallback, mMenuBuilderCallback);
+ final LayoutParams lp = generateDefaultLayoutParams();
+ lp.gravity = Gravity.END | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+ mMenuView.setLayoutParams(lp);
+ addSystemView(mMenuView, false);
+ }
+ }
+
+ private MenuInflater getMenuInflater() {
+ return new MenuInflater(getContext());
+ }
+
+ /**
+ * Inflate a menu resource into this toolbar.
+ *
+ * <p>Inflate an XML menu resource into this toolbar. Existing items in the menu will not
+ * be modified or removed.</p>
+ *
+ * @param resId ID of a menu resource to inflate
+ */
+ public void inflateMenu(@MenuRes int resId) {
+ getMenuInflater().inflate(resId, getMenu());
+ }
+
+ /**
+ * Set a listener to respond to menu item click events.
+ *
+ * <p>This listener will be invoked whenever a user selects a menu item from
+ * the action buttons presented at the end of the toolbar or the associated overflow.</p>
+ *
+ * @param listener Listener to set
+ */
+ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+ mOnMenuItemClickListener = listener;
+ }
+
+ /**
+ * Sets the content insets for this toolbar relative to layout direction.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @param contentInsetStart Content inset for the toolbar starting edge
+ * @param contentInsetEnd Content inset for the toolbar ending edge
+ *
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetStart()
+ * @see #getContentInsetEnd()
+ * @see #getContentInsetLeft()
+ * @see #getContentInsetRight()
+ * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+ * @attr ref android.R.styleable#Toolbar_contentInsetStart
+ */
+ public void setContentInsetsRelative(int contentInsetStart, int contentInsetEnd) {
+ ensureContentInsets();
+ mContentInsets.setRelative(contentInsetStart, contentInsetEnd);
+ }
+
+ /**
+ * Gets the starting content inset for this toolbar.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @return The starting content inset for this toolbar
+ *
+ * @see #setContentInsetsRelative(int, int)
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetEnd()
+ * @see #getContentInsetLeft()
+ * @see #getContentInsetRight()
+ * @attr ref android.R.styleable#Toolbar_contentInsetStart
+ */
+ public int getContentInsetStart() {
+ return mContentInsets != null ? mContentInsets.getStart() : 0;
+ }
+
+ /**
+ * Gets the ending content inset for this toolbar.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @return The ending content inset for this toolbar
+ *
+ * @see #setContentInsetsRelative(int, int)
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetStart()
+ * @see #getContentInsetLeft()
+ * @see #getContentInsetRight()
+ * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+ */
+ public int getContentInsetEnd() {
+ return mContentInsets != null ? mContentInsets.getEnd() : 0;
+ }
+
+ /**
+ * Sets the content insets for this toolbar.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @param contentInsetLeft Content inset for the toolbar's left edge
+ * @param contentInsetRight Content inset for the toolbar's right edge
+ *
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetStart()
+ * @see #getContentInsetEnd()
+ * @see #getContentInsetLeft()
+ * @see #getContentInsetRight()
+ * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+ * @attr ref android.R.styleable#Toolbar_contentInsetRight
+ */
+ public void setContentInsetsAbsolute(int contentInsetLeft, int contentInsetRight) {
+ ensureContentInsets();
+ mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight);
+ }
+
+ /**
+ * Gets the left content inset for this toolbar.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @return The left content inset for this toolbar
+ *
+ * @see #setContentInsetsRelative(int, int)
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetStart()
+ * @see #getContentInsetEnd()
+ * @see #getContentInsetRight()
+ * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+ */
+ public int getContentInsetLeft() {
+ return mContentInsets != null ? mContentInsets.getLeft() : 0;
+ }
+
+ /**
+ * Gets the right content inset for this toolbar.
+ *
+ * <p>The content inset affects the valid area for Toolbar content other than
+ * the navigation button and menu. Insets define the minimum margin for these components
+ * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+ *
+ * @return The right content inset for this toolbar
+ *
+ * @see #setContentInsetsRelative(int, int)
+ * @see #setContentInsetsAbsolute(int, int)
+ * @see #getContentInsetStart()
+ * @see #getContentInsetEnd()
+ * @see #getContentInsetLeft()
+ * @attr ref android.R.styleable#Toolbar_contentInsetRight
+ */
+ public int getContentInsetRight() {
+ return mContentInsets != null ? mContentInsets.getRight() : 0;
+ }
+
+ /**
+ * Gets the start content inset to use when a navigation button is present.
+ *
+ * <p>Different content insets are often called for when additional buttons are present
+ * in the toolbar, as well as at different toolbar sizes. The larger value of
+ * {@link #getContentInsetStart()} and this value will be used during layout.</p>
+ *
+ * @return the start content inset used when a navigation icon has been set in pixels
+ *
+ * @see #setContentInsetStartWithNavigation(int)
+ * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+ */
+ public int getContentInsetStartWithNavigation() {
+ return mContentInsetStartWithNavigation != RtlSpacingHelper.UNDEFINED
+ ? mContentInsetStartWithNavigation
+ : getContentInsetStart();
+ }
+
+ /**
+ * Sets the start content inset to use when a navigation button is present.
+ *
+ * <p>Different content insets are often called for when additional buttons are present
+ * in the toolbar, as well as at different toolbar sizes. The larger value of
+ * {@link #getContentInsetStart()} and this value will be used during layout.</p>
+ *
+ * @param insetStartWithNavigation the inset to use when a navigation icon has been set
+ * in pixels
+ *
+ * @see #getContentInsetStartWithNavigation()
+ * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+ */
+ public void setContentInsetStartWithNavigation(int insetStartWithNavigation) {
+ if (insetStartWithNavigation < 0) {
+ insetStartWithNavigation = RtlSpacingHelper.UNDEFINED;
+ }
+ if (insetStartWithNavigation != mContentInsetStartWithNavigation) {
+ mContentInsetStartWithNavigation = insetStartWithNavigation;
+ if (getNavigationIcon() != null) {
+ requestLayout();
+ }
+ }
+ }
+
+ /**
+ * Gets the end content inset to use when action buttons are present.
+ *
+ * <p>Different content insets are often called for when additional buttons are present
+ * in the toolbar, as well as at different toolbar sizes. The larger value of
+ * {@link #getContentInsetEnd()} and this value will be used during layout.</p>
+ *
+ * @return the end content inset used when a menu has been set in pixels
+ *
+ * @see #setContentInsetEndWithActions(int)
+ * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+ */
+ public int getContentInsetEndWithActions() {
+ return mContentInsetEndWithActions != RtlSpacingHelper.UNDEFINED
+ ? mContentInsetEndWithActions
+ : getContentInsetEnd();
+ }
+
+ /**
+ * Sets the start content inset to use when action buttons are present.
+ *
+ * <p>Different content insets are often called for when additional buttons are present
+ * in the toolbar, as well as at different toolbar sizes. The larger value of
+ * {@link #getContentInsetEnd()} and this value will be used during layout.</p>
+ *
+ * @param insetEndWithActions the inset to use when a menu has been set in pixels
+ *
+ * @see #setContentInsetEndWithActions(int)
+ * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+ */
+ public void setContentInsetEndWithActions(int insetEndWithActions) {
+ if (insetEndWithActions < 0) {
+ insetEndWithActions = RtlSpacingHelper.UNDEFINED;
+ }
+ if (insetEndWithActions != mContentInsetEndWithActions) {
+ mContentInsetEndWithActions = insetEndWithActions;
+ if (getNavigationIcon() != null) {
+ requestLayout();
+ }
+ }
+ }
+
+ /**
+ * Gets the content inset that will be used on the starting side of the bar in the current
+ * toolbar configuration.
+ *
+ * @return the current content inset start in pixels
+ *
+ * @see #getContentInsetStartWithNavigation()
+ */
+ public int getCurrentContentInsetStart() {
+ return getNavigationIcon() != null
+ ? Math.max(getContentInsetStart(), Math.max(mContentInsetStartWithNavigation, 0))
+ : getContentInsetStart();
+ }
+
+ /**
+ * Gets the content inset that will be used on the ending side of the bar in the current
+ * toolbar configuration.
+ *
+ * @return the current content inset end in pixels
+ *
+ * @see #getContentInsetEndWithActions()
+ */
+ public int getCurrentContentInsetEnd() {
+ boolean hasActions = false;
+ if (mMenuView != null) {
+ final MenuBuilder mb = mMenuView.peekMenu();
+ hasActions = mb != null && mb.hasVisibleItems();
+ }
+ return hasActions
+ ? Math.max(getContentInsetEnd(), Math.max(mContentInsetEndWithActions, 0))
+ : getContentInsetEnd();
+ }
+
+ /**
+ * Gets the content inset that will be used on the left side of the bar in the current
+ * toolbar configuration.
+ *
+ * @return the current content inset left in pixels
+ *
+ * @see #getContentInsetStartWithNavigation()
+ * @see #getContentInsetEndWithActions()
+ */
+ public int getCurrentContentInsetLeft() {
+ return isLayoutRtl()
+ ? getCurrentContentInsetEnd()
+ : getCurrentContentInsetStart();
+ }
+
+ /**
+ * Gets the content inset that will be used on the right side of the bar in the current
+ * toolbar configuration.
+ *
+ * @return the current content inset right in pixels
+ *
+ * @see #getContentInsetStartWithNavigation()
+ * @see #getContentInsetEndWithActions()
+ */
+ public int getCurrentContentInsetRight() {
+ return isLayoutRtl()
+ ? getCurrentContentInsetStart()
+ : getCurrentContentInsetEnd();
+ }
+
+ private void ensureNavButtonView() {
+ if (mNavButtonView == null) {
+ mNavButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle);
+ final LayoutParams lp = generateDefaultLayoutParams();
+ lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+ mNavButtonView.setLayoutParams(lp);
+ }
+ }
+
+ private void ensureCollapseButtonView() {
+ if (mCollapseButtonView == null) {
+ mCollapseButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle);
+ mCollapseButtonView.setImageDrawable(mCollapseIcon);
+ mCollapseButtonView.setContentDescription(mCollapseDescription);
+ final LayoutParams lp = generateDefaultLayoutParams();
+ lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+ lp.mViewType = LayoutParams.EXPANDED;
+ mCollapseButtonView.setLayoutParams(lp);
+ mCollapseButtonView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ collapseActionView();
+ }
+ });
+ }
+ }
+
+ private void addSystemView(View v, boolean allowHide) {
+ final ViewGroup.LayoutParams vlp = v.getLayoutParams();
+ final LayoutParams lp;
+ if (vlp == null) {
+ lp = generateDefaultLayoutParams();
+ } else if (!checkLayoutParams(vlp)) {
+ lp = generateLayoutParams(vlp);
+ } else {
+ lp = (LayoutParams) vlp;
+ }
+ lp.mViewType = LayoutParams.SYSTEM;
+
+ if (allowHide && mExpandedActionView != null) {
+ v.setLayoutParams(lp);
+ mHiddenViews.add(v);
+ } else {
+ addView(v, lp);
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ SavedState state = new SavedState(super.onSaveInstanceState());
+
+ if (mExpandedMenuPresenter != null && mExpandedMenuPresenter.mCurrentExpandedItem != null) {
+ state.expandedMenuItemId = mExpandedMenuPresenter.mCurrentExpandedItem.getItemId();
+ }
+
+ state.isOverflowOpen = isOverflowMenuShowing();
+
+ return state;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ final Menu menu = mMenuView != null ? mMenuView.peekMenu() : null;
+ if (ss.expandedMenuItemId != 0 && mExpandedMenuPresenter != null && menu != null) {
+ final MenuItem item = menu.findItem(ss.expandedMenuItemId);
+ if (item != null) {
+ item.expandActionView();
+ }
+ }
+
+ if (ss.isOverflowOpen) {
+ postShowOverflowMenu();
+ }
+ }
+
+ private void postShowOverflowMenu() {
+ removeCallbacks(mShowOverflowMenuRunnable);
+ post(mShowOverflowMenuRunnable);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeCallbacks(mShowOverflowMenuRunnable);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // Toolbars always eat touch events, but should still respect the touch event dispatch
+ // contract. If the normal View implementation doesn't want the events, we'll just silently
+ // eat the rest of the gesture without reporting the events to the default implementation
+ // since that's what it expects.
+
+ final int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mEatingTouch = false;
+ }
+
+ if (!mEatingTouch) {
+ final boolean handled = super.onTouchEvent(ev);
+ if (action == MotionEvent.ACTION_DOWN && !handled) {
+ mEatingTouch = true;
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mEatingTouch = false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ protected void onSetLayoutParams(View child, ViewGroup.LayoutParams lp) {
+ /*
+ * Apps may set ActionBar.LayoutParams on their action bar custom views when
+ * a Toolbar is actually acting in the role of the action bar. Perform a quick
+ * switch with Toolbar.LayoutParams whenever this happens. This does leave open
+ * one potential gotcha: if an app retains the ActionBar.LayoutParams reference
+ * and attempts to keep making changes to it before layout those changes won't
+ * be reflected in the final results.
+ */
+ if (!checkLayoutParams(lp)) {
+ child.setLayoutParams(generateLayoutParams(lp));
+ }
+ }
+
+ private void measureChildConstrained(View child, int parentWidthSpec, int widthUsed,
+ int parentHeightSpec, int heightUsed, int heightConstraint) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int childWidthSpec = getChildMeasureSpec(parentWidthSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ int childHeightSpec = getChildMeasureSpec(parentHeightSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ final int childHeightMode = MeasureSpec.getMode(childHeightSpec);
+ if (childHeightMode != MeasureSpec.EXACTLY && heightConstraint >= 0) {
+ final int size = childHeightMode != MeasureSpec.UNSPECIFIED ?
+ Math.min(MeasureSpec.getSize(childHeightSpec), heightConstraint) :
+ heightConstraint;
+ childHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ /**
+ * Returns the width + uncollapsed margins
+ */
+ private int measureChildCollapseMargins(View child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed, int[] collapsingMargins) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int leftDiff = lp.leftMargin - collapsingMargins[0];
+ final int rightDiff = lp.rightMargin - collapsingMargins[1];
+ final int leftMargin = Math.max(0, leftDiff);
+ final int rightMargin = Math.max(0, rightDiff);
+ final int hMargins = leftMargin + rightMargin;
+ collapsingMargins[0] = Math.max(0, -leftDiff);
+ collapsingMargins[1] = Math.max(0, -rightDiff);
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight + hMargins + widthUsed, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ return child.getMeasuredWidth() + hMargins;
+ }
+
+ /**
+ * Returns true if the Toolbar is collapsible and has no child views with a measured size > 0.
+ */
+ private boolean shouldCollapse() {
+ if (!mCollapsible) return false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (shouldLayout(child) && child.getMeasuredWidth() > 0 &&
+ child.getMeasuredHeight() > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = 0;
+ int height = 0;
+ int childState = 0;
+
+ final int[] collapsingMargins = mTempMargins;
+ final int marginStartIndex;
+ final int marginEndIndex;
+ if (isLayoutRtl()) {
+ marginStartIndex = 1;
+ marginEndIndex = 0;
+ } else {
+ marginStartIndex = 0;
+ marginEndIndex = 1;
+ }
+
+ // System views measure first.
+
+ int navWidth = 0;
+ if (shouldLayout(mNavButtonView)) {
+ measureChildConstrained(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0,
+ mMaxButtonHeight);
+ navWidth = mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView);
+ height = Math.max(height, mNavButtonView.getMeasuredHeight() +
+ getVerticalMargins(mNavButtonView));
+ childState = combineMeasuredStates(childState, mNavButtonView.getMeasuredState());
+ }
+
+ if (shouldLayout(mCollapseButtonView)) {
+ measureChildConstrained(mCollapseButtonView, widthMeasureSpec, width,
+ heightMeasureSpec, 0, mMaxButtonHeight);
+ navWidth = mCollapseButtonView.getMeasuredWidth() +
+ getHorizontalMargins(mCollapseButtonView);
+ height = Math.max(height, mCollapseButtonView.getMeasuredHeight() +
+ getVerticalMargins(mCollapseButtonView));
+ childState = combineMeasuredStates(childState, mCollapseButtonView.getMeasuredState());
+ }
+
+ final int contentInsetStart = getCurrentContentInsetStart();
+ width += Math.max(contentInsetStart, navWidth);
+ collapsingMargins[marginStartIndex] = Math.max(0, contentInsetStart - navWidth);
+
+ int menuWidth = 0;
+ if (shouldLayout(mMenuView)) {
+ measureChildConstrained(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0,
+ mMaxButtonHeight);
+ menuWidth = mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView);
+ height = Math.max(height, mMenuView.getMeasuredHeight() +
+ getVerticalMargins(mMenuView));
+ childState = combineMeasuredStates(childState, mMenuView.getMeasuredState());
+ }
+
+ final int contentInsetEnd = getCurrentContentInsetEnd();
+ width += Math.max(contentInsetEnd, menuWidth);
+ collapsingMargins[marginEndIndex] = Math.max(0, contentInsetEnd - menuWidth);
+
+ if (shouldLayout(mExpandedActionView)) {
+ width += measureChildCollapseMargins(mExpandedActionView, widthMeasureSpec, width,
+ heightMeasureSpec, 0, collapsingMargins);
+ height = Math.max(height, mExpandedActionView.getMeasuredHeight() +
+ getVerticalMargins(mExpandedActionView));
+ childState = combineMeasuredStates(childState, mExpandedActionView.getMeasuredState());
+ }
+
+ if (shouldLayout(mLogoView)) {
+ width += measureChildCollapseMargins(mLogoView, widthMeasureSpec, width,
+ heightMeasureSpec, 0, collapsingMargins);
+ height = Math.max(height, mLogoView.getMeasuredHeight() +
+ getVerticalMargins(mLogoView));
+ childState = combineMeasuredStates(childState, mLogoView.getMeasuredState());
+ }
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mViewType != LayoutParams.CUSTOM || !shouldLayout(child)) {
+ // We already got all system views above. Skip them and GONE views.
+ continue;
+ }
+
+ width += measureChildCollapseMargins(child, widthMeasureSpec, width,
+ heightMeasureSpec, 0, collapsingMargins);
+ height = Math.max(height, child.getMeasuredHeight() + getVerticalMargins(child));
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ }
+
+ int titleWidth = 0;
+ int titleHeight = 0;
+ final int titleVertMargins = mTitleMarginTop + mTitleMarginBottom;
+ final int titleHorizMargins = mTitleMarginStart + mTitleMarginEnd;
+ if (shouldLayout(mTitleTextView)) {
+ titleWidth = measureChildCollapseMargins(mTitleTextView, widthMeasureSpec,
+ width + titleHorizMargins, heightMeasureSpec, titleVertMargins,
+ collapsingMargins);
+ titleWidth = mTitleTextView.getMeasuredWidth() + getHorizontalMargins(mTitleTextView);
+ titleHeight = mTitleTextView.getMeasuredHeight() + getVerticalMargins(mTitleTextView);
+ childState = combineMeasuredStates(childState, mTitleTextView.getMeasuredState());
+ }
+ if (shouldLayout(mSubtitleTextView)) {
+ titleWidth = Math.max(titleWidth, measureChildCollapseMargins(mSubtitleTextView,
+ widthMeasureSpec, width + titleHorizMargins,
+ heightMeasureSpec, titleHeight + titleVertMargins,
+ collapsingMargins));
+ titleHeight += mSubtitleTextView.getMeasuredHeight() +
+ getVerticalMargins(mSubtitleTextView);
+ childState = combineMeasuredStates(childState, mSubtitleTextView.getMeasuredState());
+ }
+
+ width += titleWidth;
+ height = Math.max(height, titleHeight);
+
+ // Measurement already took padding into account for available space for the children,
+ // add it in for the final size.
+ width += getPaddingLeft() + getPaddingRight();
+ height += getPaddingTop() + getPaddingBottom();
+
+ final int measuredWidth = resolveSizeAndState(
+ Math.max(width, getSuggestedMinimumWidth()),
+ widthMeasureSpec, childState & MEASURED_STATE_MASK);
+ final int measuredHeight = resolveSizeAndState(
+ Math.max(height, getSuggestedMinimumHeight()),
+ heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT);
+
+ setMeasuredDimension(measuredWidth, shouldCollapse() ? 0 : measuredHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ final int width = getWidth();
+ final int height = getHeight();
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int paddingTop = getPaddingTop();
+ final int paddingBottom = getPaddingBottom();
+ int left = paddingLeft;
+ int right = width - paddingRight;
+
+ final int[] collapsingMargins = mTempMargins;
+ collapsingMargins[0] = collapsingMargins[1] = 0;
+
+ // Align views within the minimum toolbar height, if set.
+ final int minHeight = getMinimumHeight();
+ final int alignmentHeight = minHeight >= 0 ? Math.min(minHeight, b - t) : 0;
+
+ if (shouldLayout(mNavButtonView)) {
+ if (isRtl) {
+ right = layoutChildRight(mNavButtonView, right, collapsingMargins,
+ alignmentHeight);
+ } else {
+ left = layoutChildLeft(mNavButtonView, left, collapsingMargins,
+ alignmentHeight);
+ }
+ }
+
+ if (shouldLayout(mCollapseButtonView)) {
+ if (isRtl) {
+ right = layoutChildRight(mCollapseButtonView, right, collapsingMargins,
+ alignmentHeight);
+ } else {
+ left = layoutChildLeft(mCollapseButtonView, left, collapsingMargins,
+ alignmentHeight);
+ }
+ }
+
+ if (shouldLayout(mMenuView)) {
+ if (isRtl) {
+ left = layoutChildLeft(mMenuView, left, collapsingMargins,
+ alignmentHeight);
+ } else {
+ right = layoutChildRight(mMenuView, right, collapsingMargins,
+ alignmentHeight);
+ }
+ }
+
+ final int contentInsetLeft = getCurrentContentInsetLeft();
+ final int contentInsetRight = getCurrentContentInsetRight();
+ collapsingMargins[0] = Math.max(0, contentInsetLeft - left);
+ collapsingMargins[1] = Math.max(0, contentInsetRight - (width - paddingRight - right));
+ left = Math.max(left, contentInsetLeft);
+ right = Math.min(right, width - paddingRight - contentInsetRight);
+
+ if (shouldLayout(mExpandedActionView)) {
+ if (isRtl) {
+ right = layoutChildRight(mExpandedActionView, right, collapsingMargins,
+ alignmentHeight);
+ } else {
+ left = layoutChildLeft(mExpandedActionView, left, collapsingMargins,
+ alignmentHeight);
+ }
+ }
+
+ if (shouldLayout(mLogoView)) {
+ if (isRtl) {
+ right = layoutChildRight(mLogoView, right, collapsingMargins,
+ alignmentHeight);
+ } else {
+ left = layoutChildLeft(mLogoView, left, collapsingMargins,
+ alignmentHeight);
+ }
+ }
+
+ final boolean layoutTitle = shouldLayout(mTitleTextView);
+ final boolean layoutSubtitle = shouldLayout(mSubtitleTextView);
+ int titleHeight = 0;
+ if (layoutTitle) {
+ final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+ titleHeight += lp.topMargin + mTitleTextView.getMeasuredHeight() + lp.bottomMargin;
+ }
+ if (layoutSubtitle) {
+ final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+ titleHeight += lp.topMargin + mSubtitleTextView.getMeasuredHeight() + lp.bottomMargin;
+ }
+
+ if (layoutTitle || layoutSubtitle) {
+ int titleTop;
+ final View topChild = layoutTitle ? mTitleTextView : mSubtitleTextView;
+ final View bottomChild = layoutSubtitle ? mSubtitleTextView : mTitleTextView;
+ final LayoutParams toplp = (LayoutParams) topChild.getLayoutParams();
+ final LayoutParams bottomlp = (LayoutParams) bottomChild.getLayoutParams();
+ final boolean titleHasWidth = layoutTitle && mTitleTextView.getMeasuredWidth() > 0
+ || layoutSubtitle && mSubtitleTextView.getMeasuredWidth() > 0;
+
+ switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+ case Gravity.TOP:
+ titleTop = getPaddingTop() + toplp.topMargin + mTitleMarginTop;
+ break;
+ default:
+ case Gravity.CENTER_VERTICAL:
+ final int space = height - paddingTop - paddingBottom;
+ int spaceAbove = (space - titleHeight) / 2;
+ if (spaceAbove < toplp.topMargin + mTitleMarginTop) {
+ spaceAbove = toplp.topMargin + mTitleMarginTop;
+ } else {
+ final int spaceBelow = height - paddingBottom - titleHeight -
+ spaceAbove - paddingTop;
+ if (spaceBelow < toplp.bottomMargin + mTitleMarginBottom) {
+ spaceAbove = Math.max(0, spaceAbove -
+ (bottomlp.bottomMargin + mTitleMarginBottom - spaceBelow));
+ }
+ }
+ titleTop = paddingTop + spaceAbove;
+ break;
+ case Gravity.BOTTOM:
+ titleTop = height - paddingBottom - bottomlp.bottomMargin - mTitleMarginBottom -
+ titleHeight;
+ break;
+ }
+ if (isRtl) {
+ final int rd = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[1];
+ right -= Math.max(0, rd);
+ collapsingMargins[1] = Math.max(0, -rd);
+ int titleRight = right;
+ int subtitleRight = right;
+
+ if (layoutTitle) {
+ final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+ final int titleLeft = titleRight - mTitleTextView.getMeasuredWidth();
+ final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
+ mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
+ titleRight = titleLeft - mTitleMarginEnd;
+ titleTop = titleBottom + lp.bottomMargin;
+ }
+ if (layoutSubtitle) {
+ final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+ titleTop += lp.topMargin;
+ final int subtitleLeft = subtitleRight - mSubtitleTextView.getMeasuredWidth();
+ final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
+ mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
+ subtitleRight = subtitleRight - mTitleMarginEnd;
+ titleTop = subtitleBottom + lp.bottomMargin;
+ }
+ if (titleHasWidth) {
+ right = Math.min(titleRight, subtitleRight);
+ }
+ } else {
+ final int ld = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[0];
+ left += Math.max(0, ld);
+ collapsingMargins[0] = Math.max(0, -ld);
+ int titleLeft = left;
+ int subtitleLeft = left;
+
+ if (layoutTitle) {
+ final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+ final int titleRight = titleLeft + mTitleTextView.getMeasuredWidth();
+ final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
+ mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
+ titleLeft = titleRight + mTitleMarginEnd;
+ titleTop = titleBottom + lp.bottomMargin;
+ }
+ if (layoutSubtitle) {
+ final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+ titleTop += lp.topMargin;
+ final int subtitleRight = subtitleLeft + mSubtitleTextView.getMeasuredWidth();
+ final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
+ mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
+ subtitleLeft = subtitleRight + mTitleMarginEnd;
+ titleTop = subtitleBottom + lp.bottomMargin;
+ }
+ if (titleHasWidth) {
+ left = Math.max(titleLeft, subtitleLeft);
+ }
+ }
+ }
+
+ // Get all remaining children sorted for layout. This is all prepared
+ // such that absolute layout direction can be used below.
+
+ addCustomViewsWithGravity(mTempViews, Gravity.LEFT);
+ final int leftViewsCount = mTempViews.size();
+ for (int i = 0; i < leftViewsCount; i++) {
+ left = layoutChildLeft(mTempViews.get(i), left, collapsingMargins,
+ alignmentHeight);
+ }
+
+ addCustomViewsWithGravity(mTempViews, Gravity.RIGHT);
+ final int rightViewsCount = mTempViews.size();
+ for (int i = 0; i < rightViewsCount; i++) {
+ right = layoutChildRight(mTempViews.get(i), right, collapsingMargins,
+ alignmentHeight);
+ }
+
+ // Centered views try to center with respect to the whole bar, but views pinned
+ // to the left or right can push the mass of centered views to one side or the other.
+ addCustomViewsWithGravity(mTempViews, Gravity.CENTER_HORIZONTAL);
+ final int centerViewsWidth = getViewListMeasuredWidth(mTempViews, collapsingMargins);
+ final int parentCenter = paddingLeft + (width - paddingLeft - paddingRight) / 2;
+ final int halfCenterViewsWidth = centerViewsWidth / 2;
+ int centerLeft = parentCenter - halfCenterViewsWidth;
+ final int centerRight = centerLeft + centerViewsWidth;
+ if (centerLeft < left) {
+ centerLeft = left;
+ } else if (centerRight > right) {
+ centerLeft -= centerRight - right;
+ }
+
+ final int centerViewsCount = mTempViews.size();
+ for (int i = 0; i < centerViewsCount; i++) {
+ centerLeft = layoutChildLeft(mTempViews.get(i), centerLeft, collapsingMargins,
+ alignmentHeight);
+ }
+
+ mTempViews.clear();
+ }
+
+ private int getViewListMeasuredWidth(List<View> views, int[] collapsingMargins) {
+ int collapseLeft = collapsingMargins[0];
+ int collapseRight = collapsingMargins[1];
+ int width = 0;
+ final int count = views.size();
+ for (int i = 0; i < count; i++) {
+ final View v = views.get(i);
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ final int l = lp.leftMargin - collapseLeft;
+ final int r = lp.rightMargin - collapseRight;
+ final int leftMargin = Math.max(0, l);
+ final int rightMargin = Math.max(0, r);
+ collapseLeft = Math.max(0, -l);
+ collapseRight = Math.max(0, -r);
+ width += leftMargin + v.getMeasuredWidth() + rightMargin;
+ }
+ return width;
+ }
+
+ private int layoutChildLeft(View child, int left, int[] collapsingMargins,
+ int alignmentHeight) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int l = lp.leftMargin - collapsingMargins[0];
+ left += Math.max(0, l);
+ collapsingMargins[0] = Math.max(0, -l);
+ final int top = getChildTop(child, alignmentHeight);
+ final int childWidth = child.getMeasuredWidth();
+ child.layout(left, top, left + childWidth, top + child.getMeasuredHeight());
+ left += childWidth + lp.rightMargin;
+ return left;
+ }
+
+ private int layoutChildRight(View child, int right, int[] collapsingMargins,
+ int alignmentHeight) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int r = lp.rightMargin - collapsingMargins[1];
+ right -= Math.max(0, r);
+ collapsingMargins[1] = Math.max(0, -r);
+ final int top = getChildTop(child, alignmentHeight);
+ final int childWidth = child.getMeasuredWidth();
+ child.layout(right - childWidth, top, right, top + child.getMeasuredHeight());
+ right -= childWidth + lp.leftMargin;
+ return right;
+ }
+
+ private int getChildTop(View child, int alignmentHeight) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childHeight = child.getMeasuredHeight();
+ final int alignmentOffset = alignmentHeight > 0 ? (childHeight - alignmentHeight) / 2 : 0;
+ switch (getChildVerticalGravity(lp.gravity)) {
+ case Gravity.TOP:
+ return getPaddingTop() - alignmentOffset;
+
+ case Gravity.BOTTOM:
+ return getHeight() - getPaddingBottom() - childHeight
+ - lp.bottomMargin - alignmentOffset;
+
+ default:
+ case Gravity.CENTER_VERTICAL:
+ final int paddingTop = getPaddingTop();
+ final int paddingBottom = getPaddingBottom();
+ final int height = getHeight();
+ final int space = height - paddingTop - paddingBottom;
+ int spaceAbove = (space - childHeight) / 2;
+ if (spaceAbove < lp.topMargin) {
+ spaceAbove = lp.topMargin;
+ } else {
+ final int spaceBelow = height - paddingBottom - childHeight -
+ spaceAbove - paddingTop;
+ if (spaceBelow < lp.bottomMargin) {
+ spaceAbove = Math.max(0, spaceAbove - (lp.bottomMargin - spaceBelow));
+ }
+ }
+ return paddingTop + spaceAbove;
+ }
+ }
+
+ private int getChildVerticalGravity(int gravity) {
+ final int vgrav = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ switch (vgrav) {
+ case Gravity.TOP:
+ case Gravity.BOTTOM:
+ case Gravity.CENTER_VERTICAL:
+ return vgrav;
+ default:
+ return mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ }
+ }
+
+ /**
+ * Prepare a list of non-SYSTEM child views. If the layout direction is RTL
+ * this will be in reverse child order.
+ *
+ * @param views List to populate. It will be cleared before use.
+ * @param gravity Horizontal gravity to match against
+ */
+ private void addCustomViewsWithGravity(List<View> views, int gravity) {
+ final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ final int childCount = getChildCount();
+ final int absGrav = Gravity.getAbsoluteGravity(gravity, getLayoutDirection());
+
+ views.clear();
+
+ if (isRtl) {
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mViewType == LayoutParams.CUSTOM && shouldLayout(child) &&
+ getChildHorizontalGravity(lp.gravity) == absGrav) {
+ views.add(child);
+ }
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mViewType == LayoutParams.CUSTOM && shouldLayout(child) &&
+ getChildHorizontalGravity(lp.gravity) == absGrav) {
+ views.add(child);
+ }
+ }
+ }
+ }
+
+ private int getChildHorizontalGravity(int gravity) {
+ final int ld = getLayoutDirection();
+ final int absGrav = Gravity.getAbsoluteGravity(gravity, ld);
+ final int hGrav = absGrav & Gravity.HORIZONTAL_GRAVITY_MASK;
+ switch (hGrav) {
+ case Gravity.LEFT:
+ case Gravity.RIGHT:
+ case Gravity.CENTER_HORIZONTAL:
+ return hGrav;
+ default:
+ return ld == LAYOUT_DIRECTION_RTL ? Gravity.RIGHT : Gravity.LEFT;
+ }
+ }
+
+ private boolean shouldLayout(View view) {
+ return view != null && view.getParent() == this && view.getVisibility() != GONE;
+ }
+
+ private int getHorizontalMargins(View v) {
+ final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+ return mlp.getMarginStart() + mlp.getMarginEnd();
+ }
+
+ private int getVerticalMargins(View v) {
+ final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+ return mlp.topMargin + mlp.bottomMargin;
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) p);
+ } else if (p instanceof ActionBar.LayoutParams) {
+ return new LayoutParams((ActionBar.LayoutParams) p);
+ } else if (p instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) p);
+ } else {
+ return new LayoutParams(p);
+ }
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return super.checkLayoutParams(p) && p instanceof LayoutParams;
+ }
+
+ private static boolean isCustomView(View child) {
+ return ((LayoutParams) child.getLayoutParams()).mViewType == LayoutParams.CUSTOM;
+ }
+
+ /** @hide */
+ public DecorToolbar getWrapper() {
+ if (mWrapper == null) {
+ mWrapper = new ToolbarWidgetWrapper(this, true);
+ }
+ return mWrapper;
+ }
+
+ void removeChildrenForExpandedActionView() {
+ final int childCount = getChildCount();
+ // Go backwards since we're removing from the list
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mViewType != LayoutParams.EXPANDED && child != mMenuView) {
+ removeViewAt(i);
+ mHiddenViews.add(child);
+ }
+ }
+ }
+
+ void addChildrenForExpandedActionView() {
+ final int count = mHiddenViews.size();
+ // Re-add in reverse order since we removed in reverse order
+ for (int i = count - 1; i >= 0; i--) {
+ addView(mHiddenViews.get(i));
+ }
+ mHiddenViews.clear();
+ }
+
+ private boolean isChildOrHidden(View child) {
+ return child.getParent() == this || mHiddenViews.contains(child);
+ }
+
+ /**
+ * Force the toolbar to collapse to zero-height during measurement if
+ * it could be considered "empty" (no visible elements with nonzero measured size)
+ * @hide
+ */
+ public void setCollapsible(boolean collapsible) {
+ mCollapsible = collapsible;
+ requestLayout();
+ }
+
+ /**
+ * Must be called before the menu is accessed
+ * @hide
+ */
+ public void setMenuCallbacks(MenuPresenter.Callback pcb, MenuBuilder.Callback mcb) {
+ mActionMenuPresenterCallback = pcb;
+ mMenuBuilderCallback = mcb;
+ if (mMenuView != null) {
+ mMenuView.setMenuCallbacks(pcb, mcb);
+ }
+ }
+
+ private void ensureContentInsets() {
+ if (mContentInsets == null) {
+ mContentInsets = new RtlSpacingHelper();
+ }
+ }
+
+ /**
+ * Accessor to enable LayoutLib to get ActionMenuPresenter directly.
+ */
+ ActionMenuPresenter getOuterActionMenuPresenter() {
+ return mOuterActionMenuPresenter;
+ }
+
+ Context getPopupContext() {
+ return mPopupContext;
+ }
+
+ /**
+ * Interface responsible for receiving menu item click events if the items themselves
+ * do not have individual item click listeners.
+ */
+ public interface OnMenuItemClickListener {
+ /**
+ * This method will be invoked when a menu item is clicked if the item itself did
+ * not already handle the event.
+ *
+ * @param item {@link MenuItem} that was clicked
+ * @return <code>true</code> if the event was handled, <code>false</code> otherwise.
+ */
+ public boolean onMenuItemClick(MenuItem item);
+ }
+
+ /**
+ * Layout information for child views of Toolbars.
+ *
+ * <p>Toolbar.LayoutParams extends ActionBar.LayoutParams for compatibility with existing
+ * ActionBar API. See {@link android.app.Activity#setActionBar(Toolbar) Activity.setActionBar}
+ * for more info on how to use a Toolbar as your Activity's ActionBar.</p>
+ *
+ * @attr ref android.R.styleable#Toolbar_LayoutParams_layout_gravity
+ */
+ public static class LayoutParams extends ActionBar.LayoutParams {
+ static final int CUSTOM = 0;
+ static final int SYSTEM = 1;
+ static final int EXPANDED = 2;
+
+ int mViewType = CUSTOM;
+
+ public LayoutParams(@NonNull Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ this.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
+ }
+
+ public LayoutParams(int width, int height, int gravity) {
+ super(width, height);
+ this.gravity = gravity;
+ }
+
+ public LayoutParams(int gravity) {
+ this(WRAP_CONTENT, MATCH_PARENT, gravity);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+
+ mViewType = source.mViewType;
+ }
+
+ public LayoutParams(ActionBar.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ // ActionBar.LayoutParams doesn't have a MarginLayoutParams constructor.
+ // Fake it here and copy over the relevant data.
+ copyMarginsFrom(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int expandedMenuItemId;
+ public boolean isOverflowOpen;
+
+ public SavedState(Parcel source) {
+ super(source);
+ expandedMenuItemId = source.readInt();
+ isOverflowOpen = source.readInt() != 0;
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(expandedMenuItemId);
+ out.writeInt(isOverflowOpen ? 1 : 0);
+ }
+
+ public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+
+ @Override
+ public SavedState createFromParcel(Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ private class ExpandedActionViewMenuPresenter implements MenuPresenter {
+ MenuBuilder mMenu;
+ MenuItemImpl mCurrentExpandedItem;
+
+ @Override
+ public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+ // Clear the expanded action view when menus change.
+ if (mMenu != null && mCurrentExpandedItem != null) {
+ mMenu.collapseItemActionView(mCurrentExpandedItem);
+ }
+ mMenu = menu;
+ }
+
+ @Override
+ public MenuView getMenuView(ViewGroup root) {
+ return null;
+ }
+
+ @Override
+ public void updateMenuView(boolean cleared) {
+ // Make sure the expanded item we have is still there.
+ if (mCurrentExpandedItem != null) {
+ boolean found = false;
+
+ if (mMenu != null) {
+ final int count = mMenu.size();
+ for (int i = 0; i < count; i++) {
+ final MenuItem item = mMenu.getItem(i);
+ if (item == mCurrentExpandedItem) {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found) {
+ // The item we had expanded disappeared. Collapse.
+ collapseItemActionView(mMenu, mCurrentExpandedItem);
+ }
+ }
+ }
+
+ @Override
+ public void setCallback(Callback cb) {
+ }
+
+ @Override
+ public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+ return false;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ }
+
+ @Override
+ public boolean flagActionItems() {
+ return false;
+ }
+
+ @Override
+ public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+ ensureCollapseButtonView();
+ if (mCollapseButtonView.getParent() != Toolbar.this) {
+ addView(mCollapseButtonView);
+ }
+ mExpandedActionView = item.getActionView();
+ mCurrentExpandedItem = item;
+ if (mExpandedActionView.getParent() != Toolbar.this) {
+ final LayoutParams lp = generateDefaultLayoutParams();
+ lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+ lp.mViewType = LayoutParams.EXPANDED;
+ mExpandedActionView.setLayoutParams(lp);
+ addView(mExpandedActionView);
+ }
+
+ removeChildrenForExpandedActionView();
+ requestLayout();
+ item.setActionViewExpanded(true);
+
+ if (mExpandedActionView instanceof CollapsibleActionView) {
+ ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded();
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+ // Do this before detaching the actionview from the hierarchy, in case
+ // it needs to dismiss the soft keyboard, etc.
+ if (mExpandedActionView instanceof CollapsibleActionView) {
+ ((CollapsibleActionView) mExpandedActionView).onActionViewCollapsed();
+ }
+
+ removeView(mExpandedActionView);
+ removeView(mCollapseButtonView);
+ mExpandedActionView = null;
+
+ addChildrenForExpandedActionView();
+ mCurrentExpandedItem = null;
+ requestLayout();
+ item.setActionViewExpanded(false);
+
+ return true;
+ }
+
+ @Override
+ public int getId() {
+ return 0;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ }
+ }
+}
diff --git a/android/widget/Toolbar_Accessor.java b/android/widget/Toolbar_Accessor.java
new file mode 100644
index 0000000..fdd1779
--- /dev/null
+++ b/android/widget/Toolbar_Accessor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * To access non public members of classes in {@link Toolbar}
+ */
+public class Toolbar_Accessor {
+ public static ActionMenuPresenter getActionMenuPresenter(Toolbar toolbar) {
+ return toolbar.getOuterActionMenuPresenter();
+ }
+
+ public static Context getPopupContext(Toolbar toolbar) {
+ return toolbar.getPopupContext();
+ }
+}
diff --git a/android/widget/TwoLineListItem.java b/android/widget/TwoLineListItem.java
new file mode 100644
index 0000000..553b86e
--- /dev/null
+++ b/android/widget/TwoLineListItem.java
@@ -0,0 +1,97 @@
+/*
+ * 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.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * <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.
+ *
+ * @attr ref android.R.styleable#TwoLineListItem_mode
+ *
+ * @deprecated This class can be implemented easily by apps using a {@link RelativeLayout}
+ * or a {@link LinearLayout}.
+ */
+@Deprecated
+@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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.TwoLineListItem, defStyleAttr, defStyleRes);
+
+ a.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mText1 = findViewById(com.android.internal.R.id.text1);
+ mText2 = 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;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TwoLineListItem.class.getName();
+ }
+}
diff --git a/android/widget/VideoView.java b/android/widget/VideoView.java
new file mode 100644
index 0000000..58a2b0f
--- /dev/null
+++ b/android/widget/VideoView.java
@@ -0,0 +1,975 @@
+/*
+ * 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.annotation.NonNull;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Cea708CaptionRenderer;
+import android.media.ClosedCaptionRenderer;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnInfoListener;
+import android.media.Metadata;
+import android.media.SubtitleController;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.TtmlRenderer;
+import android.media.WebVttRenderer;
+import android.net.Uri;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+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;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * 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.<p>
+ *
+ * <em>Note: VideoView does not retain its full state when going into the
+ * background.</em> In particular, it does not restore the current play state,
+ * play position, selected tracks, or any subtitle tracks added via
+ * {@link #addSubtitleSource addSubtitleSource()}. Applications should
+ * save and restore these on their own in
+ * {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.<p>
+ * Also note that the audio session id (from {@link #getAudioSessionId}) may
+ * change from its previously returned value when the VideoView is restored.
+ * <p>
+ * By default, VideoView requests audio focus with {@link AudioManager#AUDIOFOCUS_GAIN}. Use
+ * {@link #setAudioFocusRequest(int)} to change this behavior.
+ * <p>
+ * The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ */
+public class VideoView extends SurfaceView
+ implements MediaPlayerControl, SubtitleController.Anchor {
+ private static final String TAG = "VideoView";
+
+ // all possible internal states
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private final Vector<Pair<InputStream, MediaFormat>> mPendingSubtitleTracks = new Vector<>();
+
+ // settable by the client
+ private Uri mUri;
+ private Map<String, String> mHeaders;
+
+ // mCurrentState is a VideoView object's current state.
+ // mTargetState is the state that a method caller intends to reach.
+ // For instance, regardless the VideoView object's current state,
+ // calling pause() intends to bring the object to a target state
+ // of STATE_PAUSED.
+ private int mCurrentState = STATE_IDLE;
+ private int mTargetState = STATE_IDLE;
+
+ // All the stuff we need for playing and showing a video
+ private SurfaceHolder mSurfaceHolder = null;
+ private MediaPlayer mMediaPlayer = null;
+ private int mAudioSession;
+ 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 OnInfoListener mOnInfoListener;
+ private int mSeekWhenPrepared; // recording the seek position while preparing
+ private boolean mCanPause;
+ private boolean mCanSeekBack;
+ private boolean mCanSeekForward;
+ private AudioManager mAudioManager;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private AudioAttributes mAudioAttributes;
+
+ /** Subtitle rendering widget overlaid on top of the video. */
+ private RenderingWidget mSubtitleWidget;
+
+ /** Listener for changes to subtitle data, used to redraw when needed. */
+ private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+ public VideoView(Context context) {
+ this(context, null);
+ }
+
+ public VideoView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+
+ getHolder().addCallback(mSHCallback);
+ getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ // + MeasureSpec.toString(heightMeasureSpec) + ")");
+
+ int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
+ int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
+ if (mVideoWidth > 0 && mVideoHeight > 0) {
+
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
+ // the size is fixed
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if ( mVideoWidth * height < width * mVideoHeight ) {
+ //Log.i("@@@", "image too wide, correcting");
+ width = height * mVideoWidth / mVideoHeight;
+ } else if ( mVideoWidth * height > width * mVideoHeight ) {
+ //Log.i("@@@", "image too tall, correcting");
+ height = width * mVideoHeight / mVideoWidth;
+ }
+ } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+ // only the width is fixed, adjust the height to match aspect ratio if possible
+ width = widthSpecSize;
+ height = width * mVideoHeight / mVideoWidth;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ height = heightSpecSize;
+ }
+ } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+ // only the height is fixed, adjust the width to match aspect ratio if possible
+ height = heightSpecSize;
+ width = height * mVideoWidth / mVideoHeight;
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ width = widthSpecSize;
+ }
+ } else {
+ // neither the width nor the height are fixed, try to use actual video size
+ width = mVideoWidth;
+ height = mVideoHeight;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // too tall, decrease both width and height
+ height = heightSpecSize;
+ width = height * mVideoWidth / mVideoHeight;
+ }
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // too wide, decrease both width and height
+ width = widthSpecSize;
+ height = width * mVideoHeight / mVideoWidth;
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return VideoView.class.getName();
+ }
+
+ public int resolveAdjustedSize(int desiredSize, int measureSpec) {
+ return getDefaultSize(desiredSize, measureSpec);
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ */
+ public void setVideoPath(String path) {
+ setVideoURI(Uri.parse(path));
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ */
+ public void setVideoURI(Uri uri) {
+ setVideoURI(uri, null);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ public void setVideoURI(Uri uri, Map<String, String> headers) {
+ mUri = uri;
+ mHeaders = headers;
+ mSeekWhenPrepared = 0;
+ openVideo();
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ mAudioAttributes = attributes;
+ }
+
+ /**
+ * Adds an external subtitle source file (from the provided input stream.)
+ *
+ * Note that a single external subtitle source may contain multiple or no
+ * supported tracks in it. If the source contained at least one track in
+ * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
+ * info message. Otherwise, if reading the source takes excessive time,
+ * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
+ * message. If the source contained no supported track (including an empty
+ * source file or null input stream), one will receive a {@link
+ * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the
+ * total number of available tracks using {@link MediaPlayer#getTrackInfo()}
+ * to see what additional tracks become available after this method call.
+ *
+ * @param is input stream containing the subtitle data. It will be
+ * closed by the media framework.
+ * @param format the format of the subtitle track(s). Must contain at least
+ * the mime type ({@link MediaFormat#KEY_MIME}) and the
+ * language ({@link MediaFormat#KEY_LANGUAGE}) of the file.
+ * If the file itself contains the language information,
+ * specify "und" for the language.
+ */
+ public void addSubtitleSource(InputStream is, MediaFormat format) {
+ if (mMediaPlayer == null) {
+ mPendingSubtitleTracks.add(Pair.create(is, format));
+ } else {
+ try {
+ mMediaPlayer.addSubtitleSource(is, format);
+ } catch (IllegalStateException e) {
+ mInfoListener.onInfo(
+ mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+ }
+ }
+ }
+
+ public void stopPlayback() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+
+ private void openVideo() {
+ if (mUri == null || mSurfaceHolder == null) {
+ // not ready for playback just yet, will try again later
+ return;
+ }
+ // we shouldn't clear the target state, because somebody might have
+ // called start() previously
+ release(false);
+
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ // TODO this should have a focus listener
+ mAudioManager.requestAudioFocus(null, mAudioAttributes, mAudioFocusType, 0 /*flags*/);
+ }
+
+ try {
+ mMediaPlayer = new MediaPlayer();
+ // TODO: create SubtitleController in MediaPlayer, but we need
+ // a context for the subtitle renderers
+ final Context context = getContext();
+ final SubtitleController controller = new SubtitleController(
+ context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
+ controller.registerRenderer(new WebVttRenderer(context));
+ controller.registerRenderer(new TtmlRenderer(context));
+ controller.registerRenderer(new Cea708CaptionRenderer(context));
+ controller.registerRenderer(new ClosedCaptionRenderer(context));
+ mMediaPlayer.setSubtitleAnchor(controller, this);
+
+ if (mAudioSession != 0) {
+ mMediaPlayer.setAudioSessionId(mAudioSession);
+ } else {
+ mAudioSession = mMediaPlayer.getAudioSessionId();
+ }
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+ mCurrentBufferPercentage = 0;
+ mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
+ mMediaPlayer.setDisplay(mSurfaceHolder);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ mMediaPlayer.setScreenOnWhilePlaying(true);
+ mMediaPlayer.prepareAsync();
+
+ for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) {
+ try {
+ mMediaPlayer.addSubtitleSource(pending.first, pending.second);
+ } catch (IllegalStateException e) {
+ mInfoListener.onInfo(
+ mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+ }
+ }
+
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ attachMediaController();
+ } catch (IOException ex) {
+ Log.w(TAG, "Unable to open content: " + mUri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ return;
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + mUri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ return;
+ } finally {
+ mPendingSubtitleTracks.clear();
+ }
+ }
+
+ 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(isInPlaybackState());
+ }
+ }
+
+ MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+ new MediaPlayer.OnVideoSizeChangedListener() {
+ public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ getHolder().setFixedSize(mVideoWidth, mVideoHeight);
+ requestLayout();
+ }
+ }
+ };
+
+ MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ mCurrentState = STATE_PREPARED;
+
+ // Get the capabilities of the player for this stream
+ Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
+ MediaPlayer.BYPASS_METADATA_FILTER);
+
+ if (data != null) {
+ mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
+ || data.getBoolean(Metadata.PAUSE_AVAILABLE);
+ mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+ || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
+ mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
+ || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
+ } else {
+ mCanPause = mCanSeekBack = mCanSeekForward = true;
+ }
+
+ if (mOnPreparedListener != null) {
+ mOnPreparedListener.onPrepared(mMediaPlayer);
+ }
+ if (mMediaController != null) {
+ mMediaController.setEnabled(true);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+
+ int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call
+ if (seekToPosition != 0) {
+ seekTo(seekToPosition);
+ }
+ 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 (mTargetState == STATE_PLAYING) {
+ start();
+ if (mMediaController != null) {
+ mMediaController.show();
+ }
+ } else if (!isPlaying() &&
+ (seekToPosition != 0 || getCurrentPosition() > 0)) {
+ if (mMediaController != null) {
+ // Show the media controls when we're paused into a video and make 'em stick.
+ mMediaController.show(0);
+ }
+ }
+ }
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (mTargetState == STATE_PLAYING) {
+ start();
+ }
+ }
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mCompletionListener =
+ new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ if (mMediaController != null) {
+ mMediaController.hide();
+ }
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion(mMediaPlayer);
+ }
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ };
+
+ private MediaPlayer.OnInfoListener mInfoListener =
+ new MediaPlayer.OnInfoListener() {
+ public boolean onInfo(MediaPlayer mp, int arg1, int arg2) {
+ if (mOnInfoListener != null) {
+ mOnInfoListener.onInfo(mp, arg1, arg2);
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnErrorListener mErrorListener =
+ new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
+ Log.d(TAG, "Error: " + framework_err + "," + impl_err);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ if (mMediaController != null) {
+ mMediaController.hide();
+ }
+
+ /* If an error handler has been supplied, use it and finish. */
+ if (mOnErrorListener != null) {
+ if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
+ 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();
+ int messageId;
+
+ if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+ messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
+ } else {
+ messageId = com.android.internal.R.string.VideoView_error_text_unknown;
+ }
+
+ new AlertDialog.Builder(mContext)
+ .setMessage(messageId)
+ .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;
+ }
+
+ /**
+ * Register a callback to be invoked when an informational event
+ * occurs during playback or setup.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnInfoListener(OnInfoListener l) {
+ mOnInfoListener = l;
+ }
+
+ SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
+ {
+ public void surfaceChanged(SurfaceHolder holder, int format,
+ int w, int h)
+ {
+ mSurfaceWidth = w;
+ mSurfaceHeight = h;
+ boolean isValidState = (mTargetState == STATE_PLAYING);
+ boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
+ if (mMediaPlayer != null && isValidState && hasValidSize) {
+ if (mSeekWhenPrepared != 0) {
+ seekTo(mSeekWhenPrepared);
+ }
+ start();
+ }
+ }
+
+ 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();
+ release(true);
+ }
+ };
+
+ /*
+ * release the media player in any state
+ */
+ private void release(boolean cleartargetstate) {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mPendingSubtitleTracks.clear();
+ mCurrentState = STATE_IDLE;
+ if (cleartargetstate) {
+ mTargetState = STATE_IDLE;
+ }
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN
+ && isInPlaybackState() && mMediaController != null) {
+ toggleMediaControlsVisiblity();
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN
+ && isInPlaybackState() && mMediaController != null) {
+ toggleMediaControlsVisiblity();
+ }
+ return super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event)
+ {
+ boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
+ keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
+ keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
+ keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
+ keyCode != KeyEvent.KEYCODE_MENU &&
+ keyCode != KeyEvent.KEYCODE_CALL &&
+ keyCode != KeyEvent.KEYCODE_ENDCALL;
+ if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
+ keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ pause();
+ mMediaController.show();
+ } else {
+ start();
+ mMediaController.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (!mMediaPlayer.isPlaying()) {
+ start();
+ mMediaController.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ pause();
+ mMediaController.show();
+ }
+ return true;
+ } else {
+ toggleMediaControlsVisiblity();
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void toggleMediaControlsVisiblity() {
+ if (mMediaController.isShowing()) {
+ mMediaController.hide();
+ } else {
+ mMediaController.show();
+ }
+ }
+
+ @Override
+ public void start() {
+ if (isInPlaybackState()) {
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ }
+ mTargetState = STATE_PLAYING;
+ }
+
+ @Override
+ public void pause() {
+ if (isInPlaybackState()) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ }
+
+ public void suspend() {
+ release(false);
+ }
+
+ public void resume() {
+ openVideo();
+ }
+
+ @Override
+ public int getDuration() {
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getDuration();
+ }
+
+ return -1;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getCurrentPosition();
+ }
+ return 0;
+ }
+
+ @Override
+ public void seekTo(int msec) {
+ if (isInPlaybackState()) {
+ mMediaPlayer.seekTo(msec);
+ mSeekWhenPrepared = 0;
+ } else {
+ mSeekWhenPrepared = msec;
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return isInPlaybackState() && mMediaPlayer.isPlaying();
+ }
+
+ @Override
+ public int getBufferPercentage() {
+ if (mMediaPlayer != null) {
+ return mCurrentBufferPercentage;
+ }
+ return 0;
+ }
+
+ private boolean isInPlaybackState() {
+ return (mMediaPlayer != null &&
+ mCurrentState != STATE_ERROR &&
+ mCurrentState != STATE_IDLE &&
+ mCurrentState != STATE_PREPARING);
+ }
+
+ @Override
+ public boolean canPause() {
+ return mCanPause;
+ }
+
+ @Override
+ public boolean canSeekBackward() {
+ return mCanSeekBack;
+ }
+
+ @Override
+ public boolean canSeekForward() {
+ return mCanSeekForward;
+ }
+
+ @Override
+ public int getAudioSessionId() {
+ if (mAudioSession == 0) {
+ MediaPlayer foo = new MediaPlayer();
+ mAudioSession = foo.getAudioSessionId();
+ foo.release();
+ }
+ return mAudioSession;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onAttachedToWindow();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mSubtitleWidget != null) {
+ measureAndLayoutSubtitleWidget();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mSubtitleWidget != null) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mSubtitleWidget.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ /**
+ * Forces a measurement and layout pass for all overlaid views.
+ *
+ * @see #setSubtitleWidget(RenderingWidget)
+ */
+ private void measureAndLayoutSubtitleWidget() {
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ mSubtitleWidget.setSize(width, height);
+ }
+
+ /** @hide */
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ if (mSubtitleWidget == subtitleWidget) {
+ return;
+ }
+
+ final boolean attachedToWindow = isAttachedToWindow();
+ if (mSubtitleWidget != null) {
+ if (attachedToWindow) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+
+ mSubtitleWidget.setOnChangedListener(null);
+ }
+
+ mSubtitleWidget = subtitleWidget;
+
+ if (subtitleWidget != null) {
+ if (mSubtitlesChangedListener == null) {
+ mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+ @Override
+ public void onChanged(RenderingWidget renderingWidget) {
+ invalidate();
+ }
+ };
+ }
+
+ setWillNotDraw(false);
+ subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+ if (attachedToWindow) {
+ subtitleWidget.onAttachedToWindow();
+ requestLayout();
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+
+ invalidate();
+ }
+
+ /** @hide */
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+}
diff --git a/android/widget/ViewAnimator.java b/android/widget/ViewAnimator.java
new file mode 100644
index 0000000..1580f51
--- /dev/null
+++ b/android/widget/ViewAnimator.java
@@ -0,0 +1,363 @@
+/*
+ * 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.annotation.AnimRes;
+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.
+ *
+ * @attr ref android.R.styleable#ViewAnimator_inAnimation
+ * @attr ref android.R.styleable#ViewAnimator_outAnimation
+ * @attr ref android.R.styleable#ViewAnimator_animateFirstView
+ */
+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(context, null);
+ }
+
+ 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);
+ }
+
+ boolean flag = a.getBoolean(com.android.internal.R.styleable.ViewAnimator_animateFirstView, true);
+ setAnimateFirstView(flag);
+
+ a.recycle();
+
+ initViewAnimator(context, attrs);
+ }
+
+ /**
+ * Initialize this {@link ViewAnimator}, possibly setting
+ * {@link #setMeasureAllChildren(boolean)} based on {@link FrameLayout} flags.
+ */
+ private void initViewAnimator(Context context, AttributeSet attrs) {
+ if (attrs == null) {
+ // For compatibility, always measure children when undefined.
+ mMeasureAllChildren = true;
+ return;
+ }
+
+ // For compatibility, default to measure children, but allow XML
+ // attribute to override.
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.FrameLayout);
+ final boolean measureAllChildren = a.getBoolean(
+ com.android.internal.R.styleable.FrameLayout_measureAllChildren, true);
+ setMeasureAllChildren(measureAllChildren);
+ a.recycle();
+ }
+
+ /**
+ * Sets which child view will be displayed.
+ *
+ * @param whichChild the index of the child view to display
+ */
+ @android.view.RemotableViewMethod
+ 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.
+ */
+ @android.view.RemotableViewMethod
+ public void showNext() {
+ setDisplayedChild(mWhichChild + 1);
+ }
+
+ /**
+ * Manually shows the previous child.
+ */
+ @android.view.RemotableViewMethod
+ public void showPrevious() {
+ setDisplayedChild(mWhichChild - 1);
+ }
+
+ /**
+ * Shows only the specified child. The other displays Views exit the screen,
+ * optionally with the with the {@link #getOutAnimation() out animation} and
+ * the specified child enters the screen, optionally with the
+ * {@link #getInAnimation() in animation}.
+ *
+ * @param childIndex The index of the child to be shown.
+ * @param animate Whether or not to use the in and out animations, defaults
+ * to true.
+ */
+ void showOnly(int childIndex, boolean animate) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (i == childIndex) {
+ if (animate && mInAnimation != null) {
+ child.startAnimation(mInAnimation);
+ }
+ child.setVisibility(View.VISIBLE);
+ mFirstTime = false;
+ } else {
+ if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
+ child.startAnimation(mOutAnimation);
+ } else if (child.getAnimation() == mInAnimation)
+ child.clearAnimation();
+ child.setVisibility(View.GONE);
+ }
+ }
+ }
+ /**
+ * 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 boolean animate = (!mFirstTime || mAnimateFirstTime);
+ showOnly(childIndex, animate);
+ }
+
+ @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);
+ }
+ if (index >= 0 && mWhichChild >= index) {
+ // Added item above current one, increment the index of the displayed child
+ setDisplayedChild(mWhichChild + 1);
+ }
+ }
+
+ @Override
+ public void removeAllViews() {
+ super.removeAllViews();
+ mWhichChild = 0;
+ mFirstTime = true;
+ }
+
+ @Override
+ public void removeView(View view) {
+ final int index = indexOfChild(view);
+ if (index >= 0) {
+ removeViewAt(index);
+ }
+ }
+
+ @Override
+ public void removeViewAt(int index) {
+ super.removeViewAt(index);
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ mWhichChild = 0;
+ mFirstTime = true;
+ } else if (mWhichChild >= childCount) {
+ // Displayed is above child count, so float down to top of stack
+ setDisplayedChild(childCount - 1);
+ } else if (mWhichChild == index) {
+ // Displayed was removed, so show the new child living in its place
+ setDisplayedChild(mWhichChild);
+ }
+ }
+
+ public void removeViewInLayout(View view) {
+ removeView(view);
+ }
+
+ public void removeViews(int start, int count) {
+ super.removeViews(start, count);
+ if (getChildCount() == 0) {
+ mWhichChild = 0;
+ mFirstTime = true;
+ } else if (mWhichChild >= start && mWhichChild < start + count) {
+ // Try showing new displayed child, wrapping if needed
+ setDisplayedChild(mWhichChild);
+ }
+ }
+
+ public void removeViewsInLayout(int start, int count) {
+ removeViews(start, count);
+ }
+
+ /**
+ * 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, @AnimRes 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, @AnimRes int resourceID) {
+ setOutAnimation(AnimationUtils.loadAnimation(context, resourceID));
+ }
+
+ /**
+ * Returns whether the current View should be animated the first time the ViewAnimator
+ * is displayed.
+ *
+ * @return true if the current View will be animated the first time it is displayed,
+ * false otherwise.
+ *
+ * @see #setAnimateFirstView(boolean)
+ */
+ public boolean getAnimateFirstView() {
+ return mAnimateFirstTime;
+ }
+
+ /**
+ * Indicates whether the current View should be animated the first time
+ * the ViewAnimator 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();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ViewAnimator.class.getName();
+ }
+}
diff --git a/android/widget/ViewFlipper.java b/android/widget/ViewFlipper.java
new file mode 100644
index 0000000..e769d71
--- /dev/null
+++ b/android/widget/ViewFlipper.java
@@ -0,0 +1,220 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * 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.
+ *
+ * @attr ref android.R.styleable#ViewFlipper_flipInterval
+ * @attr ref android.R.styleable#ViewFlipper_autoStart
+ */
+@RemoteView
+public class ViewFlipper extends ViewAnimator {
+ private static final String TAG = "ViewFlipper";
+ private static final boolean LOGD = false;
+
+ private static final int DEFAULT_INTERVAL = 3000;
+
+ private int mFlipInterval = DEFAULT_INTERVAL;
+ private boolean mAutoStart = false;
+
+ private boolean mRunning = false;
+ private boolean mStarted = false;
+ private boolean mVisible = false;
+ private boolean mUserPresent = true;
+
+ 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, DEFAULT_INTERVAL);
+ mAutoStart = a.getBoolean(
+ com.android.internal.R.styleable.ViewFlipper_autoStart, false);
+ a.recycle();
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ mUserPresent = false;
+ updateRunning();
+ } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+ mUserPresent = true;
+ updateRunning(false);
+ }
+ }
+ };
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // Listen for broadcasts related to user-presence
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+
+ // OK, this is gross but needed. This class is supported by the
+ // remote views machanism and as a part of that the remote views
+ // can be inflated by a context for another user without the app
+ // having interact users permission - just for loading resources.
+ // For exmaple, when adding widgets from a user profile to the
+ // home screen. Therefore, we register the receiver as the current
+ // user not the one the context is for.
+ getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
+ filter, null, getHandler());
+
+ if (mAutoStart) {
+ // Automatically start when requested
+ startFlipping();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mVisible = false;
+
+ getContext().unregisterReceiver(mReceiver);
+ updateRunning();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mVisible = visibility == VISIBLE;
+ updateRunning(false);
+ }
+
+ /**
+ * How long to wait before flipping to the next view
+ *
+ * @param milliseconds
+ * time in milliseconds
+ */
+ @android.view.RemotableViewMethod
+ public void setFlipInterval(int milliseconds) {
+ mFlipInterval = milliseconds;
+ }
+
+ /**
+ * Start a timer to cycle through child views
+ */
+ public void startFlipping() {
+ mStarted = true;
+ updateRunning();
+ }
+
+ /**
+ * No more flips
+ */
+ public void stopFlipping() {
+ mStarted = false;
+ updateRunning();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ViewFlipper.class.getName();
+ }
+
+ /**
+ * Internal method to start or stop dispatching flip {@link Message} based
+ * on {@link #mRunning} and {@link #mVisible} state.
+ */
+ private void updateRunning() {
+ updateRunning(true);
+ }
+
+ /**
+ * Internal method to start or stop dispatching flip {@link Message} based
+ * on {@link #mRunning} and {@link #mVisible} state.
+ *
+ * @param flipNow Determines whether or not to execute the animation now, in
+ * addition to queuing future flips. If omitted, defaults to
+ * true.
+ */
+ private void updateRunning(boolean flipNow) {
+ boolean running = mVisible && mStarted && mUserPresent;
+ if (running != mRunning) {
+ if (running) {
+ showOnly(mWhichChild, flipNow);
+ postDelayed(mFlipRunnable, mFlipInterval);
+ } else {
+ removeCallbacks(mFlipRunnable);
+ }
+ mRunning = running;
+ }
+ if (LOGD) {
+ Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+ + ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
+ }
+ }
+
+ /**
+ * Returns true if the child views are flipping.
+ */
+ public boolean isFlipping() {
+ return mStarted;
+ }
+
+ /**
+ * Set if this view automatically calls {@link #startFlipping()} when it
+ * becomes attached to a window.
+ */
+ public void setAutoStart(boolean autoStart) {
+ mAutoStart = autoStart;
+ }
+
+ /**
+ * Returns true if this view automatically calls {@link #startFlipping()}
+ * when it becomes attached to a window.
+ */
+ public boolean isAutoStart() {
+ return mAutoStart;
+ }
+
+ private final Runnable mFlipRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mRunning) {
+ showNext();
+ postDelayed(mFlipRunnable, mFlipInterval);
+ }
+ }
+ };
+}
diff --git a/android/widget/ViewSwitcher.java b/android/widget/ViewSwitcher.java
new file mode 100644
index 0000000..0d5627e
--- /dev/null
+++ b/android/widget/ViewSwitcher.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+/**
+ * {@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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ViewSwitcher.class.getName();
+ }
+
+ /**
+ * 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.MATCH_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/android/widget/WrapperListAdapter.java b/android/widget/WrapperListAdapter.java
new file mode 100644
index 0000000..7fe12ae
--- /dev/null
+++ b/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/android/widget/YearPickerView.java b/android/widget/YearPickerView.java
new file mode 100644
index 0000000..824fec8
--- /dev/null
+++ b/android/widget/YearPickerView.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2014 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.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.R;
+
+/**
+ * Displays a selectable list of years.
+ */
+class YearPickerView extends ListView {
+ private final YearAdapter mAdapter;
+ private final int mViewSize;
+ private final int mChildSize;
+
+ private OnYearSelectedListener mOnYearSelectedListener;
+
+ public YearPickerView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.listViewStyle);
+ }
+
+ public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final LayoutParams frame = new LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ setLayoutParams(frame);
+
+ final Resources res = context.getResources();
+ mViewSize = res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height);
+ mChildSize = res.getDimensionPixelOffset(R.dimen.datepicker_year_label_height);
+
+ setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final int year = mAdapter.getYearForPosition(position);
+ mAdapter.setSelection(year);
+
+ if (mOnYearSelectedListener != null) {
+ mOnYearSelectedListener.onYearChanged(YearPickerView.this, year);
+ }
+ }
+ });
+
+ mAdapter = new YearAdapter(getContext());
+ setAdapter(mAdapter);
+ }
+
+ public void setOnYearSelectedListener(OnYearSelectedListener listener) {
+ mOnYearSelectedListener = listener;
+ }
+
+ /**
+ * Sets the currently selected year. Jumps immediately to the new year.
+ *
+ * @param year the target year
+ */
+ public void setYear(final int year) {
+ mAdapter.setSelection(year);
+
+ post(new Runnable() {
+ @Override
+ public void run() {
+ final int position = mAdapter.getPositionForYear(year);
+ if (position >= 0 && position < getCount()) {
+ setSelectionCentered(position);
+ }
+ }
+ });
+ }
+
+ public void setSelectionCentered(int position) {
+ final int offset = mViewSize / 2 - mChildSize / 2;
+ setSelectionFromTop(position, offset);
+ }
+
+ public void setRange(Calendar min, Calendar max) {
+ mAdapter.setRange(min, max);
+ }
+
+ private static class YearAdapter extends BaseAdapter {
+ private static final int ITEM_LAYOUT = R.layout.year_label_text_view;
+ private static final int ITEM_TEXT_APPEARANCE =
+ R.style.TextAppearance_Material_DatePicker_List_YearLabel;
+ private static final int ITEM_TEXT_ACTIVATED_APPEARANCE =
+ R.style.TextAppearance_Material_DatePicker_List_YearLabel_Activated;
+
+ private final LayoutInflater mInflater;
+
+ private int mActivatedYear;
+ private int mMinYear;
+ private int mCount;
+
+ public YearAdapter(Context context) {
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setRange(Calendar minDate, Calendar maxDate) {
+ final int minYear = minDate.get(Calendar.YEAR);
+ final int count = maxDate.get(Calendar.YEAR) - minYear + 1;
+
+ if (mMinYear != minYear || mCount != count) {
+ mMinYear = minYear;
+ mCount = count;
+ notifyDataSetInvalidated();
+ }
+ }
+
+ public boolean setSelection(int year) {
+ if (mActivatedYear != year) {
+ mActivatedYear = year;
+ notifyDataSetChanged();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public Integer getItem(int position) {
+ return getYearForPosition(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getYearForPosition(position);
+ }
+
+ public int getPositionForYear(int year) {
+ return year - mMinYear;
+ }
+
+ public int getYearForPosition(int position) {
+ return mMinYear + position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final TextView v;
+ final boolean hasNewView = convertView == null;
+ if (hasNewView) {
+ v = (TextView) mInflater.inflate(ITEM_LAYOUT, parent, false);
+ } else {
+ v = (TextView) convertView;
+ }
+
+ final int year = getYearForPosition(position);
+ final boolean activated = mActivatedYear == year;
+
+ if (hasNewView || v.isActivated() != activated) {
+ final int textAppearanceResId;
+ if (activated && ITEM_TEXT_ACTIVATED_APPEARANCE != 0) {
+ textAppearanceResId = ITEM_TEXT_ACTIVATED_APPEARANCE;
+ } else {
+ textAppearanceResId = ITEM_TEXT_APPEARANCE;
+ }
+ v.setTextAppearance(textAppearanceResId);
+ v.setActivated(activated);
+ }
+
+ v.setText(Integer.toString(year));
+ return v;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+ }
+
+ public int getFirstPositionOffset() {
+ final View firstChild = getChildAt(0);
+ if (firstChild == null) {
+ return 0;
+ }
+ return firstChild.getTop();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+
+ // There are a bunch of years, so don't bother.
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ event.setFromIndex(0);
+ event.setToIndex(0);
+ }
+ }
+
+ /**
+ * The callback used to indicate the user changed the year.
+ */
+ public interface OnYearSelectedListener {
+ /**
+ * Called upon a year change.
+ *
+ * @param view The view associated with this listener.
+ * @param year The year that was set.
+ */
+ void onYearChanged(YearPickerView view, int year);
+ }
+}
\ No newline at end of file
diff --git a/android/widget/ZoomButton.java b/android/widget/ZoomButton.java
new file mode 100644
index 0000000..5cff335
--- /dev/null
+++ b/android/widget/ZoomButton.java
@@ -0,0 +1,125 @@
+/*
+ * 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.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+
+/**
+ * This widget provides a simple utility for turning a continued long-press event
+ * into a series of clicks at some set frequency. There is no actual 'zoom' functionality
+ * handled by this widget directly. Instead, clients of this API should set up an
+ * {@link View#setOnClickListener(OnClickListener) onClickListener} to handle
+ * zoom functionality. That click listener is called on a frequency
+ * determined by {@link #setZoomSpeed(long)} whenever the user long-presses
+ * on the ZoomButton.
+ *
+ * @deprecated Use other means to handle this functionality. This widget is merely a
+ * simple wrapper around a long-press handler.
+ */
+@Deprecated
+public class ZoomButton extends ImageButton implements OnLongClickListener {
+
+ private final Runnable mRunnable = new Runnable() {
+ public void run() {
+ if (hasOnClickListeners() && mIsInLongpress && isEnabled()) {
+ callOnClick();
+ postDelayed(this, mZoomSpeed);
+ }
+ }
+ };
+
+ 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 defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setOnLongClickListener(this);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if ((event.getAction() == MotionEvent.ACTION_CANCEL)
+ || (event.getAction() == MotionEvent.ACTION_UP)) {
+ mIsInLongpress = false;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * Sets the delay between calls to the widget's {@link View#setOnClickListener(OnClickListener)
+ * onClickListener}.
+ *
+ * @param speed The delay between calls to the click listener, in milliseconds
+ */
+ public void setZoomSpeed(long speed) {
+ mZoomSpeed = speed;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ mIsInLongpress = true;
+ 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);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ZoomButton.class.getName();
+ }
+}
diff --git a/android/widget/ZoomButtonsController.java b/android/widget/ZoomButtonsController.java
new file mode 100644
index 0000000..1a3ca86
--- /dev/null
+++ b/android/widget/ZoomButtonsController.java
@@ -0,0 +1,704 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+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.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+/*
+ * Implementation notes:
+ * - The zoom controls are displayed in their own window.
+ * (Easier for the client and better performance)
+ * - This window is never touchable, and by default is not focusable.
+ * Its rect is quite big (fills horizontally) but has empty space between the
+ * edges and center. Touches there should be given to the owner. Instead of
+ * having the window touchable and dispatching these empty touch events to the
+ * owner, we set the window to not touchable and steal events from owner
+ * via onTouchListener.
+ * - To make the buttons clickable, it attaches an OnTouchListener to the owner
+ * view and does the hit detection locally (attaches when visible, detaches when invisible).
+ * - When it is focusable, it forwards uninteresting events to the owner view's
+ * view hierarchy.
+ */
+/**
+ * The {@link ZoomButtonsController} handles showing and hiding the zoom
+ * controls and positioning it relative to an owner view. It also gives the
+ * client access to the zoom controls container, allowing for additional
+ * accessory buttons to be shown in the zoom controls window.
+ * <p>
+ * Typically, clients should call {@link #setVisible(boolean) setVisible(true)}
+ * on a touch down or move (no need to call {@link #setVisible(boolean)
+ * setVisible(false)} since it will time out on its own). Also, whenever the
+ * owner cannot be zoomed further, the client should update
+ * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
+ * <p>
+ * If you are using this with a custom View, please call
+ * {@link #setVisible(boolean) setVisible(false)} from
+ * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged}
+ * when <code>visibility != View.VISIBLE</code>.
+ *
+ * @deprecated This functionality and UI is better handled with custom views and layouts
+ * rather than a dedicated zoom-control widget
+ */
+@Deprecated
+public class ZoomButtonsController implements View.OnTouchListener {
+
+ private static final String TAG = "ZoomButtonsController";
+
+ private static final int ZOOM_CONTROLS_TIMEOUT =
+ (int) ViewConfiguration.getZoomControlsTimeout();
+
+ private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
+ private int mTouchPaddingScaledSq;
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private boolean mAutoDismissControls = true;
+
+ /**
+ * The view that is being zoomed by this zoom controller.
+ */
+ private final View mOwnerView;
+
+ /**
+ * The location of the owner view on the screen. This is recalculated
+ * each time the zoom controller is shown.
+ */
+ private final int[] mOwnerViewRawLocation = new int[2];
+
+ /**
+ * The container that is added as a window.
+ */
+ private final FrameLayout mContainer;
+ private LayoutParams mContainerLayoutParams;
+ private final int[] mContainerRawLocation = new int[2];
+
+ private ZoomControls mControls;
+
+ /**
+ * The view (or null) that should receive touch events. This will get set if
+ * the touch down hits the container. It will be reset on the touch up.
+ */
+ private View mTouchTargetView;
+ /**
+ * The {@link #mTouchTargetView}'s location in window, set on touch down.
+ */
+ private final int[] mTouchTargetWindowLocation = new int[2];
+
+ /**
+ * If the zoom controller is dismissed but the user is still in a touch
+ * interaction, we set this to true. This will ignore all touch events until
+ * up/cancel, and then set the owner's touch listener to null.
+ * <p>
+ * Otherwise, the owner view would get mismatched events (i.e., touch move
+ * even though it never got the touch down.)
+ */
+ private boolean mReleaseTouchListenerOnUp;
+
+ /** Whether the container has been added to the window manager. */
+ private boolean mIsVisible;
+
+ private final Rect mTempRect = new Rect();
+ private final int[] mTempIntArray = new int[2];
+
+ private OnZoomListener mCallback;
+
+ /**
+ * When showing the zoom, we add the view as a new window. However, there is
+ * logic that needs to know the size of the zoom which is determined after
+ * it's laid out. Therefore, we must post this logic onto the UI thread so
+ * it will be exceuted AFTER the layout. This is the logic.
+ */
+ private Runnable mPostedVisibleInitializer;
+
+ private final IntentFilter mConfigurationChangedFilter =
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
+
+ /**
+ * Needed to reposition the zoom controls after configuration changes.
+ */
+ private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mIsVisible) return;
+
+ mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
+ mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
+ }
+ };
+
+ /** When configuration changes, this is called after the UI thread is idle. */
+ private static final int MSG_POST_CONFIGURATION_CHANGED = 2;
+ /** Used to delay the zoom controller dismissal. */
+ private static final int MSG_DISMISS_ZOOM_CONTROLS = 3;
+ /**
+ * If setVisible(true) is called and the owner view's window token is null,
+ * we delay the setVisible(true) call until it is not null.
+ */
+ private static final int MSG_POST_SET_VISIBLE = 4;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_POST_CONFIGURATION_CHANGED:
+ onPostConfigurationChanged();
+ break;
+
+ case MSG_DISMISS_ZOOM_CONTROLS:
+ setVisible(false);
+ break;
+
+ case MSG_POST_SET_VISIBLE:
+ if (mOwnerView.getWindowToken() == null) {
+ // Doh, it is still null, just ignore the set visible call
+ Log.e(TAG,
+ "Cannot make the zoom controller visible if the owner view is " +
+ "not attached to a window.");
+ } else {
+ setVisible(true);
+ }
+ break;
+ }
+
+ }
+ };
+
+ /**
+ * Constructor for the {@link ZoomButtonsController}.
+ *
+ * @param ownerView The view that is being zoomed by the zoom controls. The
+ * zoom controls will be displayed aligned with this view.
+ */
+ public ZoomButtonsController(View ownerView) {
+ mContext = ownerView.getContext();
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mOwnerView = ownerView;
+
+ mTouchPaddingScaledSq = (int)
+ (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
+ mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
+
+ mContainer = createContainer();
+ }
+
+ /**
+ * Whether to enable the zoom in control.
+ *
+ * @param enabled Whether to enable the zoom in control.
+ */
+ public void setZoomInEnabled(boolean enabled) {
+ mControls.setIsZoomInEnabled(enabled);
+ }
+
+ /**
+ * Whether to enable the zoom out control.
+ *
+ * @param enabled Whether to enable the zoom out control.
+ */
+ public void setZoomOutEnabled(boolean enabled) {
+ mControls.setIsZoomOutEnabled(enabled);
+ }
+
+ /**
+ * Sets the delay between zoom callbacks as the user holds a zoom button.
+ *
+ * @param speed The delay in milliseconds between zoom callbacks.
+ */
+ public void setZoomSpeed(long speed) {
+ mControls.setZoomSpeed(speed);
+ }
+
+ private FrameLayout createContainer() {
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ // Controls are positioned BOTTOM | CENTER with respect to the owner view.
+ lp.gravity = Gravity.TOP | Gravity.START;
+ lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
+ LayoutParams.FLAG_NOT_FOCUSABLE |
+ LayoutParams.FLAG_LAYOUT_NO_LIMITS |
+ LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ lp.height = LayoutParams.WRAP_CONTENT;
+ lp.width = LayoutParams.MATCH_PARENT;
+ lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
+ lp.format = PixelFormat.TRANSLUCENT;
+ lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
+ mContainerLayoutParams = lp;
+
+ FrameLayout container = new Container(mContext);
+ container.setLayoutParams(lp);
+ container.setMeasureAllChildren(true);
+
+ LayoutInflater inflater = (LayoutInflater) mContext
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(com.android.internal.R.layout.zoom_container, container);
+
+ mControls = container.findViewById(com.android.internal.R.id.zoomControls);
+ mControls.setOnZoomInClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+ if (mCallback != null) mCallback.onZoom(true);
+ }
+ });
+ mControls.setOnZoomOutClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+ if (mCallback != null) mCallback.onZoom(false);
+ }
+ });
+
+ return container;
+ }
+
+ /**
+ * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
+ *
+ * @param listener The listener that will be told to zoom.
+ */
+ public void setOnZoomListener(OnZoomListener listener) {
+ mCallback = listener;
+ }
+
+ /**
+ * Sets whether the zoom controls should be focusable. If the controls are
+ * focusable, then trackball and arrow key interactions are possible.
+ * Otherwise, only touch interactions are possible.
+ *
+ * @param focusable Whether the zoom controls should be focusable.
+ */
+ public void setFocusable(boolean focusable) {
+ int oldFlags = mContainerLayoutParams.flags;
+ if (focusable) {
+ mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
+ } else {
+ mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+
+ if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
+ mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
+ }
+ }
+
+ /**
+ * Whether the zoom controls will be automatically dismissed after showing.
+ *
+ * @return Whether the zoom controls will be auto dismissed after showing.
+ */
+ public boolean isAutoDismissed() {
+ return mAutoDismissControls;
+ }
+
+ /**
+ * Sets whether the zoom controls will be automatically dismissed after
+ * showing.
+ */
+ public void setAutoDismissed(boolean autoDismiss) {
+ if (mAutoDismissControls == autoDismiss) return;
+ mAutoDismissControls = autoDismiss;
+ }
+
+ /**
+ * Whether the zoom controls are visible to the user.
+ *
+ * @return Whether the zoom controls are visible to the user.
+ */
+ public boolean isVisible() {
+ return mIsVisible;
+ }
+
+ /**
+ * Sets whether the zoom controls should be visible to the user.
+ *
+ * @param visible Whether the zoom controls should be visible to the user.
+ */
+ public void setVisible(boolean visible) {
+
+ if (visible) {
+ if (mOwnerView.getWindowToken() == null) {
+ /*
+ * We need a window token to show ourselves, maybe the owner's
+ * window hasn't been created yet but it will have been by the
+ * time the looper is idle, so post the setVisible(true) call.
+ */
+ if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
+ mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
+ }
+ return;
+ }
+
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+ }
+
+ if (mIsVisible == visible) {
+ return;
+ }
+ mIsVisible = visible;
+
+ if (visible) {
+ if (mContainerLayoutParams.token == null) {
+ mContainerLayoutParams.token = mOwnerView.getWindowToken();
+ }
+
+ mWindowManager.addView(mContainer, mContainerLayoutParams);
+
+ if (mPostedVisibleInitializer == null) {
+ mPostedVisibleInitializer = new Runnable() {
+ public void run() {
+ refreshPositioningVariables();
+
+ if (mCallback != null) {
+ mCallback.onVisibilityChanged(true);
+ }
+ }
+ };
+ }
+
+ mHandler.post(mPostedVisibleInitializer);
+
+ // Handle configuration changes when visible
+ mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
+
+ // Steal touches events from the owner
+ mOwnerView.setOnTouchListener(this);
+ mReleaseTouchListenerOnUp = false;
+
+ } else {
+ // Don't want to steal any more touches
+ if (mTouchTargetView != null) {
+ // We are still stealing the touch events for this touch
+ // sequence, so release the touch listener later
+ mReleaseTouchListenerOnUp = true;
+ } else {
+ mOwnerView.setOnTouchListener(null);
+ }
+
+ // No longer care about configuration changes
+ mContext.unregisterReceiver(mConfigurationChangedReceiver);
+
+ mWindowManager.removeViewImmediate(mContainer);
+ mHandler.removeCallbacks(mPostedVisibleInitializer);
+
+ if (mCallback != null) {
+ mCallback.onVisibilityChanged(false);
+ }
+ }
+
+ }
+
+ /**
+ * Gets the container that is the parent of the zoom controls.
+ * <p>
+ * The client can add other views to this container to link them with the
+ * zoom controls.
+ *
+ * @return The container of the zoom controls. It will be a layout that
+ * respects the gravity of a child's layout parameters.
+ */
+ public ViewGroup getContainer() {
+ return mContainer;
+ }
+
+ /**
+ * Gets the view for the zoom controls.
+ *
+ * @return The zoom controls view.
+ */
+ public View getZoomControls() {
+ return mControls;
+ }
+
+ private void dismissControlsDelayed(int delay) {
+ if (mAutoDismissControls) {
+ mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
+ }
+ }
+
+ private void refreshPositioningVariables() {
+ // if the mOwnerView is detached from window then skip.
+ if (mOwnerView.getWindowToken() == null) return;
+
+ // Position the zoom controls on the bottom of the owner view.
+ int ownerHeight = mOwnerView.getHeight();
+ int ownerWidth = mOwnerView.getWidth();
+ // The gap between the top of the owner and the top of the container
+ int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
+
+ // Calculate the owner view's bounds
+ mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
+ mContainerRawLocation[0] = mOwnerViewRawLocation[0];
+ mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
+
+ int[] ownerViewWindowLoc = mTempIntArray;
+ mOwnerView.getLocationInWindow(ownerViewWindowLoc);
+
+ // lp.x and lp.y should be relative to the owner's window top-left
+ mContainerLayoutParams.x = ownerViewWindowLoc[0];
+ mContainerLayoutParams.width = ownerWidth;
+ mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
+ if (mIsVisible) {
+ mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
+ }
+
+ }
+
+ /* This will only be called when the container has focus. */
+ private boolean onContainerKey(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (isInterestingKey(keyCode)) {
+
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0) {
+ if (mOwnerView != null) {
+ KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
+ if (ds != null) {
+ ds.startTracking(event, this);
+ }
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP
+ && event.isTracking() && !event.isCanceled()) {
+ setVisible(false);
+ return true;
+ }
+
+ } else {
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+ }
+
+ // Let the container handle the key
+ return false;
+
+ } else {
+
+ ViewRootImpl viewRoot = mOwnerView.getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.dispatchInputEvent(event);
+ }
+
+ // We gave the key to the owner, don't let the container handle this key
+ return true;
+ }
+ }
+
+ private boolean isInterestingKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ 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_BACK:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * @hide The ZoomButtonsController implements the OnTouchListener, but this
+ * does not need to be shown in its public API.
+ */
+ public boolean onTouch(View v, MotionEvent event) {
+ int action = event.getAction();
+
+ if (event.getPointerCount() > 1) {
+ // ZoomButtonsController doesn't handle mutitouch. Give up control.
+ return false;
+ }
+
+ if (mReleaseTouchListenerOnUp) {
+ // The controls were dismissed but we need to throw away all events until the up
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mOwnerView.setOnTouchListener(null);
+ setTouchTargetView(null);
+ mReleaseTouchListenerOnUp = false;
+ }
+
+ // Eat this event
+ return true;
+ }
+
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+
+ View targetView = mTouchTargetView;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
+ setTouchTargetView(targetView);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ setTouchTargetView(null);
+ break;
+ }
+
+ if (targetView != null) {
+ // The upperleft corner of the target view in raw coordinates
+ int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
+ int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
+
+ MotionEvent containerEvent = MotionEvent.obtain(event);
+ // Convert the motion event into the target view's coordinates (from
+ // owner view's coordinates)
+ containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
+ mOwnerViewRawLocation[1] - targetViewRawY);
+ /* Disallow negative coordinates (which can occur due to
+ * ZOOM_CONTROLS_TOUCH_PADDING) */
+ // These are floats because we need to potentially offset away this exact amount
+ float containerX = containerEvent.getX();
+ float containerY = containerEvent.getY();
+ if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
+ containerEvent.offsetLocation(-containerX, 0);
+ }
+ if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
+ containerEvent.offsetLocation(0, -containerY);
+ }
+ boolean retValue = targetView.dispatchTouchEvent(containerEvent);
+ containerEvent.recycle();
+ return retValue;
+
+ } else {
+ return false;
+ }
+ }
+
+ private void setTouchTargetView(View view) {
+ mTouchTargetView = view;
+ if (view != null) {
+ view.getLocationInWindow(mTouchTargetWindowLocation);
+ }
+ }
+
+ /**
+ * Returns the View that should receive a touch at the given coordinates.
+ *
+ * @param rawX The raw X.
+ * @param rawY The raw Y.
+ * @return The view that should receive the touches, or null if there is not one.
+ */
+ private View findViewForTouch(int rawX, int rawY) {
+ // Reverse order so the child drawn on top gets first dibs.
+ int containerCoordsX = rawX - mContainerRawLocation[0];
+ int containerCoordsY = rawY - mContainerRawLocation[1];
+ Rect frame = mTempRect;
+
+ View closestChild = null;
+ int closestChildDistanceSq = Integer.MAX_VALUE;
+
+ for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
+ View child = mContainer.getChildAt(i);
+ if (child.getVisibility() != View.VISIBLE) {
+ continue;
+ }
+
+ child.getHitRect(frame);
+ if (frame.contains(containerCoordsX, containerCoordsY)) {
+ return child;
+ }
+
+ int distanceX;
+ if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
+ distanceX = 0;
+ } else {
+ distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
+ Math.abs(containerCoordsX - frame.right));
+ }
+ int distanceY;
+ if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
+ distanceY = 0;
+ } else {
+ distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
+ Math.abs(containerCoordsY - frame.bottom));
+ }
+ int distanceSq = distanceX * distanceX + distanceY * distanceY;
+
+ if ((distanceSq < mTouchPaddingScaledSq) &&
+ (distanceSq < closestChildDistanceSq)) {
+ closestChild = child;
+ closestChildDistanceSq = distanceSq;
+ }
+ }
+
+ return closestChild;
+ }
+
+ private void onPostConfigurationChanged() {
+ dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+ refreshPositioningVariables();
+ }
+
+ /**
+ * Interface that will be called when the user performs an interaction that
+ * triggers some action, for example zooming.
+ */
+ public interface OnZoomListener {
+
+ /**
+ * Called when the zoom controls' visibility changes.
+ *
+ * @param visible Whether the zoom controls are visible.
+ */
+ void onVisibilityChanged(boolean visible);
+
+ /**
+ * Called when the owner view needs to be zoomed.
+ *
+ * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
+ */
+ void onZoom(boolean zoomIn);
+ }
+
+ private class Container extends FrameLayout {
+ public Container(Context context) {
+ super(context);
+ }
+
+ /*
+ * Need to override this to intercept the key events. Otherwise, we
+ * would attach a key listener to the container but its superclass
+ * ViewGroup gives it to the focused View instead of calling the key
+ * listener, and so we wouldn't get the events.
+ */
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
+ }
+ }
+
+}
diff --git a/android/widget/ZoomControls.java b/android/widget/ZoomControls.java
new file mode 100644
index 0000000..66c052b
--- /dev/null
+++ b/android/widget/ZoomControls.java
@@ -0,0 +1,114 @@
+/*
+ * 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();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return ZoomControls.class.getName();
+ }
+}