diff options
Diffstat (limited to 'core/java/android/widget')
34 files changed, 4150 insertions, 1066 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 094f195..82dd5db 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -55,6 +55,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -2532,6 +2533,21 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return mContextMenuInfo; } + /** @hide */ + @Override + public boolean showContextMenu(float x, float y, int metaState) { + 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); + return super.showContextMenuForChild(AbsListView.this); + } + } + return super.showContextMenu(x, y, metaState); + } + @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); @@ -2806,7 +2822,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down event // was touching the edge, we will bail out and try again. - // This allows the edge correcting code in ViewRoot to try to + // This allows the edge correcting code in ViewAncestor to try to // find a nearby view to select return false; } @@ -2834,6 +2850,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } } + + if (performButtonActionOnTouchDown(ev)) { + if (mTouchMode == TOUCH_MODE_DOWN) { + removeCallbacks(mPendingCheckForTap); + } + } break; } @@ -4529,8 +4551,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Otherwise resurrects the selection and returns true if resurrected. */ boolean resurrectSelectionIfNeeded() { - if (mSelectedPosition < 0) { - return resurrectSelection(); + if (mSelectedPosition < 0 && resurrectSelection()) { + updateSelectorState(); + return true; } return false; } @@ -5009,7 +5032,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te 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 ViewRoot to dispatch events with. + // no ViewAncestor to dispatch events with. return mDefInputConnection.sendKeyEvent(event); } }; diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 0da73a4..2621e64 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -201,7 +201,8 @@ public abstract class AbsSeekBar extends ProgressBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { + void onProgressRefresh(float scale, boolean fromUser) { + super.onProgressRefresh(scale, fromUser); Drawable thumb = mThumb; if (thumb != null) { setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index ac82af7..7df6aab 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -141,6 +141,11 @@ public class AbsoluteLayout extends ViewGroup { return new LayoutParams(p); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * Per-child layout information associated with AbsoluteLayout. * See diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index f16efbd..c4d05e9 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -876,7 +876,6 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = false; // This is an exceptional case which occurs when a window gets the // focus and sends a focus event via its focused child to announce // current focus/selection. AdapterView fires selection but not focus @@ -885,22 +884,43 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); } - // we send selection events only from AdapterView to avoid - // generation of such event for each child + // We first get a chance to populate the event. + onPopulateAccessibilityEvent(event); + + return false; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + // We send selection events only from AdapterView to avoid + // generation of such event for each child. View selectedView = getSelectedView(); if (selectedView != null) { - populated = selectedView.dispatchPopulateAccessibilityEvent(event); + selectedView.dispatchPopulateAccessibilityEvent(event); } + } - if (!populated) { - if (selectedView != null) { - event.setEnabled(selectedView.isEnabled()); - } - event.setItemCount(getCount()); - event.setCurrentItemIndex(getSelectedItemPosition()); - } + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + // Add a record for ourselves as well. + AccessibilityEvent record = AccessibilityEvent.obtain(); + // Set the class since it is not populated in #dispatchPopulateAccessibilityEvent + record.setClassName(getClass().getName()); + child.dispatchPopulateAccessibilityEvent(record); + event.appendRecord(record); + return true; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); - return populated; + View selectedView = getSelectedView(); + if (selectedView != null) { + event.setEnabled(selectedView.isEnabled()); + } + event.setItemCount(getCount()); + event.setCurrentItemIndex(getSelectedItemPosition()); } @Override diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 072992e..c773527 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -79,7 +79,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> /** * Map of the children of the {@link AdapterViewAnimator}. */ - HashMap<Integer, ViewAndIndex> mViewsMap = new HashMap<Integer, ViewAndIndex>(); + HashMap<Integer, ViewAndMetaData> mViewsMap = new HashMap<Integer, ViewAndMetaData>(); /** * List of views pending removal from the {@link AdapterViewAnimator} @@ -103,11 +103,6 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int mCurrentWindowStartUnbounded = 0; /** - * Handler to post events to the main thread - */ - Handler mMainQueue; - - /** * Listens for data changes from the adapter */ AdapterDataSetObserver mDataSetObserver; @@ -163,15 +158,18 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> private static final int DEFAULT_ANIMATION_DURATION = 200; public AdapterViewAnimator(Context context) { - super(context); - initViewAnimator(); + this(context, null); } public AdapterViewAnimator(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewAnimator); + com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, 0); int resource = a.getResourceId( com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0); if (resource > 0) { @@ -203,17 +201,21 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> * Initialize this {@link AdapterViewAnimator} */ private void initViewAnimator() { - mMainQueue = new Handler(Looper.myLooper()); mPreviousViews = new ArrayList<Integer>(); } - class ViewAndIndex { - ViewAndIndex(View v, int i) { - view = v; - index = i; - } + class ViewAndMetaData { View view; - int index; + 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; + } } /** @@ -379,6 +381,15 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } } + private ViewAndMetaData getMetaDataForChild(View child) { + for (ViewAndMetaData vm: mViewsMap.values()) { + if (vm.view == child) { + return vm; + } + } + return null; + } + LayoutParams createOrReuseLayoutParams(View v) { final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); if (currentLp instanceof ViewGroup.LayoutParams) { @@ -481,7 +492,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (remove) { View previousView = mViewsMap.get(index).view; - int oldRelativeIndex = mViewsMap.get(index).index; + int oldRelativeIndex = mViewsMap.get(index).relativeIndex; mPreviousViews.add(index); transformViewForTransition(oldRelativeIndex, -1, previousView, animate); @@ -497,7 +508,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int index = modulo(i, getWindowSize()); int oldRelativeIndex; if (mViewsMap.containsKey(index)) { - oldRelativeIndex = mViewsMap.get(index).index; + oldRelativeIndex = mViewsMap.get(index).relativeIndex; } else { oldRelativeIndex = -1; } @@ -510,14 +521,16 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (inOldRange) { View view = mViewsMap.get(index).view; - mViewsMap.get(index).index = newRelativeIndex; + 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 - View newView = mAdapter.getView(modulo(i, adapterCount), null, this); + 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 @@ -527,7 +540,8 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (newView != null) { fl.addView(newView); } - mViewsMap.put(index, new ViewAndIndex(fl, newRelativeIndex)); + mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex, + adapterPosition, itemId)); addChild(fl); applyTransformForChildAtIndex(fl, newRelativeIndex); transformViewForTransition(-1, newRelativeIndex, fl, animate); @@ -604,6 +618,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> 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(); @@ -616,7 +631,12 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> hideTapFeedback(v); post(new Runnable() { public void run() { - performItemClick(v, 0, 0); + if (viewData != null) { + performItemClick(v, viewData.adapterPosition, + viewData.itemId); + } else { + performItemClick(v, 0, 0); + } } }); } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index bf63607..8d4aaea 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -199,11 +199,8 @@ public class CheckedTextView extends TextView implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - if (!populated) { - event.setChecked(mChecked); - } - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 0df45cc..a730018 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -208,22 +208,9 @@ public abstract class CompoundButton extends Button implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - - if (!populated) { - int resourceId = 0; - if (mChecked) { - resourceId = R.string.accessibility_compound_button_selected; - } else { - resourceId = R.string.accessibility_compound_button_unselected; - } - String state = getResources().getString(resourceId); - event.getText().add(state); - event.setChecked(mChecked); - } - - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } @Override diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index 516162a..6c4c39d 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -21,7 +21,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -440,7 +439,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, */ protected void onContentChanged() { if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } } diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java index 3fadf4c..44d1656 100644 --- a/core/java/android/widget/CursorTreeAdapter.java +++ b/core/java/android/widget/CursorTreeAdapter.java @@ -22,7 +22,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.util.SparseArray; import android.view.View; @@ -499,7 +498,7 @@ public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implem @Override public void onChange(boolean selfChange) { if (mAutoRequery && mCursor != null) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 1d442db..30fb927 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -353,13 +353,14 @@ public class DatePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR; String selectedDateUtterance = DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } /** @@ -410,74 +411,28 @@ public class DatePicker extends FrameLayout { } /** - * Reorders the spinners according to the date format in the current - * {@link Locale}. + * 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() { - java.text.DateFormat format; - String order; - - /* - * If the user is in a locale where the medium date format is still - * numeric (Japanese and Czech, for example), respect the date format - * order setting. Otherwise, use the order that the locale says is - * appropriate for a spelled-out date. - */ - - if (getShortMonths()[0].startsWith("1")) { - format = DateFormat.getDateFormat(getContext()); - } else { - format = DateFormat.getMediumDateFormat(getContext()); - } - - if (format instanceof SimpleDateFormat) { - order = ((SimpleDateFormat) format).toPattern(); - } else { - // Shouldn't happen, but just in case. - order = new String(DateFormat.getDateFormatOrder(getContext())); - } - - /* - * Remove the 3 spinners from their parent and then add them back in the - * required order. - */ - LinearLayout parent = mSpinners; - parent.removeAllViews(); - - boolean quoted = false; - boolean didDay = false, didMonth = false, didYear = false; - - for (int i = 0; i < order.length(); i++) { - char c = order.charAt(i); - - if (c == '\'') { - quoted = !quoted; - } - - if (!quoted) { - if (c == DateFormat.DATE && !didDay) { - parent.addView(mDaySpinner); - didDay = true; - } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { - parent.addView(mMonthSpinner); - didMonth = true; - } else if (c == DateFormat.YEAR && !didYear) { - parent.addView(mYearSpinner); - didYear = true; - } + mSpinners.removeAllViews(); + char[] order = DateFormat.getDateFormatOrder(getContext()); + for (int i = 0; i < order.length; i++) { + switch (order[i]) { + case DateFormat.DATE: + mSpinners.addView(mDaySpinner); + break; + case DateFormat.MONTH: + mSpinners.addView(mMonthSpinner); + break; + case DateFormat.YEAR: + mSpinners.addView(mYearSpinner); + break; + default: + throw new IllegalArgumentException(); } } - - // Shouldn't happen, but just in case. - if (!didMonth) { - parent.addView(mMonthSpinner); - } - if (!didDay) { - parent.addView(mDaySpinner); - } - if (!didYear) { - parent.addView(mYearSpinner); - } } /** diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index f862368..ead9b4f 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -599,12 +599,35 @@ public class ExpandableListView extends ListView { * was already expanded, this will return false) */ public boolean expandGroup(int groupPos) { - boolean retValue = mConnector.expandGroup(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) { + PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1)); + 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; } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index f659ead..4ee16e7 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -16,6 +16,8 @@ package android.widget; +import java.util.ArrayList; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -23,14 +25,12 @@ import android.graphics.Rect; import android.graphics.Region; 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.Gravity; import android.widget.RemoteViews.RemoteView; -import java.util.ArrayList; - /** * FrameLayout is designed to block out an area on the screen to display @@ -39,7 +39,7 @@ import java.util.ArrayList; * Children are drawn in a stack, with the most recently added child on top. * The size of the frame layout is the size of its largest child (plus padding), visible * or not (if the FrameLayout's parent permits). Views that are GONE are used for sizing - * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()} + * only if {@link #setMeasureAllChildren(boolean) setMeasureAllChildren()} * is set to true. * * @attr ref android.R.styleable#FrameLayout_foreground @@ -115,7 +115,7 @@ public class FrameLayout extends ViewGroup { } /** - * Describes how the foreground is positioned. Defaults to FILL. + * Describes how the foreground is positioned. Defaults to BEFORE and TOP. * * @param foregroundGravity See {@link android.view.Gravity} * @@ -124,8 +124,8 @@ public class FrameLayout extends ViewGroup { @android.view.RemotableViewMethod public void setForegroundGravity(int foregroundGravity) { if (mForegroundGravity != foregroundGravity) { - if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - foregroundGravity |= Gravity.LEFT; + if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.BEFORE; } if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -364,10 +364,10 @@ public class FrameLayout extends ViewGroup { gravity = DEFAULT_CHILD_GRAVITY; } - final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; - switch (horizontalGravity) { + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: childLeft = parentLeft + lp.leftMargin; break; @@ -436,7 +436,7 @@ public class FrameLayout extends ViewGroup { } Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), - foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + foreground.getIntrinsicHeight(), selfBounds, overlayBounds, isLayoutRtl()); foreground.setBounds(overlayBounds); } @@ -485,6 +485,11 @@ public class FrameLayout extends ViewGroup { return new FrameLayout.LayoutParams(getContext(), attrs); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * {@inheritDoc} */ @@ -566,4 +571,3 @@ public class FrameLayout extends ViewGroup { } } } - diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java new file mode 100644 index 0000000..b99cd7f --- /dev/null +++ b/core/java/android/widget/GridLayout.java @@ -0,0 +1,2262 @@ +/* + * 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.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import com.android.internal.R.styleable; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.UNSPECIFIED; +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * 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</code> columns + * has <code>N + 1</code> grid indices that run from <code>0</code> + * through <code>N</code> inclusive. Regardless of how GridLayout is + * configured, grid index <code>0</code> is fixed to the leading edge of the + * container and grid index <code>N</code> is fixed to its trailing edge + * (after padding is taken into account). + * + * <h4>Row and Column Groups</h4> + * + * Children occupy one or more contiguous cells, as defined + * by their {@link GridLayout.LayoutParams#rowGroup rowGroup} and + * {@link GridLayout.LayoutParams#columnGroup columnGroup} layout parameters. + * Each group specifies 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 no child specifies 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 child's visual characteristics. Each of the + * margins so defined may be independently overridden by an assignment + * to the appropriate layout parameter. + * + * <h4>Excess Space Distribution</h4> + * + * Like {@link LinearLayout}, a child's ability to stretch is controlled + * using <em>weights</em>, which are specified using the + * {@link GridLayout.LayoutParams#rowWeight rowWeight} and + * {@link GridLayout.LayoutParams#columnWeight columnWeight} layout parameters. + * <p> + * <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 + */ +public class GridLayout extends ViewGroup { + + // Public constants + + /** + * 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; + + // Misc constants + + private static final String TAG = GridLayout.class.getName(); + private static final boolean DEBUG = false; + private static final Paint GRID_PAINT = new Paint(); + private static final double GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; + private static final int MIN = 0; + private static final int PRF = 1; + private static final int MAX = 2; + + // 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 = false; + private static final boolean DEFAULT_MARGINS_INCLUDED = true; + // todo remove this + private static final int DEFAULT_CONTAINER_MARGIN = 20; + + // TypedArray indices + + private static final int ORIENTATION = styleable.GridLayout_orientation; + private static final int ROW_COUNT = styleable.GridLayout_rowCount; + private static final int COLUMN_COUNT = styleable.GridLayout_columnCount; + private static final int USE_DEFAULT_MARGINS = styleable.GridLayout_useDefaultMargins; + private static final int MARGINS_INCLUDED = styleable.GridLayout_marginsIncludedInAlignment; + private static final int ROW_ORDER_PRESERVED = styleable.GridLayout_rowOrderPreserved; + private static final int COLUMN_ORDER_PRESERVED = styleable.GridLayout_columnOrderPreserved; + + // Static initialization + + static { + GRID_PAINT.setColor(Color.argb(50, 255, 255, 255)); + } + + // Instance variables + + private final Axis mHorizontalAxis = new Axis(true); + private final Axis mVerticalAxis = new Axis(false); + private boolean mLayoutParamsValid = false; + private int mOrientation = DEFAULT_ORIENTATION; + private boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS; + private boolean mMarginsIncludedInAlignment = DEFAULT_MARGINS_INCLUDED; + private int mDefaultGravity = Gravity.NO_GRAVITY; + + /* package */ boolean accommodateBothMinAndMax = false; + + // Constructors + + /** + * {@inheritDoc} + */ + public GridLayout(Context context) { + super(context); + if (DEBUG) { + setWillNotDraw(false); + } + } + + /** + * {@inheritDoc} + */ + public GridLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + processAttributes(context, attrs); + } + + /** + * {@inheritDoc} + */ + public GridLayout(Context context, AttributeSet attrs) { + super(context, attrs); + processAttributes(context, attrs); + } + + private void processAttributes(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout); + try { + setRowCount(a.getInteger(ROW_COUNT, DEFAULT_COUNT)); + setColumnCount(a.getInteger(COLUMN_COUNT, DEFAULT_COUNT)); + mOrientation = a.getInteger(ORIENTATION, DEFAULT_ORIENTATION); + mUseDefaultMargins = a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS); + mMarginsIncludedInAlignment = a.getBoolean(MARGINS_INCLUDED, DEFAULT_MARGINS_INCLUDED); + 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}. The default + * is {@link #HORIZONTAL}. + * + * @see #setOrientation(int) + * + * @attr ref android.R.styleable#GridLayout_orientation + */ + public int getOrientation() { + return mOrientation; + } + + /** + * The orientation property does not affect layout. Orientation is used + * only to generate default row/column indices when they are not specified + * by a component's layout parameters. + * + * @param orientation the orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}. + * + * @see #getOrientation() + * + * @attr ref android.R.styleable#GridLayout_orientation + */ + public void setOrientation(int orientation) { + if (mOrientation != orientation) { + mOrientation = orientation; + 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#rowGroup}. + * + * @return the current number of rows + * + * @see #setRowCount(int) + * @see LayoutParams#rowGroup + * + * @attr ref android.R.styleable#GridLayout_rowCount + */ + public int getRowCount() { + return mVerticalAxis.getCount(); + } + + /** + * The rowCount property does not affect layout. 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#rowGroup + * + * @attr ref android.R.styleable#GridLayout_rowCount + */ + public void setRowCount(int rowCount) { + mVerticalAxis.setCount(rowCount); + } + + /** + * 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#columnGroup}. + * + * @return the current number of columns + * + * @see #setColumnCount(int) + * @see LayoutParams#columnGroup + * + * @attr ref android.R.styleable#GridLayout_columnCount + */ + public int getColumnCount() { + return mHorizontalAxis.getCount(); + } + + /** + * The columnCount property does not affect layout. 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#columnGroup + * + * @attr ref android.R.styleable#GridLayout_columnCount + */ + public void setColumnCount(int columnCount) { + mHorizontalAxis.setCount(columnCount); + } + + /** + * Returns whether or not this GridLayout will allocate default margins when no + * corresponding layout parameters are defined. + * + * @return true if default margins should be allocated. + * + * @see #setUseDefaultMargins(boolean) + * + * @attr ref android.R.styleable#GridLayout_useDefaultMargins + */ + public boolean getUseDefaultMargins() { + return mUseDefaultMargins; + } + + /** + * When 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 false, the default value of all margins is zero. + * <p> + * When setting to true, consider setting the value of the + * {@link #setMarginsIncludedInAlignment(boolean) marginsIncludedInAlignment} + * property to false. + * + * @param useDefaultMargins use true to make GridLayout allocate default margins + * + * @see #getUseDefaultMargins() + * @see #setMarginsIncludedInAlignment(boolean) + * + * @see MarginLayoutParams#leftMargin + * @see MarginLayoutParams#topMargin + * @see MarginLayoutParams#rightMargin + * @see MarginLayoutParams#bottomMargin + * + * @attr ref android.R.styleable#GridLayout_useDefaultMargins + */ + public void setUseDefaultMargins(boolean useDefaultMargins) { + mUseDefaultMargins = useDefaultMargins; + requestLayout(); + } + + /** + * Returns whether GridLayout aligns the edges of the view or the edges + * of the larger rectangle created by extending the view by its associated + * margins. + * + * @see #setMarginsIncludedInAlignment(boolean) + * + * @return true if alignment is between edges including margins. + * + * @attr ref android.R.styleable#GridLayout_marginsIncludedInAlignment + */ + public boolean getMarginsIncludedInAlignment() { + return mMarginsIncludedInAlignment; + } + + /** + * When true, the bounds of a view are extended outwards according to its + * margins before the edges of the resulting rectangle are aligned. + * When false, alignment occurs between the bounds of the view - i.e. + * {@link #LEFT} alignment means align the left edges of the view. + * + * @param marginsIncludedInAlignment true if alignment is between edges including margins. + * + * @see #getMarginsIncludedInAlignment() + * + * @attr ref android.R.styleable#GridLayout_marginsIncludedInAlignment + */ + public void setMarginsIncludedInAlignment(boolean marginsIncludedInAlignment) { + mMarginsIncludedInAlignment = marginsIncludedInAlignment; + requestLayout(); + } + + /** + * Returns whether or not row boundaries are ordered by their grid indices. + * + * @return true if row boundaries must appear in the order of their indices, false otherwise. + * The default is false. + * + * @see #setRowOrderPreserved(boolean) + * + * @attr ref android.R.styleable#GridLayout_rowOrderPreserved + */ + public boolean isRowOrderPreserved() { + return mVerticalAxis.isOrderPreserved(); + } + + /** + * When this property is <code>false</code>, the default state, GridLayout + * is at liberty to choose an order that better suits the heights of its children. + <p> + * When this property is <code>true</code>, GridLayout is forced to place the row boundaries + * so that their associated grid indices are in ascending order in the view. + * <p> + * GridLayout implements this specification by creating ordering constraints between + * the variables that represent the locations of the row boundaries. + * + * When this property is <code>true</code>, constraints are added for each pair of consecutive + * indices: i.e. between row boundaries: <code>[0..1], [1..2], [3..4],...</code> etc. + * + * When the property is <code>false</code>, the ordering constraints are placed + * only between boundaries that separate opposing edges of the layout's children. + * + * @param rowOrderPreserved use 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 true if column boundaries must appear in the order of their indices, false otherwise. + * The default is false. + * + * @see #setColumnOrderPreserved(boolean) + * + * @attr ref android.R.styleable#GridLayout_columnOrderPreserved + */ + public boolean isColumnOrderPreserved() { + return mHorizontalAxis.isOrderPreserved(); + } + + /** + * When this property is <code>false</code>, the default state, GridLayout + * is at liberty to choose an order that better suits the widths of its children. + <p> + * When this property is <code>true</code>, GridLayout is forced to place the column boundaries + * so that their associated grid indices are in ascending order in the view. + * <p> + * GridLayout implements this specification by creating ordering constraints between + * the variables that represent the locations of the column boundaries. + * + * When this property is <code>true</code>, constraints are added for each pair of consecutive + * indices: i.e. between column boundaries: <code>[0..1], [1..2], [3..4],...</code> etc. + * + * When the property is <code>false</code>, the ordering constraints are placed + * only between boundaries that separate opposing edges of the layout's children. + * + * @param columnOrderPreserved use 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(); + } + + private static int sum(float[] a) { + int result = 0; + for (int i = 0, length = a.length; i < length; i++) { + result += a[i]; + } + return result; + } + + private int getDefaultMargin(View c, boolean leading, boolean horizontal) { + // In the absence of any other information, calculate a default gap such + // that, in a grid of identical components, the heights and the vertical + // gaps are in the proportion of the golden ratio. + // To effect this with equal margins at each edge, set each of the + // four margin values to half this amount. + return (int) (c.getMeasuredHeight() / GOLDEN_RATIO / 2); + } + + private int getDefaultMargin(View c, boolean isAtEdge, boolean leading, boolean horizontal) { + // todo remove DEFAULT_CONTAINER_MARGIN. Use padding? Seek advice on Themes/Styles, etc. + return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, leading, horizontal); + } + + private int getDefaultMarginValue(View c, LayoutParams p, boolean leading, boolean horizontal) { + if (!mUseDefaultMargins) { + return 0; + } + Group group = horizontal ? p.columnGroup : p.rowGroup; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; + Interval span = group.span; + boolean isAtEdge = leading ? (span.min == 0) : (span.max == axis.getCount()); + + return getDefaultMargin(c, isAtEdge, leading, horizontal); + } + + private int getMargin(View view, boolean leading, boolean horizontal) { + LayoutParams lp = getLayoutParams(view); + int margin = horizontal ? + (leading ? lp.leftMargin : lp.rightMargin) : + (leading ? lp.topMargin : lp.bottomMargin); + return margin == UNDEFINED ? getDefaultMarginValue(view, lp, leading, horizontal) : margin; + } + + private static boolean isUndefined(Interval span) { + return span.min == UNDEFINED || span.max == UNDEFINED; + } + + private void validateLayoutParams() { + // install default indices for cells if *none* are defined + if (mHorizontalAxis.maxIndex1() == UNDEFINED || (mVerticalAxis.maxIndex1() == UNDEFINED)) { + boolean horizontal = mOrientation == HORIZONTAL; + int count = horizontal ? mHorizontalAxis.count : mVerticalAxis.count; + if (count == UNDEFINED) { + count = Integer.MAX_VALUE; + } + int x = 0; + int y = 0; + int maxSize = 0; + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams lp = getLayoutParams1(getChildAt(i)); + + Interval hSpan = lp.columnGroup.span; + int cellWidth = hSpan.size(); + + Interval vSpan = lp.rowGroup.span; + int cellHeight = vSpan.size(); + + if (horizontal) { + if (x + cellWidth > count) { + x = 0; + y += maxSize; + maxSize = 0; + } + } else { + if (y + cellHeight > count) { + y = 0; + x += maxSize; + maxSize = 0; + } + } + lp.setHorizontalGroupSpan(new Interval(x, x + cellWidth)); + lp.setVerticalGroupSpan(new Interval(y, y + cellHeight)); + + if (horizontal) { + x = x + cellWidth; + } else { + y = y + cellHeight; + } + maxSize = max(maxSize, horizontal ? cellHeight : cellWidth); + } + } else { + /* + At least one row and one column index have been defined. + Assume missing row/cols are in error and set them to zero so that + they will display top/left and the developer can add the right indices. + Without this UNDEFINED would cause ArrayIndexOutOfBoundsException. + */ + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams lp = getLayoutParams1(getChildAt(i)); + if (isUndefined(lp.columnGroup.span)) { + lp.setHorizontalGroupSpan(LayoutParams.DEFAULT_SPAN); + } + if (isUndefined(lp.rowGroup.span)) { + lp.setVerticalGroupSpan(LayoutParams.DEFAULT_SPAN); + } + } + } + } + + private void invalidateStructure() { + mLayoutParamsValid = false; + mHorizontalAxis.invalidateStructure(); + mVerticalAxis.invalidateStructure(); + // This can end up being done twice. But better that 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(); + } + } + + private LayoutParams getLayoutParams1(View c) { + return (LayoutParams) c.getLayoutParams(); + } + + private LayoutParams getLayoutParams(View c) { + if (!mLayoutParamsValid) { + validateLayoutParams(); + mLayoutParamsValid = true; + } + return getLayoutParams1(c); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs, mDefaultGravity); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + // Draw grid + + private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) { + int dx = getPaddingLeft(); + int dy = getPaddingTop(); + graphics.drawLine(dx + x1, dy + y1, dx + x2, dy + y2, paint); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (DEBUG) { + int height = getHeight() - getPaddingTop() - getPaddingBottom(); + int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + int[] xs = mHorizontalAxis.locations; + for (int i = 0, length = xs.length; i < length; i++) { + int x = xs[i]; + drawLine(canvas, x, 0, x, height - 1, GRID_PAINT); + } + int[] ys = mVerticalAxis.locations; + for (int i = 0, length = ys.length; i < length; i++) { + int y = ys[i]; + drawLine(canvas, 0, y, width - 1, y, GRID_PAINT); + } + } + } + + // Add/remove + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + invalidateStructure(); + } + + @Override + public void removeView(View view) { + super.removeView(view); + invalidateStructure(); + } + + @Override + public void removeViewInLayout(View view) { + super.removeViewInLayout(view); + invalidateStructure(); + } + + @Override + public void removeViewsInLayout(int start, int count) { + super.removeViewsInLayout(start, count); + invalidateStructure(); + } + + @Override + public void removeViewAt(int index) { + super.removeViewAt(index); + invalidateStructure(); + } + + // Measurement + + private static int getChildMeasureSpec2(int spec, int padding, int childDimension) { + int resultSize; + int resultMode; + + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = EXACTLY; + } else { + /* + using the following lines would replicate the logic of ViewGroup.getChildMeasureSpec() + + int specMode = MeasureSpec.getMode(spec); + int specSize = MeasureSpec.getSize(spec); + int size = Math.max(0, specSize - padding); + + resultSize = size; + resultMode = (specMode == EXACTLY && childDimension == LayoutParams.WRAP_CONTENT) ? + AT_MOST : specMode; + */ + resultSize = 0; + resultMode = UNSPECIFIED; + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + @Override + protected void measureChild(View child, int parentWidthSpec, int parentHeightSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec2(parentWidthSpec, + mPaddingLeft + mPaddingRight, lp.width); + int childHeightMeasureSpec = getChildMeasureSpec2(parentHeightSpec, + mPaddingTop + mPaddingBottom, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + measureChildren(widthSpec, heightSpec); + + int computedWidth = getPaddingLeft() + mHorizontalAxis.getMin() + getPaddingRight(); + int computedHeight = getPaddingTop() + mVerticalAxis.getMin() + getPaddingBottom(); + + setMeasuredDimension( + resolveSizeAndState(computedWidth, widthSpec, 0), + resolveSizeAndState(computedHeight, heightSpec, 0)); + } + + private int protect(int alignment) { + return (alignment == UNDEFINED) ? 0 : alignment; + } + + private int getMeasurement(View c, boolean horizontal, int measurementType) { + return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight(); + } + + private int getMeasurementIncludingMargin(View c, boolean horizontal, int measurementType) { + int result = getMeasurement(c, horizontal, measurementType); + if (mMarginsIncludedInAlignment) { + int leadingMargin = getMargin(c, true, horizontal); + int trailingMargin = getMargin(c, false, horizontal); + return result + leadingMargin + trailingMargin; + } + return result; + } + + private int getAlignmentValue(Alignment alignment, View c, int dim, boolean horizontal, View c1) { + int result = alignment.getAlignmentValue(c, dim); + if (mMarginsIncludedInAlignment) { + int leadingMargin = getMargin(c1, true, horizontal); + return result + leadingMargin; + } + return result; + } + + @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 l, int t, int r, int b) { + int targetWidth = r - l; + int targetHeight = b - t; + + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + + mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight); + mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom); + + for (int i = 0, size = getChildCount(); i < size; i++) { + View view = getChildAt(i); + LayoutParams lp = getLayoutParams(view); + Group columnGroup = lp.columnGroup; + Group rowGroup = lp.rowGroup; + + Interval colSpan = columnGroup.span; + Interval rowSpan = rowGroup.span; + + int x1 = mHorizontalAxis.getLocationIncludingMargin(view, true, colSpan.min); + int y1 = mVerticalAxis.getLocationIncludingMargin(view, true, rowSpan.min); + + int x2 = mHorizontalAxis.getLocationIncludingMargin(view, false, colSpan.max); + int y2 = mVerticalAxis.getLocationIncludingMargin(view, false, rowSpan.max); + + int cellWidth = x2 - x1; + int cellHeight = y2 - y1; + + int pWidth = getMeasurement(view, true, PRF); + int pHeight = getMeasurement(view, false, PRF); + + Alignment hAlignment = columnGroup.alignment; + Alignment vAlignment = rowGroup.alignment; + + int dx, dy; + + if (mMarginsIncludedInAlignment) { + dx = protect(hAlignment.getAlignmentValue(view, cellWidth - pWidth)); + dy = protect(vAlignment.getAlignmentValue(view, cellHeight - pHeight)); + } else { + Bounds colBounds = mHorizontalAxis.getGroupBounds().getValue(i); + Bounds rowBounds = mVerticalAxis.getGroupBounds().getValue(i); + + int mx = protect(hAlignment.getAlignmentValue(null, cellWidth - colBounds.size())); + int my = protect(vAlignment.getAlignmentValue(null, cellHeight - rowBounds.size())); + + dx = mx + -colBounds.below - hAlignment.getAlignmentValue(view, pWidth); + dy = my + -rowBounds.below - vAlignment.getAlignmentValue(view, pHeight); + } + + int width = hAlignment.getSizeInCell(view, pWidth, cellWidth); + int height = vAlignment.getSizeInCell(view, pHeight, cellHeight); + + int cx = paddingLeft + x1 + dx; + int cy = paddingTop + y1 + dy; + view.layout(cx, cy, cx + width, cy + height); + } + } + + // 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. + */ + private class Axis { + private static final int MIN_VALUE = -1000000; + + private static final int UNVISITED = 0; + private static final int PENDING = 1; + private static final int COMPLETE = 2; + + public final boolean horizontal; + + public int count = UNDEFINED; + public boolean countValid = false; + public boolean countWasExplicitySet = false; + + PackedMap<Group, Bounds> groupBounds; + public boolean groupBoundsValid = false; + + PackedMap<Interval, MutableInt> spanSizes; + public boolean spanSizesValid = false; + + public int[] leadingMargins; + public boolean leadingMarginsValid = false; + + public int[] trailingMargins; + public boolean trailingMarginsValid = false; + + public Arc[] arcs; + public boolean arcsValid = false; + + public int[] minima; + public boolean minimaValid = false; + + public float[] weights; + public int[] locations; + + private boolean mOrderPreserved = DEFAULT_ORDER_PRESERVED; + + private Axis(boolean horizontal) { + this.horizontal = horizontal; + } + + private int maxIndex(boolean internal) { + // note the number Integer.MIN_VALUE + 1 comes up in undefined cells + int count = -1; + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams params = internal ? + getLayoutParams1(getChildAt(i)) : + getLayoutParams(getChildAt(i)); + Group g = horizontal ? params.columnGroup : params.rowGroup; + count = max(count, g.span.min); + count = max(count, g.span.max); + } + return count == -1 ? UNDEFINED : count; + } + + private int maxIndex1() { + return maxIndex(true); + } + + public int getCount() { + if (!countWasExplicitySet && !countValid) { + count = max(0, maxIndex(false)); // if there are no cells, the count is zero + countValid = true; + } + return count; + } + + public void setCount(int count) { + this.count = count; + this.countWasExplicitySet = count != UNDEFINED; + } + + public boolean isOrderPreserved() { + return mOrderPreserved; + } + + public void setOrderPreserved(boolean orderPreserved) { + mOrderPreserved = orderPreserved; + invalidateStructure(); + } + + private PackedMap<Group, Bounds> createGroupBounds() { + int N = getChildCount(); + Group[] groups = new Group[N]; + Bounds[] bounds = new Bounds[N]; + for (int i = 0; i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group group = horizontal ? lp.columnGroup : lp.rowGroup; + + groups[i] = group; + bounds[i] = new Bounds(); + } + + return new PackedMap<Group, Bounds>(groups, bounds); + } + + private void computeGroupBounds() { + for (int i = 0; i < groupBounds.values.length; i++) { + groupBounds.values[i].reset(); + } + for (int i = 0, N = getChildCount(); i < N; i++) { + View c = getChildAt(i); + LayoutParams lp = getLayoutParams(c); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + + Bounds bounds = groupBounds.getValue(i); + + int size = getMeasurementIncludingMargin(c, horizontal, PRF); + // todo test this works correctly when the returned value is UNDEFINED + int below = getAlignmentValue(g.alignment, c, size, horizontal, c); + bounds.include(-below, size - below); + } + } + + private PackedMap<Group, 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> createSpanSizes() { + PackedMap<Group, Bounds> groupBounds = getGroupBounds(); + int N = groupBounds.keys.length; + Interval[] spans = new Interval[N]; + MutableInt[] values = new MutableInt[N]; + for (int i = 0; i < N; i++) { + Interval key = groupBounds.keys[i].span; + + spans[i] = key; + values[i] = new MutableInt(); + } + return new PackedMap<Interval, MutableInt>(spans, values); + } + + private void computeSpanSizes() { + MutableInt[] spans = spanSizes.values; + for (int i = 0; i < spans.length; i++) { + spans[i].reset(); + } + + Bounds[] bounds = getGroupBounds().values; // use getter to trigger a re-evaluation + for (int i = 0; i < bounds.length; i++) { + int value = bounds[i].size(); + + MutableInt valueHolder = spanSizes.getValue(i); + valueHolder.value = max(valueHolder.value, value); + } + } + + private PackedMap<Interval, MutableInt> getSpanSizes() { + if (spanSizes == null) { + spanSizes = createSpanSizes(); + } + if (!spanSizesValid) { + computeSpanSizes(); + spanSizesValid = true; + } + return spanSizes; + } + + private void include(List<Arc> arcs, Interval key, MutableInt size) { + // this bit below should really be computed outside here - + // its just to stop default (col>0) constraints obliterating valid entries + for (Arc arc : arcs) { + Interval span = arc.span; + if (span.equals(key)) { + return; + } + } + arcs.add(new Arc(key, size)); + } + + private void include2(List<Arc> arcs, Interval span, MutableInt min, MutableInt max, + boolean both) { + include(arcs, span, min); + if (both) { + // todo +// include(arcs, span.inverse(), max.neg()); + } + } + + private void include2(List<Arc> arcs, Interval span, int min, int max, boolean both) { + include2(arcs, span, new MutableInt(min), new MutableInt(max), both); + } + + // Group arcs by their first vertex, returning an array of arrays. + // This is linear in the number of arcs. + private 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; + } + + /* + Topological sort. + */ + private Arc[] topologicalSort(final Arc[] arcs, int start) { + // todo ensure the <start> vertex is added in edge cases + final List<Arc> result = new ArrayList<Arc>(); + new Object() { + Arc[][] arcsByFirstVertex = groupArcsByFirstVertex(arcs); + int[] visited = new int[getCount() + 1]; + + boolean completesCycle(int loc) { + int state = visited[loc]; + if (state == UNVISITED) { + visited[loc] = PENDING; + for (Arc arc : arcsByFirstVertex[loc]) { + Interval span = arc.span; + // the recursive call + if (completesCycle(span.max)) { + // which arcs get set here is dependent on the order + // in which we explore nodes + arc.completesCycle = true; + } + result.add(arc); + } + visited[loc] = COMPLETE; + } else if (state == PENDING) { + return true; + } else if (state == COMPLETE) { + } + return false; + } + }.completesCycle(start); + Collections.reverse(result); + assert arcs.length == result.size(); + return result.toArray(new Arc[result.size()]); + } + + private boolean[] findUsed(Collection<Arc> arcs) { + boolean[] result = new boolean[getCount()]; + for (Arc arc : arcs) { + Interval span = arc.span; + int min = min(span.min, span.max); + int max = max(span.min, span.max); + for (int i = min; i < max; i++) { + result[i] = true; + } + } + return result; + } + + // todo unify with findUsed above. Both routines analyze which rows/columns are empty. + private Collection<Interval> getSpacers() { + List<Interval> result = new ArrayList<Interval>(); + int N = getCount() + 1; + int[] leadingEdgeCount = new int[N]; + int[] trailingEdgeCount = new int[N]; + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + leadingEdgeCount[span.min]++; + trailingEdgeCount[span.max]++; + } + + int lastTrailingEdge = 0; + + // treat the parent's edges like peer edges of the opposite type + trailingEdgeCount[0] = 1; + leadingEdgeCount[N - 1] = 1; + + for (int i = 0; i < N; i++) { + if (trailingEdgeCount[i] > 0) { + lastTrailingEdge = i; + continue; // if this is also a leading edge, don't add a space of length zero + } + if (leadingEdgeCount[i] > 0) { + result.add(new Interval(lastTrailingEdge, i)); + } + } + return result; + } + + private Arc[] createArcs() { + List<Arc> spanToSize = new ArrayList<Arc>(); + + // Add all the preferred elements that were not defined by the user. + PackedMap<Interval, MutableInt> spanSizes = getSpanSizes(); + for (int i = 0; i < spanSizes.keys.length; i++) { + Interval key = spanSizes.keys[i]; + MutableInt value = spanSizes.values[i]; + // todo remove value duplicate + include2(spanToSize, key, value, value, accommodateBothMinAndMax); + } + + // Find redundant rows/cols and glue them together with 0-length arcs to link the tree + boolean[] used = findUsed(spanToSize); + for (int i = 0; i < getCount(); i++) { + if (!used[i]) { + Interval span = new Interval(i, i + 1); + include(spanToSize, span, new MutableInt(0)); + include(spanToSize, span.inverse(), new MutableInt(0)); + } + } + + if (mOrderPreserved) { + // Add preferred gaps + for (int i = 0; i < getCount(); i++) { + if (used[i]) { + include2(spanToSize, new Interval(i, i + 1), 0, 0, false); + } + } + } else { + for (Interval gap : getSpacers()) { + include2(spanToSize, gap, 0, 0, false); + } + } + Arc[] arcs = spanToSize.toArray(new Arc[spanToSize.size()]); + return topologicalSort(arcs, 0); + } + + public Arc[] getArcs() { + if (arcs == null) { + arcs = createArcs(); + } + if (!arcsValid) { + getSpanSizes(); + arcsValid = true; + } + return arcs; + } + + private boolean relax(int[] locations, Arc entry) { + 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; + } + + /* + 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. + + Other algorithms are faster in the case where no arcs have negative weights + but allowing negative weights turns out to be the same as accommodating maximum + size requirements as well as minimum ones. + + Bellman-Ford works by iteratively 'relaxing' constraints over all nodes (an O(N) + process) and performing this step N times. Proof of correctness hinges on the + fact that there can be no negative weight chains of length > N - unless a + 'negative weight loop' exists. The algorithm catches this case in a final + checking phase that reports failure. + + By topologically sorting the nodes and checking this condition at each step + typical layout problems complete after the first iteration and the algorithm + completes in O(N) steps with very low constants. + */ + private int[] solve(Arc[] arcs, int[] locations) { + int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1. + + boolean changed = false; + // We take one extra pass over traditional Bellman-Ford (and omit their final step) + for (int i = 0; i < N; i++) { + changed = false; + for (int j = 0, length = arcs.length; j < length; j++) { + changed = changed | relax(locations, arcs[j]); + } + if (!changed) { + if (DEBUG) { + Log.d(TAG, "Iteration " + + " completed after " + (1 + i) + " steps out of " + N); + } + break; + } + } + if (changed) { + Log.d(TAG, "*** Algorithm failed to terminate ***"); + } + return locations; + } + + private void computeMargins(boolean leading) { + int[] margins = leading ? leadingMargins : trailingMargins; + for (int i = 0, size = getChildCount(); i < size; i++) { + View c = getChildAt(i); + LayoutParams lp = getLayoutParams(c); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + int index = leading ? span.min : span.max; + margins[index] = max(margins[index], getMargin(c, leading, horizontal)); + } + } + + private int[] getLeadingMargins() { + if (leadingMargins == null) { + leadingMargins = new int[getCount() + 1]; + } + if (!leadingMarginsValid) { + computeMargins(true); + leadingMarginsValid = true; + } + return leadingMargins; + } + + private int[] getTrailingMargins() { + if (trailingMargins == null) { + trailingMargins = new int[getCount() + 1]; + } + if (!trailingMarginsValid) { + computeMargins(false); + trailingMarginsValid = true; + } + return trailingMargins; + } + + private void addMargins() { + int[] leadingMargins = getLeadingMargins(); + int[] trailingMargins = getTrailingMargins(); + + int delta = 0; + for (int i = 0, N = getCount(); i < N; i++) { + int margins = leadingMargins[i] + trailingMargins[i + 1]; + delta += margins; + minima[i + 1] += delta; + } + } + + private int getLocationIncludingMargin(View view, boolean leading, int index) { + int location = locations[index]; + int margin; + if (!mMarginsIncludedInAlignment) { + margin = (leading ? leadingMargins : trailingMargins)[index]; + } else { + margin = getMargin(view, leading, horizontal); + } + return leading ? (location + margin) : (location - margin); + } + + private void computeMinima(int[] a) { + Arrays.fill(a, MIN_VALUE); + a[0] = 0; + solve(getArcs(), a); + if (!mMarginsIncludedInAlignment) { + addMargins(); + } + } + + private int[] getMinima() { + if (minima == null) { + int N = getCount() + 1; + minima = new int[N]; + } + if (!minimaValid) { + computeMinima(minima); + minimaValid = true; + } + return minima; + } + + private void computeWeights() { + for (int i = 0, N = getChildCount(); i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + int penultimateIndex = span.max - 1; + weights[penultimateIndex] += horizontal ? lp.columnWeight : lp.rowWeight; + } + } + + private float[] getWeights() { + if (weights == null) { + int N = getCount() + 1; + weights = new float[N]; + } + computeWeights(); + return weights; + } + + private int[] getLocations() { + if (locations == null) { + int N = getCount() + 1; + locations = new int[N]; + } + return locations; + } + + // External entry points + + private int size(int[] locations) { + return locations[locations.length - 1] - locations[0]; + } + + private int getMin() { + return size(getMinima()); + } + + private void layout(int targetSize) { + int[] mins = getMinima(); + + int totalDelta = max(0, targetSize - size(mins)); // confine to expansion + + float[] weights = getWeights(); + float totalWeight = sum(weights); + + if (totalWeight == 0f) { + weights[weights.length - 1] = 1; + totalWeight = 1; + } + + int[] locations = getLocations(); + int cumulativeDelta = 0; + + for (int i = 0; i < locations.length; i++) { + float weight = weights[i]; + int delta = (int) (totalDelta * weight / totalWeight); + cumulativeDelta += delta; + locations[i] = mins[i] + cumulativeDelta; + + totalDelta -= delta; + totalWeight -= weight; + } + } + + private void invalidateStructure() { + countValid = false; + + groupBounds = null; + spanSizes = null; + leadingMargins = null; + trailingMargins = null; + minima = null; + weights = null; + locations = null; + + invalidateValues(); + } + + private void invalidateValues() { + groupBoundsValid = false; + spanSizesValid = false; + arcsValid = false; + leadingMarginsValid = false; + trailingMarginsValid = false; + minimaValid = 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 #rowGroup} and {@link #columnGroup} layout parameters. + * {@link Group Groups} are immutable structures and may be shared between the layout + * parameters of different children. + * <p> + * The row and column groups 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 {@link Group#alignment alignment} fields of the row and column groups 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> + * See {@link GridLayout} for a description of the conventions used by GridLayout + * in reference to grid indices. + * + * <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</code>; 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</code>; 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</code>; 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</code>; otherwise {@link #UNDEFINED}, to + * indicate that a default value should be computed on demand. </li> + * <li>{@link #rowGroup}<code>.span</code> = <code>[0, 1]</code> </li> + * <li>{@link #rowGroup}<code>.alignment</code> = {@link #BASELINE} </li> + * <li>{@link #columnGroup}<code>.span</code> = <code>[0, 1]</code> </li> + * <li>{@link #columnGroup}<code>.alignment</code> = {@link #LEFT} </li> + * <li>{@link #rowWeight} = <code>0f</code> </li> + * <li>{@link #columnWeight} = <code>0f</code> </li> + * </ul> + * + * @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(0, 1); + private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size(); + private static final Alignment DEFAULT_HORIZONTAL_ALIGNMENT = LEFT; + private static final Alignment DEFAULT_VERTCIAL_ALGIGNMENT = BASELINE; + private static final Group DEFAULT_HORIZONTAL_GROUP = + new Group(DEFAULT_SPAN, DEFAULT_HORIZONTAL_ALIGNMENT); + private static final Group DEFAULT_VERTICAL_GROUP = + new Group(DEFAULT_SPAN, DEFAULT_VERTCIAL_ALGIGNMENT); + private static final int DEFAULT_WEIGHT_0 = 0; + private static final int DEFAULT_WEIGHT_1 = 1; + + // Misc + + private static final Rect CONTAINER_BOUNDS = new Rect(0, 0, 2, 2); + private static final Alignment[] HORIZONTAL_ALIGNMENTS = { LEFT, CENTER, RIGHT }; + private static final Alignment[] VERTICAL_ALIGNMENTS = { TOP, CENTER, BOTTOM }; + + // TypedArray indices + + private static final int MARGIN = styleable.ViewGroup_MarginLayout_layout_margin; + private static final int LEFT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginLeft; + private static final int TOP_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginTop; + private static final int RIGHT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginRight; + private static final int BOTTOM_MARGIN = + styleable.ViewGroup_MarginLayout_layout_marginBottom; + + private static final int COLUMN = styleable.GridLayout_Layout_layout_column; + private static final int COLUMN_SPAN = styleable.GridLayout_Layout_layout_columnSpan; + private static final int COLUMN_WEIGHT = styleable.GridLayout_Layout_layout_columnWeight; + private static final int ROW = styleable.GridLayout_Layout_layout_row; + private static final int ROW_SPAN = styleable.GridLayout_Layout_layout_rowSpan; + private static final int ROW_WEIGHT = styleable.GridLayout_Layout_layout_rowWeight; + private static final int GRAVITY = styleable.GridLayout_Layout_layout_gravity; + + // Instance variables + + /** + * The group that specifies the vertical characteristics of the cell group + * described by these layout parameters. + */ + public Group rowGroup; + /** + * The group that specifies the horizontal characteristics of the cell group + * described by these layout parameters. + */ + public Group columnGroup; + /** + * The proportional space that should be taken by the associated row group + * during excess space distribution. + */ + public float rowWeight; + /** + * The proportional space that should be taken by the associated column group + * during excess space distribution. + */ + public float columnWeight; + + // Constructors + + private LayoutParams( + int width, int height, + int left, int top, int right, int bottom, + Group rowGroup, Group columnGroup, float rowWeight, float columnWeight) { + super(width, height); + setMargins(left, top, right, bottom); + this.rowGroup = rowGroup; + this.columnGroup = columnGroup; + this.rowWeight = rowWeight; + this.columnWeight = columnWeight; + } + + /** + * Constructs a new LayoutParams instance for this <code>rowGroup</code> + * and <code>columnGroup</code>. All other fields are initialized with + * default values as defined in {@link LayoutParams}. + * + * @param rowGroup the rowGroup + * @param columnGroup the columnGroup + */ + public LayoutParams(Group rowGroup, Group columnGroup) { + this(DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, + rowGroup, columnGroup, DEFAULT_WEIGHT_0, DEFAULT_WEIGHT_0); + } + + /** + * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. + */ + public LayoutParams() { + this(DEFAULT_HORIZONTAL_GROUP, DEFAULT_VERTICAL_GROUP); + } + + // Copying constructors + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams params) { + super(params); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams params) { + super(params); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(LayoutParams that) { + super(that); + this.columnGroup = that.columnGroup; + this.rowGroup = that.rowGroup; + this.columnWeight = that.columnWeight; + this.rowWeight = that.rowWeight; + } + + // AttributeSet constructors + + private LayoutParams(Context context, AttributeSet attrs, int defaultGravity) { + super(context, attrs); + reInitSuper(context, attrs); + init(context, attrs, defaultGravity); + } + + /** + * {@inheritDoc} + * + * Values not defined in the attribute set take the default values + * defined in {@link LayoutParams}. + */ + public LayoutParams(Context context, AttributeSet attrs) { + this(context, attrs, Gravity.NO_GRAVITY); + } + + // Implementation + + private static boolean definesVertical(int gravity) { + return gravity > 0 && (gravity & Gravity.VERTICAL_GRAVITY_MASK) != 0; + } + + private static boolean definesHorizontal(int gravity) { + return gravity > 0 && (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != 0; + } + + private static <T> T getAlignment(T[] alignments, T fill, int min, int max, + boolean isUndefined, T defaultValue) { + if (isUndefined) { + return defaultValue; + } + return min != max ? fill : alignments[min]; + } + + // 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, 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(); + } + } + + // Gravity. For conversion from the static the integers defined in the Gravity class, + // use Gravity.apply() to apply gravity to a view of zero size and see where it ends up. + private static Alignment getHorizontalAlignment(int gravity, int width) { + Rect r = new Rect(0, 0, 0, 0); + Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); + + boolean fill = width == MATCH_PARENT; + Alignment defaultAlignment = fill ? FILL : DEFAULT_HORIZONTAL_ALIGNMENT; + return getAlignment(HORIZONTAL_ALIGNMENTS, FILL, r.left, r.right, + !definesHorizontal(gravity), defaultAlignment); + } + + private static Alignment getVerticalAlignment(int gravity, int height) { + Rect r = new Rect(0, 0, 0, 0); + Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); + + boolean fill = height == MATCH_PARENT; + Alignment defaultAlignment = fill ? FILL : DEFAULT_VERTCIAL_ALGIGNMENT; + return getAlignment(VERTICAL_ALIGNMENTS, FILL, r.top, r.bottom, + !definesVertical(gravity), defaultAlignment); + } + + private int getDefaultWeight(int size) { + return (size == MATCH_PARENT) ? DEFAULT_WEIGHT_1 : DEFAULT_WEIGHT_0; + } + + private void init(Context context, AttributeSet attrs, int defaultGravity) { + TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout_Layout); + try { + int gravity = a.getInteger(GRAVITY, defaultGravity); + + int column = a.getInteger(COLUMN, DEFAULT_COLUMN); + int columnSpan = a.getInteger(COLUMN_SPAN, DEFAULT_SPAN_SIZE); + Interval hSpan = new Interval(column, column + columnSpan); + this.columnGroup = new Group(hSpan, getHorizontalAlignment(gravity, width)); + this.columnWeight = a.getFloat(COLUMN_WEIGHT, getDefaultWeight(width)); + + int row = a.getInteger(ROW, DEFAULT_ROW); + int rowSpan = a.getInteger(ROW_SPAN, DEFAULT_SPAN_SIZE); + Interval vSpan = new Interval(row, row + rowSpan); + this.rowGroup = new Group(vSpan, getVerticalAlignment(gravity, height)); + this.rowWeight = a.getFloat(ROW_WEIGHT, getDefaultWeight(height)); + } finally { + a.recycle(); + } + } + + /** + * Describes how the child views are positioned. Default is <code>LEFT | BASELINE</code>. + * + * @param gravity the new gravity. See {@link android.view.Gravity}. + * + * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity + */ + public void setGravity(int gravity) { + columnGroup = columnGroup.copyWriteAlignment(getHorizontalAlignment(gravity, width)); + rowGroup = rowGroup.copyWriteAlignment(getVerticalAlignment(gravity, height)); + } + + @Override + protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) { + this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH); + this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT); + } + + private void setVerticalGroupSpan(Interval span) { + rowGroup = rowGroup.copyWriteSpan(span); + } + + private void setHorizontalGroupSpan(Interval span) { + columnGroup = columnGroup.copyWriteSpan(span); + } + } + + /* + 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. + */ + private static class Arc { + public final Interval span; + public final MutableInt value; + public boolean completesCycle; + + public Arc(Interval span, MutableInt value) { + this.span = span; + this.value = value; + } + + @Override + public String toString() { + return span + " " + (completesCycle ? "+>" : "->") + " " + value; + } + } + + // A mutable Integer - used to avoid heap allocation during the layout operation + + private static class MutableInt { + public int value; + + private MutableInt() { + reset(); + } + + private MutableInt(int value) { + this.value = value; + } + + private void reset() { + value = Integer.MIN_VALUE; + } + } + + /* + 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") + private 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); + } + + private K getKey(int i) { + return keys[index[i]]; + } + + private 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; + } + + private static int max(int[] a, int valueIfEmpty) { + int result = valueIfEmpty; + for (int i = 0, length = a.length; i < length; i++) { + result = Math.max(result, a[i]); + } + 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, max(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 + above the alignment point and the amount of space required below it. One side of this + calculation is always 0 for LEADING and TRAILING 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. + */ + private static class Bounds { + public int below; + public int above; + + private Bounds(int below, int above) { + this.below = below; + this.above = above; + } + + private Bounds() { + reset(); + } + + private void reset() { + below = Integer.MAX_VALUE; + above = Integer.MIN_VALUE; + } + + private void include(int below, int above) { + this.below = min(this.below, below); + this.above = max(this.above, above); + } + + private int size() { + return above - below; + } + + @Override + public String toString() { + return "Bounds{" + + "below=" + below + + ", above=" + above + + '}'; + } + } + + /** + * 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]</code> and represent the set of values + * <em>x</em> such that <em>min <= x < max</em>. + */ + /* package */ static class Interval { + /** + * The minimum value. + */ + public final int min; + + /** + * The maximum value. + */ + public final int max; + + /** + * Construct a new Interval, <code>interval</code>, where: + * <ul> + * <li> <code>interval.min = min</code> </li> + * <li> <code>interval.max = max</code> </li> + * </ul> + * + * @param min the minimum value. + * @param max the maximum value. + */ + public Interval(int min, int max) { + this.min = min; + this.max = max; + } + + private int size() { + return max - min; + } + + private Interval inverse() { + return new Interval(max, min); + } + + /** + * Returns true if the {@link #getClass class}, {@link #min} and {@link #max} properties + * of this Interval and the supplied parameter are pairwise equal; 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; + } + 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 group specifies either the horizontal or vertical characteristics of a group of + * cells. + * <p> + * Groups are immutable and so may be shared between views with the same + * <code>span</code> and <code>alignment</code>. + */ + public static class Group { + /** + * The grid indices of the leading and trailing edges of this cell group for the + * appropriate axis. + * <p> + * See {@link GridLayout} for a description of the conventions used by GridLayout + * for grid indices. + */ + /* package */ final Interval span; + /** + * 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. + */ + public final Alignment alignment; + + /** + * Construct a new Group, <code>group</code>, where: + * <ul> + * <li> <code>group.span = span</code> </li> + * <li> <code>group.alignment = alignment</code> </li> + * </ul> + * + * @param span the span. + * @param alignment the alignment. + */ + /* package */ Group(Interval span, Alignment alignment) { + this.span = span; + this.alignment = alignment; + } + + /** + * Construct a new Group, <code>group</code>, where: + * <ul> + * <li> <code>group.span = [min, max]</code> </li> + * <li> <code>group.alignment = alignment</code> </li> + * </ul> + * + * @param min the minimum. + * @param max the maximum. + * @param alignment the alignment. + */ + public Group(int min, int max, Alignment alignment) { + this(new Interval(min, max), alignment); + } + + /** + * Construct a new Group, <code>group</code>, where: + * <ul> + * <li> <code>group.span = [min, min + 1]</code> </li> + * <li> <code>group.alignment = alignment</code> </li> + * </ul> + * + * @param min the minimum. + * @param alignment the alignment. + */ + public Group(int min, Alignment alignment) { + this(min, min + 1, alignment); + } + + private Group copyWriteSpan(Interval span) { + return new Group(span, alignment); + } + + private Group copyWriteAlignment(Alignment alignment) { + return new Group(span, alignment); + } + + /** + * Returns true if the {@link #getClass class}, {@link #alignment} and <code>span</code> + * properties of this Group and the supplied parameter are pairwise equal; false otherwise. + * + * @param that the object to compare this group with. + * + * @return {@code true} if the specified object is equal to this + * {@code Group}; {@code false} otherwise. + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + if (that == null || getClass() != that.getClass()) { + return false; + } + + Group group = (Group) that; + + if (!alignment.equals(group.alignment)) { + return false; + } + if (!span.equals(group.span)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = span.hashCode(); + result = 31 * result + alignment.hashCode(); + return result; + } + } + + /** + * 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#rowGroup rowGroup} + * and a {@link LayoutParams#columnGroup columnGroup} each of which contains an + * {@link Group#alignment alignment}. Overall placement of the view in the cell + * group is specified by the two alignments which act along each axis independently. + * <p> + * An Alignment implementation must define the {@link #getAlignmentValue(View, int)} + * to return the appropriate value for the type of alignment being defined. + * The enclosing algorithms position the children + * so that the values returned from the alignment + * are the same for all of the views in a group. + * <p> + * The GridLayout class defines the most common alignments used in general layout: + * {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #CENTER}, {@link + * #BASELINE} and {@link #FILL}. + */ + public static interface Alignment { + /** + * 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. + * @return the alignment value. + */ + public int getAlignmentValue(View view, int viewSize); + + /** + * 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. + * + * @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. + */ + public int getSizeInCell(View view, int viewSize, int cellSize); + } + + private static abstract class AbstractAlignment implements Alignment { + public int getSizeInCell(View view, int viewSize, int cellSize) { + return viewSize; + } + } + + private static final Alignment LEADING = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + return 0; + } + + }; + + private static final Alignment TRAILING = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + 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>right</em> + * edges of the other views in its cell group. + */ + public static final Alignment RIGHT = TRAILING; + + /** + * 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 = LEADING; + + /** + * 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#rowGroup rowGroups} and {@link + * LayoutParams#columnGroup columnGroups}. + */ + public static final Alignment CENTER = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + 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#rowGroup rowGroups}. + * + * @see View#getBaseline() + */ + public static final Alignment BASELINE = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + if (view == null) { + return UNDEFINED; + } + // todo do we need to call measure first? + int baseline = view.getBaseline(); + return baseline == -1 ? UNDEFINED : baseline; + } + + }; + + /** + * Indicates that a view should expanded to fit the boundaries of its cell group. + * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and + * {@link LayoutParams#columnGroup columnGroups}. + */ + public static final Alignment FILL = new Alignment() { + public int getAlignmentValue(View view, int viewSize) { + return UNDEFINED; + } + + public int getSizeInCell(View view, int viewSize, int cellSize) { + return cellSize; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 0383b5c..732cedc 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1408,19 +1408,20 @@ public class GridView extends AbsListView { int childLeft; final int childTop = flow ? y : y - h; - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - childLeft = childrenLeft; - break; - case Gravity.CENTER_HORIZONTAL: - childLeft = childrenLeft + ((mColumnWidth - w) / 2); - break; - case Gravity.RIGHT: - childLeft = childrenLeft + mColumnWidth - w; - break; - default: - childLeft = childrenLeft; - break; + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity,isLayoutRtl()); + 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) { diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 1fe6f4b..4b870ec 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -187,6 +187,11 @@ public class ImageView extends View { } @Override + public boolean isLayoutRtl(Drawable dr) { + return (dr == mDrawable) ? isLayoutRtl() : super.isLayoutRtl(dr); + } + + @Override protected boolean onSetAlpha(int alpha) { if (getBackground() == null) { int scale = alpha + (alpha >> 7); @@ -218,15 +223,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum width for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@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} to determine how to fit the image within the bounds. + * 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 @@ -240,15 +246,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum height for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@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} to determine how to fit the image within the bounds. + * 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 @@ -272,8 +279,8 @@ public class ImageView extends View { * * <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} or - * {@link #setImageBitmap} and + * 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 the drawable @@ -297,8 +304,8 @@ public class ImageView extends View { * * <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} or - * {@link #setImageBitmap} and + * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or + * {@link #setImageBitmap(android.graphics.Bitmap)} and * {@link android.graphics.BitmapFactory} instead.</p> * * @param uri The Uri of an image @@ -902,12 +909,12 @@ public class ImageView extends View { /** * <p>Set the offset of the widget's text baseline from the widget's top - * boundary. This value is overridden by the {@link #setBaselineAlignBottom} + * 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 + * @see #setBaseline(int) * @attr ref android.R.styleable#ImageView_baseline */ public void setBaseline(int baseline) { diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index fd0e53d..f843574 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -103,21 +103,39 @@ public class LinearLayout extends ViewGroup { @ViewDebug.ExportedProperty(category = "measurement") private int mOrientation; - @ViewDebug.ExportedProperty(category = "measurement", mapping = { - @ViewDebug.IntToString(from = -1, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), - @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), - @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), - @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), - @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), - @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") + @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.BEFORE, + equals = Gravity.BEFORE, name = "BEFORE"), + @ViewDebug.FlagToString(mask = Gravity.AFTER, + equals = Gravity.AFTER, name = "AFTER"), + @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_HORIZONTAL_DIRECTION, + equals = Gravity.RELATIVE_HORIZONTAL_DIRECTION, name = "RELATIVE") }) - private int mGravity = Gravity.LEFT | Gravity.TOP; + private int mGravity = Gravity.BEFORE | Gravity.TOP; @ViewDebug.ExportedProperty(category = "measurement") private int mTotalLength; @@ -201,6 +219,11 @@ public class LinearLayout extends ViewGroup { mShowDividers = showDividers; } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * @return A flag set indicating how dividers should be shown around items. * @see #setShowDividers(int) @@ -230,6 +253,39 @@ public class LinearLayout extends ViewGroup { requestLayout(); } + /** + * Set padding displayed on both ends 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) { + mDividerPadding = padding; + } + + /** + * 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) { @@ -244,29 +300,15 @@ public class LinearLayout extends ViewGroup { } void drawDividersVertical(Canvas canvas) { - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - final boolean showDividerEnd = - (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END; - final int count = getVirtualChildCount(); int top = getPaddingTop(); - boolean firstVisible = true; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { top += measureNullChild(i); } else if (child.getVisibility() != GONE) { - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - drawHorizontalDivider(canvas, top); - top += mDividerHeight; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { drawHorizontalDivider(canvas, top); top += mDividerHeight; } @@ -276,35 +318,21 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawHorizontalDivider(canvas, top); } } void drawDividersHorizontal(Canvas canvas) { - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - final boolean showDividerEnd = - (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END; - final int count = getVirtualChildCount(); int left = getPaddingLeft(); - boolean firstVisible = true; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { left += measureNullChild(i); } else if (child.getVisibility() != GONE) { - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - drawVerticalDivider(canvas, left); - left += mDividerWidth; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { drawVerticalDivider(canvas, left); left += mDividerWidth; } @@ -314,7 +342,7 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawVerticalDivider(canvas, left); } } @@ -523,6 +551,23 @@ public class LinearLayout extends ViewGroup { } /** + * 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 == 0) { + return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0; + } else if (childIndex == getChildCount()) { + return (mShowDividers & SHOW_DIVIDER_END) != 0; + } else { + return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0; + } + } + + /** * Measures the children when the orientation of this LinearLayout is set * to {@link #VERTICAL}. * @@ -554,14 +599,7 @@ public class LinearLayout extends ViewGroup { int largestChildHeight = Integer.MIN_VALUE; - // A divider at the end will change how much space views can consume. - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - // See how tall everyone is. Also remember max width. - boolean firstVisible = true; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); @@ -575,12 +613,7 @@ public class LinearLayout extends ViewGroup { continue; } - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - mTotalLength += mDividerHeight; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } @@ -677,11 +710,12 @@ public class LinearLayout extends ViewGroup { i += getChildrenSkipCount(child, i); } - if (mTotalLength > 0 && (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END) { + if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } - if (useLargestChild && heightMode == MeasureSpec.AT_MOST) { + if (useLargestChild && + (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { @@ -794,6 +828,31 @@ public class LinearLayout extends ViewGroup { } 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 && widthMode == MeasureSpec.UNSPECIFIED) { + 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) { @@ -881,14 +940,7 @@ public class LinearLayout extends ViewGroup { int largestChildWidth = Integer.MIN_VALUE; - // A divider at the end will change how much space views can consume. - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - // See how wide everyone is. Also remember max height. - boolean firstVisible = true; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); @@ -902,12 +954,7 @@ public class LinearLayout extends ViewGroup { continue; } - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - mTotalLength += mDividerWidth; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerWidth; } @@ -1022,7 +1069,7 @@ public class LinearLayout extends ViewGroup { i += getChildrenSkipCount(child, i); } - if (mTotalLength > 0 && (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END) { + if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerWidth; } @@ -1041,7 +1088,8 @@ public class LinearLayout extends ViewGroup { maxHeight = Math.max(maxHeight, ascent + descent); } - if (useLargestChild && widthMode == MeasureSpec.AT_MOST) { + if (useLargestChild && + (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { @@ -1197,6 +1245,29 @@ public class LinearLayout extends ViewGroup { } } 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.UNSPECIFIED) { + 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) { @@ -1328,7 +1399,7 @@ public class LinearLayout extends ViewGroup { void layoutVertical() { final int paddingLeft = mPaddingLeft; - int childTop = mPaddingTop; + int childTop; int childLeft; // Where right end of child should go @@ -1341,28 +1412,23 @@ public class LinearLayout extends ViewGroup { final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; - final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - - if (majorGravity != Gravity.TOP) { - switch (majorGravity) { - case Gravity.BOTTOM: - // mTotalLength contains the padding already, we add the top - // padding to compensate - childTop = mBottom - mTop + mPaddingTop - mTotalLength; - break; - - case Gravity.CENTER_VERTICAL: - childTop += ((mBottom - mTop) - mTotalLength) / 2; - break; - } - - } - - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childTop += mDividerHeight; + final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; + + switch (majorGravity) { + case Gravity.BOTTOM: + // mTotalLength contains the padding already + childTop = mPaddingTop + mBottom - mTop - mTotalLength; + break; + + // mTotalLength contains the padding already + case Gravity.CENTER_VERTICAL: + childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2; + break; + + case Gravity.TOP: + default: + childTop = mPaddingTop; + break; } for (int i = 0; i < count; i++) { @@ -1380,12 +1446,8 @@ public class LinearLayout extends ViewGroup { if (gravity < 0) { gravity = minorGravity; } - - switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - childLeft = paddingLeft + lp.leftMargin; - break; - + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; @@ -1394,20 +1456,22 @@ public class LinearLayout extends ViewGroup { case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; + + case Gravity.LEFT: default: - childLeft = paddingLeft; + 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); - if (showDividerMiddle) { - childTop += mDividerHeight; - } - i += getChildrenSkipCount(child, i); } } @@ -1422,10 +1486,11 @@ public class LinearLayout extends ViewGroup { * @see #onLayout(boolean, int, int, int, int) */ void layoutHorizontal() { + final boolean isLayoutRtl = isLayoutRtl(); final int paddingTop = mPaddingTop; int childTop; - int childLeft = mPaddingLeft; + int childLeft; // Where bottom of child should go final int height = mBottom - mTop; @@ -1436,7 +1501,7 @@ public class LinearLayout extends ViewGroup { final int count = getVirtualChildCount(); - final int majorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean baselineAligned = mBaselineAligned; @@ -1444,32 +1509,37 @@ public class LinearLayout extends ViewGroup { final int[] maxAscent = mMaxAscent; final int[] maxDescent = mMaxDescent; - if (majorGravity != Gravity.LEFT) { - switch (majorGravity) { - case Gravity.RIGHT: - // mTotalLength contains the padding already, we add the left - // padding to compensate - childLeft = mRight - mLeft + mPaddingLeft - mTotalLength; - break; - - case Gravity.CENTER_HORIZONTAL: - childLeft += ((mRight - mLeft) - mTotalLength) / 2; - break; - } + switch (Gravity.getAbsoluteGravity(majorGravity, isLayoutRtl())) { + case Gravity.RIGHT: + // mTotalLength contains the padding already + childLeft = mPaddingLeft + mRight - mLeft - mTotalLength; + break; + + case Gravity.CENTER_HORIZONTAL: + // mTotalLength contains the padding already + childLeft = mPaddingLeft + (mRight - mLeft - mTotalLength) / 2; + break; + + case Gravity.LEFT: + default: + childLeft = mPaddingLeft; + break; } - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childLeft += mDividerWidth; + 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 View child = getVirtualChildAt(i); + int childIndex = start + dir * i; + final View child = getVirtualChildAt(childIndex); if (child == null) { - childLeft += measureNullChild(i); + childLeft += measureNullChild(childIndex); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); @@ -1523,17 +1593,17 @@ public class LinearLayout extends ViewGroup { break; } + if (hasDividerBeforeChildAt(childIndex)) { + childLeft += mDividerWidth; + } + childLeft += lp.leftMargin; setChildFrame(child, childLeft + getLocationOffset(child), childTop, childWidth, childHeight); childLeft += childWidth + lp.rightMargin + getNextLocationOffset(child); - if (showDividerMiddle) { - childLeft += mDividerWidth; - } - - i += getChildrenSkipCount(child, i); + i += getChildrenSkipCount(child, childIndex); } } } @@ -1578,8 +1648,8 @@ public class LinearLayout extends ViewGroup { @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.BEFORE; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -1593,9 +1663,9 @@ public class LinearLayout extends ViewGroup { @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { - final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { - mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + 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(); } } @@ -1672,6 +1742,8 @@ public class LinearLayout extends ViewGroup { @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.BEFORE, to = "BEFORE"), + @ViewDebug.IntToString(from = Gravity.AFTER, to = "AFTER"), @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"), diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index af954c9..e7a9e41 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -251,7 +251,7 @@ public class ListView extends AbsListView { */ public void addHeaderView(View v, Object data, boolean isSelectable) { - if (mAdapter != null) { + if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) { throw new IllegalStateException( "Cannot add header view to list -- setAdapter has already been called."); } @@ -261,6 +261,12 @@ public class ListView extends AbsListView { info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); + + // 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(); + } } /** @@ -294,7 +300,9 @@ public class ListView extends AbsListView { if (mHeaderViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeHeader(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mHeaderViewInfos); @@ -328,6 +336,12 @@ public class ListView extends AbsListView { * @param isSelectable true if the footer view can be selected */ public void addFooterView(View v, Object data, boolean isSelectable) { + + // NOTE: do not enforce the adapter being null here, since unlike in + // addHeaderView, it was never enforced here, and so existing apps are + // relying on being able to add a footer and then calling setAdapter to + // force creation of the HeaderViewListAdapter wrapper + FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; @@ -371,7 +385,9 @@ public class ListView extends AbsListView { if (mFooterViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeFooter(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mFooterViewInfos); @@ -1552,7 +1568,7 @@ public class ListView extends AbsListView { // take focus back to us temporarily to avoid the eventual // call to clear focus when removing the focused child below - // from messing things up when ViewRoot assigns focus back + // from messing things up when ViewAncestor assigns focus back // to someone else final View focusedChild = getFocusedChild(); if (focusedChild != null) { @@ -1982,36 +1998,28 @@ public class ListView extends AbsListView { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); // If the item count is less than 15 then subtract disabled items from the count and // position. Otherwise ignore disabled items. - if (!populated) { - int itemCount = 0; - int currentItemIndex = getSelectedItemPosition(); - - ListAdapter adapter = getAdapter(); - if (adapter != null) { - final int count = adapter.getCount(); - if (count < 15) { - for (int i = 0; i < count; i++) { - if (adapter.isEnabled(i)) { - itemCount++; - } else if (i <= currentItemIndex) { - currentItemIndex--; - } - } - } else { - itemCount = count; + int itemCount = 0; + int currentItemIndex = getSelectedItemPosition(); + + ListAdapter adapter = getAdapter(); + if (adapter != null) { + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + if (adapter.isEnabled(i)) { + itemCount++; + } else if (i <= currentItemIndex) { + currentItemIndex--; } } - - event.setItemCount(itemCount); - event.setCurrentItemIndex(currentItemIndex); } - return populated; + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); } /** diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 02c1ec7..134e4c4 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -30,7 +30,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * can show completion suggestions for the substring of the text where * the user is typing instead of necessarily for the entire thing. * <p> - * You must must provide a {@link Tokenizer} to distinguish the + * 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 @@ -41,7 +41,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.autocomplete_7); - * + * * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, * android.R.layout.simple_dropdown_item_1line, COUNTRIES); * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); @@ -132,7 +132,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { * Instead of validating the entire text, this subclass method validates * each token of the text individually. Empty tokens are removed. */ - @Override + @Override public void performValidation() { Validator v = getValidator(); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index a5b7281..563fc26 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -392,11 +392,11 @@ public class PopupWindow { mContentView = contentView; - if (mContext == null) { + if (mContext == null && mContentView != null) { mContext = mContentView.getContext(); } - if (mWindowManager == null) { + if (mWindowManager == null && mContentView != null) { mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } } @@ -939,7 +939,9 @@ public class PopupWindow { * @param p the layout parameters of the popup's content view */ private void invokePopup(WindowManager.LayoutParams p) { - p.packageName = mContext.getPackageName(); + if (mContext != null) { + p.packageName = mContext.getPackageName(); + } mWindowManager.addView(mPopupView, p); } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 6b676b4..ed9114a 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -41,6 +43,8 @@ import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -49,8 +53,6 @@ import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - /** * <p> @@ -187,6 +189,7 @@ import com.android.internal.R; public class ProgressBar extends View { private static final int MAX_LEVEL = 10000; private static final int ANIMATION_RESOLUTION = 200; + private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; int mMinWidth; int mMaxWidth; @@ -218,6 +221,8 @@ public class ProgressBar extends View { private int mAnimationResolution; + private AccessibilityEventSender mAccessibilityEventSender; + /** * Create a new progress bar with range 0...100 and initial progress of 0. * @param context the application environment @@ -604,8 +609,11 @@ public class ProgressBar extends View { onProgressRefresh(scale, fromUser); } } - - void onProgressRefresh(float scale, boolean fromUser) { + + void onProgressRefresh(float scale, boolean fromUser) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + scheduleAccessibilityEventSender(); + } } private synchronized void refreshProgress(int id, int progress, boolean fromUser) { @@ -908,6 +916,12 @@ public class ProgressBar extends View { } @Override + public boolean isLayoutRtl(Drawable who) { + return (who == mProgressDrawable || who == mIndeterminateDrawable) ? + isLayoutRtl() : super.isLayoutRtl(who); + } + + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateDrawableBounds(w, h); } @@ -1069,8 +1083,46 @@ public class ProgressBar extends View { if (mIndeterminate) { stopAnimation(); } + if(mRefreshProgressRunnable != null) { + removeCallbacks(mRefreshProgressRunnable); + } + 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(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(mMax); + event.setCurrentItemIndex(mProgress); + } + + /** + * 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); + } + + /** + * Command for sending an accessibility event. + */ + private class AccessibilityEventSender implements Runnable { + public void run() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index a47359f..acd8539 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -16,32 +16,32 @@ package android.widget; -import com.android.internal.R; - import android.content.Context; -import android.content.res.TypedArray; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.SparseArray; -import android.util.Poolable; import android.util.Pool; -import android.util.Pools; +import android.util.Poolable; import android.util.PoolableManager; -import static android.util.Log.d; +import android.util.Pools; +import android.util.SparseArray; import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.RemoteViews.RemoteView; +import com.android.internal.R; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedList; import java.util.SortedSet; import java.util.TreeSet; -import java.util.LinkedList; -import java.util.HashSet; -import java.util.ArrayList; + +import static android.util.Log.d; /** * A Layout where the positions of the children can be described in relation to each other or to the @@ -186,6 +186,11 @@ public class RelativeLayout extends ViewGroup { a.recycle(); } + @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.LEFT | Gravity.TOP</code>. @@ -216,8 +221,8 @@ public class RelativeLayout extends ViewGroup { @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.BEFORE; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -231,9 +236,9 @@ public class RelativeLayout extends ViewGroup { @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { - final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { - mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + 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(); } } @@ -334,7 +339,7 @@ public class RelativeLayout extends ViewGroup { mHasBaselineAlignedChild = false; View ignore = null; - int gravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.LEFT && gravity != 0; gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0; @@ -489,7 +494,8 @@ public class RelativeLayout extends ViewGroup { height - mPaddingBottom); final Rect contentBounds = mContentBounds; - Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds); + Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds, + isLayoutRtl()); final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c854fac..9cf2718 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -125,7 +125,7 @@ public class RemoteViews implements Parcelable, Filter { * SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!! */ private abstract static class Action implements Parcelable { - public abstract void apply(View root) throws ActionException; + public abstract void apply(View root, ViewGroup rootParent) throws ActionException; public int describeContents() { return 0; @@ -183,7 +183,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (!(view instanceof AdapterView<?>)) return; @@ -214,7 +214,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -295,7 +295,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -360,6 +360,60 @@ public class RemoteViews implements Parcelable, Filter { public final static int TAG = 8; } + 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(TAG); + dest.writeInt(viewId); + intent.writeToParcel(dest, flags); + } + + @Override + public void apply(View root, ViewGroup rootParent) { + 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("RemoteViews", "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("RemoteViews", "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); + } else if (target instanceof AdapterViewAnimator) { + AdapterViewAnimator v = (AdapterViewAnimator) target; + v.setRemoteViewsAdapter(intent); + } + } + + int viewId; + Intent intent; + + public final static int TAG = 10; + } + /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} @@ -383,7 +437,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -479,7 +533,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -539,7 +593,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -755,7 +809,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -850,7 +904,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final Context context = root.getContext(); final ViewGroup target = (ViewGroup) root.findViewById(viewId); if (target == null) return; @@ -952,6 +1006,9 @@ public class RemoteViews implements Parcelable, Filter { case SetOnClickFillInIntent.TAG: mActions.add(new SetOnClickFillInIntent(parcel)); break; + case SetRemoteViewsAdapterIntent.TAG: + mActions.add(new SetRemoteViewsAdapterIntent(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1287,16 +1344,29 @@ public class RemoteViews implements Parcelable, Filter { /** * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * - * @param appWidgetId The id of the app widget which contains the specified view + * @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 view whose text should change * @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) { - // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent - // RemoteViewsService - intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, appWidgetId); - setIntent(viewId, "setRemoteViewsAdapter", 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 view whose text should change + * @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)); } /** @@ -1499,7 +1569,7 @@ public class RemoteViews implements Parcelable, Filter { result = inflater.inflate(mLayoutId, parent, false); - performApply(result); + performApply(result, parent); return result; } @@ -1514,15 +1584,15 @@ public class RemoteViews implements Parcelable, Filter { */ public void reapply(Context context, View v) { prepareContext(context); - performApply(v); + performApply(v, (ViewGroup) v.getParent()); } - private void performApply(View v) { + private void performApply(View v, ViewGroup parent) { if (mActions != null) { final int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); - a.apply(v); + a.apply(v, parent); } } } diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 1c0a2bb..40b0a9c 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -29,6 +29,7 @@ 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.view.LayoutInflater; import android.view.View; @@ -156,13 +157,16 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // create in response to this bind factory.onDataSetChanged(); } - } catch (Exception e) { + } 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 @@ -777,7 +781,9 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback tmpMetaData.count = count; tmpMetaData.setLoadingViewTemplates(loadingView, firstView); } - } catch (Exception e) { + } catch(RemoteException e) { + processException("updateMetaData", e); + } catch(RuntimeException e) { processException("updateMetaData", e); } } @@ -792,12 +798,15 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback try { remoteViews = factory.getViewAt(position); itemId = factory.getItemId(position); - } catch (Exception e) { + } 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) { @@ -971,18 +980,20 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback return getCount() <= 0; } - private void onNotifyDataSetChanged() { // Complete the actual notifyDataSetChanged() call initiated earlier IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); try { factory.onDataSetChanged(); - } catch (Exception e) { + } 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 diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index e0b08d4..7ba4777 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -138,34 +138,87 @@ public abstract class RemoteViewsService extends Service { return mIsCreated; } public synchronized void onDataSetChanged() { - mFactory.onDataSetChanged(); + try { + mFactory.onDataSetChanged(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } } public synchronized int getCount() { - return mFactory.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 = mFactory.getViewAt(position); - rv.setIsWidgetCollectionChild(true); + 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() { - return mFactory.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() { - return mFactory.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) { - return mFactory.getItemId(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() { - return mFactory.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); - factory.onDestroy(); + try { + factory.onDestroy(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } RemoteViewsService.sRemoteViewFactories.remove(fc); } } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index ade3a0a..27edb88 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -162,6 +162,11 @@ public class ScrollView extends FrameLayout { } @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override protected float getTopFadingEdgeStrength() { if (getChildCount() == 0) { return 0.0f; diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 9933d68..586ece8 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -93,6 +93,7 @@ public class SearchView extends LinearLayout { private boolean mClearingFocus; private int mMaxWidth; private boolean mVoiceButtonEnabled; + private CharSequence mUserQuery; private SearchableInfo mSearchable; private Bundle mAppSearchData; @@ -372,6 +373,7 @@ public class SearchView extends LinearLayout { mQueryTextView.setText(query); if (query != null) { mQueryTextView.setSelection(query.length()); + mUserQuery = query; } // If the query is not empty and submit is requested, submit the query @@ -885,6 +887,7 @@ public class SearchView extends LinearLayout { private void onTextChanged(CharSequence newText) { CharSequence text = mQueryTextView.getText(); + mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); if (isSubmitButtonEnabled()) { updateSubmitButton(hasText); @@ -1124,7 +1127,7 @@ public class SearchView extends LinearLayout { if (data != null) { intent.setData(data); } - intent.putExtra(SearchManager.USER_QUERY, query); + intent.putExtra(SearchManager.USER_QUERY, mUserQuery); if (query != null) { intent.putExtra(SearchManager.QUERY, query); } diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 3d2a252..c5c6c69 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -338,6 +338,12 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { @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 + if (mFrom == null) { + findColumns(mOriginalFrom); + } Cursor res = super.swapCursor(c); // rescan columns in case cursor layout is different findColumns(mOriginalFrom); @@ -358,7 +364,13 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { mOriginalFrom = from; mTo = to; - super.changeCursor(c); + // super.changeCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + if (mFrom == null) { + findColumns(mOriginalFrom); + } + super.changeCursor(c); findColumns(mOriginalFrom); } diff --git a/core/java/android/widget/Space.java b/core/java/android/widget/Space.java new file mode 100644 index 0000000..d98b937 --- /dev/null +++ b/core/java/android/widget/Space.java @@ -0,0 +1,75 @@ +/* + * 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; +import android.view.ViewGroup; + +/** + * 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 defStyle) { + super(context, attrs, defStyle); + } + + /** + * {@inheritDoc} + */ + public Space(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * {@inheritDoc} + */ + public Space(Context context) { + super(context); + } + + /** + * Draw nothing. + * + * @param canvas an unused parameter. + */ + @Override + public void draw(Canvas canvas) { + } + + /** + * {@inheritDoc} + */ + @Override + public ViewGroup.LayoutParams getLayoutParams() { + return super.getLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setLayoutParams(ViewGroup.LayoutParams params) { + super.setLayoutParams(params); + } +} diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 21c61bd..c4ba7c8 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -20,6 +20,7 @@ import java.lang.ref.WeakReference; 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; @@ -115,7 +116,7 @@ public class StackView extends AdapterViewAnimator { private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; - private static long MIN_TIME_BETWEEN_SCROLLS = 100; + private static final long MIN_TIME_BETWEEN_SCROLLS = 100; /** * These variables are all related to the current state of touch interaction @@ -132,6 +133,8 @@ public class StackView extends AdapterViewAnimator { private int mMaximumVelocity; private VelocityTracker mVelocityTracker; private boolean mTransitionIsSetup = false; + private int mResOutColor; + private int mClickColor; private static HolographicHelper sHolographicHelper; private ImageView mHighlight; @@ -146,12 +149,24 @@ public class StackView extends AdapterViewAnimator { private final Rect stackInvalidateRect = new Rect(); public StackView(Context context) { - super(context); - initStackView(); + this(context, null); } public StackView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, com.android.internal.R.attr.stackViewStyle); + } + + public StackView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.StackView, defStyleAttr, 0); + + 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(); } @@ -198,8 +213,7 @@ public class StackView extends AdapterViewAnimator { * Animate the views between different relative indexes within the {@link AdapterViewAnimator} */ void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) { - ObjectAnimator alphaOa = null; - ObjectAnimator oldAlphaOa = null; + ObjectAnimator alphaOa; if (!animate) { ((StackFrame) view).cancelSliderAnimator(); @@ -357,7 +371,7 @@ public class StackView extends AdapterViewAnimator { private void setupStackSlider(View v, int mode) { mStackSlider.setMode(mode); if (v != null) { - mHighlight.setImageBitmap(sHolographicHelper.createOutline(v)); + mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor)); mHighlight.setRotation(v.getRotation()); mHighlight.setTranslationY(v.getTranslationY()); mHighlight.setTranslationX(v.getTranslationX()); @@ -412,8 +426,8 @@ public class StackView extends AdapterViewAnimator { // 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()); - ViewAndIndex vi = mViewsMap.get(index); - if (vi != null) { + ViewAndMetaData vm = mViewsMap.get(index); + if (vm != null) { View v = mViewsMap.get(index).view; if (v != null) v.bringToFront(); } @@ -429,8 +443,8 @@ public class StackView extends AdapterViewAnimator { if (!mClickFeedbackIsValid) { View v = getViewAtRelativeIndex(1); if (v != null) { - mClickFeedback.setImageBitmap(sHolographicHelper.createOutline(v, - HolographicHelper.CLICK_FEEDBACK)); + mClickFeedback.setImageBitmap( + sHolographicHelper.createClickOutline(v, mClickColor)); mClickFeedback.setTranslationX(v.getTranslationX()); mClickFeedback.setTranslationY(v.getTranslationY()); } @@ -1261,13 +1275,11 @@ public class StackView extends AdapterViewAnimator { boolean firstPass = true; parentRect.set(0, 0, 0, 0); - int depth = 0; while (p.getParent() != null && p.getParent() instanceof View && !parentRect.contains(globalInvalidateRect)) { if (!firstPass) { globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() - p.getScrollY()); - depth++; } firstPass = false; p = (View) p.getParent(); @@ -1355,16 +1367,19 @@ public class StackView extends AdapterViewAnimator { mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); } - Bitmap createOutline(View v) { - return createOutline(v, RES_OUT); + 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) { + Bitmap createOutline(View v, int type, int color) { + mHolographicPaint.setColor(color); if (type == RES_OUT) { - mHolographicPaint.setColor(0xff6699ff); mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); } else if (type == CLICK_FEEDBACK) { - mHolographicPaint.setColor(0x886699ff); mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); } diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 6f76dd0..1fe1f79 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -427,12 +427,19 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - event.setItemCount(getTabCount()); - event.setCurrentItemIndex(mSelectedTab); + onPopulateAccessibilityEvent(event); + // Dispatch only to the selected tab. if (mSelectedTab != -1) { - getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); + return getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); } - return true; + return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(getTabCount()); + event.setCurrentItemIndex(mSelectedTab); } /** diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index b612004..5f20c85 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -224,7 +224,8 @@ public class TableRow extends LinearLayout { final int childWidth = child.getMeasuredWidth(); lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth; - switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: // don't offset on X axis break; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 13b9285f..9592d0c 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -16,11 +16,6 @@ package android.widget; -import com.android.internal.util.FastMath; -import com.android.internal.widget.EditableInputConnection; - -import org.xmlpull.v1.XmlPullParserException; - import android.R; import android.content.ClipData; import android.content.ClipData.Item; @@ -60,6 +55,7 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; @@ -80,12 +76,17 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; +import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; +import android.text.style.SuggestionSpan; +import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; @@ -102,12 +103,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewAncestor; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; -import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; @@ -123,8 +124,14 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.RemoteViews.RemoteView; +import com.android.internal.util.FastMath; +import com.android.internal.widget.EditableInputConnection; + +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.lang.ref.WeakReference; +import java.text.BreakIterator; import java.util.ArrayList; /** @@ -309,6 +316,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; + private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; + private int mTextEditSuggestionItemLayout; + private SuggestionsPopupWindow mSuggestionsPopupWindow; + private SuggestionRangeSpan mSuggestionRangeSpan; + private boolean mSuggestionsEnabled = true; + private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 @@ -317,13 +330,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private Drawable mSelectHandleRight; private Drawable mSelectHandleCenter; - private int mLastDownPositionX, mLastDownPositionY; + private float mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; private final int mSquaredTouchSlopDistance; // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; + private WordIterator mWordIterator; + /* * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). @@ -777,9 +792,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0); break; + case com.android.internal.R.styleable.TextView_textEditSuggestionsBottomWindowLayout: + mTextEditSuggestionsBottomWindowLayout = a.getResourceId(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_textEditSuggestionsTopWindowLayout: + mTextEditSuggestionsTopWindowLayout = 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_textIsSelectable: mTextIsSelectable = a.getBoolean(attr, false); break; + + case com.android.internal.R.styleable.TextView_suggestionsEnabled: + mSuggestionsEnabled = a.getBoolean(attr, true); + break; } } a.recycle(); @@ -2062,8 +2093,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_gravity */ public void setGravity(int gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.BEFORE; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { gravity |= Gravity.TOP; @@ -2071,8 +2102,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean newLayout = false; - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != - (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != + (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) { newLayout = true; } @@ -2534,6 +2565,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sp.removeSpan(cw); } + // hideControllers would do it, but it gets called after this method on rotation + sp.removeSpan(mSuggestionRangeSpan); + ss.text = sp; } else { ss.text = mText.toString(); @@ -2872,8 +2906,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mCharWrapper, mBufferType, false, oldlen); } - private static class CharWrapper - implements CharSequence, GetChars, GraphicsOperations { + private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations { private char[] mChars; private int mStart, mLength; @@ -2949,6 +2982,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener advancesIndex); } + public float getTextRunAdvances(int start, int end, int contextStart, + int contextEnd, int flags, float[] advances, int advancesIndex, + Paint p, int reserved) { + int count = end - start; + int contextCount = contextEnd - contextStart; + return p.getTextRunAdvances(mChars, start + mStart, count, + contextStart + mStart, contextCount, flags, advances, + advancesIndex, reserved); + } + public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, int cursorOpt, Paint p) { int contextCount = contextEnd - contextStart; @@ -3337,13 +3380,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Handler h = getHandler(); if (h != null) { long eventTime = SystemClock.uptimeMillis(); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, 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))); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, @@ -3977,13 +4020,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener observer.removeOnPreDrawListener(this); mPreDrawState = PREDRAW_NOT_REGISTERED; } - // No need to create the controller, as getXXController would. - if (mInsertionPointCursorController != null) { - observer.removeOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - observer.removeOnTouchModeChangeListener(mSelectionModifierCursorController); - } if (mError != null) { hideError(); @@ -4109,6 +4145,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override + public boolean isLayoutRtl(Drawable who) { + if (who == null) return false; + if (mDrawables != null) { + final Drawables drawables = mDrawables; + if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight || + who == drawables.mDrawableTop || who == drawables.mDrawableBottom) { + return isLayoutRtl(); + } + } + return super.isLayoutRtl(who); + } + + @Override protected boolean onSetAlpha(int alpha) { // Alpha is supported if and only if the drawing can be done in one pass. // TODO text with spans with a background color currently do not respect this alpha. @@ -4210,6 +4259,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onDraw(Canvas canvas) { + if (mPreDrawState == PREDRAW_DONE) { + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnPreDrawListener(this); + mPreDrawState = PREDRAW_NOT_REGISTERED; + } + if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return; restartMarqueeIfNeeded(); @@ -4281,12 +4336,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (mPreDrawState == PREDRAW_DONE) { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnPreDrawListener(this); - mPreDrawState = PREDRAW_NOT_REGISTERED; - } - int color = mCurTextColor; if (mLayout == null) { @@ -4347,9 +4396,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); } + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (!mSingleLine && getLineCount() == 1 && canMarquee() && - (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { + (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f); } @@ -4549,15 +4599,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (translate) canvas.translate(0, -cursorOffsetVertical); } - /** - * Update the positions of the CursorControllers. Needed by WebTextView, - * which does not draw. - * @hide - */ - protected void updateCursorControllerPositions() { - // TODO remove - } - @Override public void getFocusedRect(Rect r) { if (mLayout == null) { @@ -5236,6 +5277,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param text The auto complete text the user has selected. */ public void onCommitCompletion(CompletionInfo text) { + // intentionally empty } /** @@ -5422,6 +5464,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #beginBatchEdit()}. */ public void onBeginBatchEdit() { + // intentionally empty } /** @@ -5429,6 +5472,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #endBatchEdit}. */ public void onEndBatchEdit() { + // intentionally empty } /** @@ -5501,7 +5545,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } Layout.Alignment alignment; - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: alignment = Layout.Alignment.ALIGN_CENTER; break; @@ -6275,15 +6320,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (isFocused()) { - // This offsets because getInterestingRect() is in terms of - // viewport coordinates, but requestRectangleOnScreen() - // is in terms of content coordinates. + // This offsets because getInterestingRect() is in terms of viewport coordinates, but + // requestRectangleOnScreen() is in terms of content coordinates. - Rect r = new Rect(x, top, x + 1, bottom); - getInterestingRect(r, line); - r.offset(mScrollX, mScrollY); + if (mTempRect == null) mTempRect = new Rect(); + mTempRect.set(x, top, x + 1, bottom); + getInterestingRect(mTempRect, line); + mTempRect.offset(mScrollX, mScrollY); - if (requestRectangleOnScreen(r)) { + if (requestRectangleOnScreen(mTempRect)) { changed = true; } } @@ -6756,25 +6801,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * This method is called when the text is changed, in case any - * subclasses would like to know. + * 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 before The offset of the former end of the range of the - * text that was modified. If text was simply inserted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or deleted, the - * length of the old text was <code>before-start</code>. - * @param after The offset of the end of the range of the text - * that was modified. If text was simply deleted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or inserted, - * the length of the new text is <code>after-start</code>. + * @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 before, int after) { + protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + // intentionally empty } /** @@ -6785,6 +6827,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param selEnd The new selection end location. */ protected void onSelectionChanged(int selStart, int selEnd) { + // intentionally empty } /** @@ -7131,7 +7174,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // The DecorView does not have focus when the 'Done' ExtractEditText button is - // pressed. Since it is the ViewRoot's mView, it requests focus before + // 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. @@ -7200,14 +7243,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } super.onFocusChanged(focused, direction, previouslyFocusedRect); - - // Performed after super.onFocusChanged so that this TextView is registered and can ask for - // the IME. Showing the IME while focus is moved using the D-Pad is a bad idea, however this - // does not happen in that case (using the arrows on a bluetooth keyboard). - if (focused && isTextEditable()) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) imm.showSoftInput(this, 0); - } } private int getLastTapPosition() { @@ -7247,11 +7282,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInputContentType.enterDown = false; } hideControllers(); + removeAllSuggestionSpans(); } startStopMarquee(hasWindowFocus); } + private void removeAllSuggestionSpans() { + if (mText instanceof Editable) { + Editable editable = ((Editable) mText); + SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class); + final int length = spans.length; + for (int i = 0; i < length; i++) { + editable.removeSpan(spans[i]); + } + } + } + @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); @@ -7298,8 +7345,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (action == MotionEvent.ACTION_DOWN) { - mLastDownPositionX = (int) event.getX(); - mLastDownPositionY = (int) event.getY(); + mLastDownPositionX = event.getX(); + mLastDownPositionY = event.getY(); // Reset this state; it will be re-set if super.onTouchEvent // causes focus to move to the view. @@ -7326,9 +7373,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener && mText instanceof Spannable && mLayout != null) { boolean handled = false; - final int oldScrollX = mScrollX; - final int oldScrollY = mScrollY; - if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } @@ -7345,27 +7389,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (isTextEditable() || mTextIsSelectable) { - if (mScrollX != oldScrollX || mScrollY != oldScrollY) { // TODO remove - // Hide insertion anchor while scrolling. Leave selection. - hideInsertionPointCursorController(); // TODO any motion should hide it + if ((isTextEditable() || mTextIsSelectable) && touchIsFinished) { + // Show the IME, except when selecting in read-only text. + if (!mTextIsSelectable) { + final InputMethodManager imm = InputMethodManager.peekInstance(); + handled |= imm != null && imm.showSoftInput(this, 0); } - if (touchIsFinished) { - // Show the IME, except when selecting in read-only text. - if (!mTextIsSelectable) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - handled |= imm != null && imm.showSoftInput(this, 0); - } - - boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); - if (!selectAllGotFocus && hasSelection()) { - startSelectionActionMode(); - } else { - stopSelectionActionMode(); - if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) { - getInsertionController().show(); - } + boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); + if (!selectAllGotFocus && hasSelection()) { + startSelectionActionMode(); + } else { + stopSelectionActionMode(); + hideSuggestions(); + if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) { + getInsertionController().show(); } } } @@ -7543,7 +7581,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return 0.0f; } } else if (getLineCount() == 1) { - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: return 0.0f; case Gravity.RIGHT: @@ -7566,7 +7605,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final Marquee marquee = mMarquee; return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength(); } else if (getLineCount() == 1) { - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() - getCompoundPaddingRight(); @@ -7742,88 +7782,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener hasPrimaryClip()); } - private boolean isWordCharacter(int c, int type) { - return (c == '\'' || c == '"' || - type == Character.UPPERCASE_LETTER || - type == Character.LOWERCASE_LETTER || - type == Character.TITLECASE_LETTER || - type == Character.MODIFIER_LETTER || - type == Character.OTHER_LETTER || // Should handle asian characters - type == Character.DECIMAL_DIGIT_NUMBER); - } - - /** - * Returns the offsets delimiting the 'word' located at position offset. - * - * @param offset An offset in the text. - * @return The offsets for the start and end of the word located at <code>offset</code>. - * The two ints offsets are packed in a long using {@link #packRangeInLong(int, int)}. - * Returns -1 if no valid word was found. - */ - private long getWordLimitsAt(int offset) { - int klass = mInputType & InputType.TYPE_MASK_CLASS; - int variation = mInputType & InputType.TYPE_MASK_VARIATION; - - // Text selection is not permitted in password fields - if (hasPasswordTransformationMethod()) { - return -1; - } - - final int len = mText.length(); - - // Specific text fields: always select the entire text - 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 len > 0 ? packRangeInLong(0, len) : -1; - } - - int end = Math.min(offset, len); - if (end < 0) { - return -1; - } - - final int MAX_LENGTH = 48; - int start = end; - - for (; start > 0; start--) { - final char c = mTransformed.charAt(start - 1); - final int type = Character.getType(c); - if (start == end && type == Character.OTHER_PUNCTUATION) { - // Cases where the text ends with a '.' and we select from the end of the line - // (right after the dot), or when we select from the space character in "aaa, bbb". - continue; - } - if (type == Character.SURROGATE) { // Two Character codepoint - end = start - 1; // Recheck as a pair when scanning forward - continue; - } - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - } - - for (; end < len; end++) { - final int c = Character.codePointAt(mTransformed, end); - final int type = Character.getType(c); - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - if (c > 0xFFFF) { // Two Character codepoint - end++; - } - } - - if (start == end) { - return -1; - } - - // Two ints packed in a long - return packRangeInLong(start, end); - } - private static long packRangeInLong(int start, int end) { return (((long) start) << 32) | end; } @@ -7836,21 +7794,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return (int) (range & 0x00000000FFFFFFFFL); } - private void selectAll() { - Selection.setSelection((Spannable) mText, 0, mText.length()); + private boolean selectAll() { + final int length = mText.length(); + Selection.setSelection((Spannable) mText, 0, length); + return length > 0; } - private void selectCurrentWord() { + /** + * Adjusts selection to the word under last touch offset. + * Return true if the operation was successfully performed. + */ + private boolean selectCurrentWord() { if (!canSelectText()) { - return; + return false; } if (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. - selectAll(); - return; + return selectAll(); + } + + int klass = mInputType & InputType.TYPE_MASK_CLASS; + int variation = mInputType & 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 selectAll(); } long lastTouchOffsets = getLastTouchOffsets(); @@ -7866,22 +7843,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectionStart = ((Spanned) mText).getSpanStart(url); selectionEnd = ((Spanned) mText).getSpanEnd(url); } else { - long wordLimits = getWordLimitsAt(minOffset); - if (wordLimits >= 0) { - selectionStart = extractRangeStartFromLong(wordLimits); - } else { - selectionStart = Math.max(minOffset - 5, 0); + if (mWordIterator == null) { + mWordIterator = new WordIterator(); } + // WordIerator handles text changes, this is a no-op if text in unchanged. + mWordIterator.setCharSequence(mText); - wordLimits = getWordLimitsAt(maxOffset); - if (wordLimits >= 0) { - selectionEnd = extractRangeEndFromLong(wordLimits); - } else { - selectionEnd = Math.min(maxOffset + 5, mText.length()); - } + selectionStart = mWordIterator.getBeginning(minOffset); + if (selectionStart == BreakIterator.DONE) return false; + + selectionEnd = mWordIterator.getEnd(maxOffset); + if (selectionEnd == BreakIterator.DONE) return false; } Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + return true; } private long getLastTouchOffsets() { @@ -7900,28 +7876,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (!isShown()) { - return false; - } + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); final boolean isPassword = hasPasswordTransformationMethod(); - if (!isPassword) { CharSequence text = getText(); if (TextUtils.isEmpty(text)) { text = getHint(); } if (!TextUtils.isEmpty(text)) { - if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) { - text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1); - } event.getText().add(text); } - } else { - event.setPassword(isPassword); } - return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + final boolean isPassword = hasPasswordTransformationMethod(); + event.setPassword(isPassword); } void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, @@ -8009,6 +7984,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode}, * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut} * or {@link android.R.id#copy}. + * + * @return true if the context menu item action was performed. */ public boolean onTextContextMenuItem(int id) { int min = 0; @@ -8045,7 +8022,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECTION_MODE: if (mSelectionActionMode != null) { // Selection mode is already started, simply change selected part. - updateSelectedRegion(); + selectCurrentWord(); } else { startSelectionActionMode(); } @@ -8053,7 +8030,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECT_ALL: // This does not enter text selection mode. Text is highlighted, so that it can be - // bulk edited, like selectAllOnFocus does. + // bulk edited, like selectAllOnFocus does. Returns true even if text is empty. selectAll(); return true; @@ -8177,10 +8154,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Long press in empty space moves cursor and shows the Paste affordance if available. if (!isPositionOnText(mLastDownPositionX, mLastDownPositionY) && mInsertionControllerEnabled) { - final int offset = getOffset(mLastDownPositionX, mLastDownPositionY); + final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); - Selection.setSelection((Spannable)mText, offset); - getInsertionController().show(0); + Selection.setSelection((Spannable) mText, offset); + getInsertionController().showWithPaste(); handled = true; } @@ -8195,8 +8172,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); stopSelectionActionMode(); } else { - // New selection at touch position - updateSelectedRegion(); + selectCurrentWord(); } handled = true; } @@ -8212,17 +8188,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return handled; } - /** - * When selection mode is already started, this method simply updates the selected part of text - * to the text under the finger. - */ - private void updateSelectedRegion() { - // Start a new selection at current position, keep selectionAction mode on - selectCurrentWord(); - // Updates handles' positions - getSelectionController().show(); - } - private boolean touchPositionIsInSelection() { int selectionStart = getSelectionStart(); int selectionEnd = getSelectionEnd(); @@ -8245,6 +8210,460 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); } + private static class SuggestionRangeSpan extends UnderlineSpan { + // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but + // there is no way to have underline and TextAppearanceSpan. + } + + private class SuggestionsPopupWindow implements OnClickListener { + private static final int MAX_NUMBER_SUGGESTIONS = 5; + private static final int NO_SUGGESTIONS = -1; + private final PopupWindow mContainer; + private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; + private final int[] mSuggestionViewLayouts = new int[] { + mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; + private WordIterator mSuggestionWordIterator; + private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; + + public SuggestionsPopupWindow() { + mContainer = new PopupWindow(TextView.this.mContext, null, + com.android.internal.R.attr.textSuggestionsWindowStyle); + 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); + } + + private class SuggestionInfo { + int suggestionStart, suggestionEnd; // range of suggestion item with replacement text + int spanStart, spanEnd; // range in TextView where text should be inserted + SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents + int suggestionIndex; // the index of the suggestion inside suggestionSpan + } + + private ViewGroup getViewGroup(boolean under) { + final int viewIndex = under ? 0 : 1; + ViewGroup viewGroup = mSuggestionViews[viewIndex]; + + if (viewGroup == null) { + final int layout = mSuggestionViewLayouts[viewIndex]; + LayoutInflater inflater = (LayoutInflater) TextView.this.mContext. + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (inflater == null) { + throw new IllegalArgumentException( + "Unable to create TextEdit suggestion window inflater"); + } + + View view = inflater.inflate(layout, null); + + if (! (view instanceof ViewGroup)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion window is not a ViewGroup: " + view); + } + + viewGroup = (ViewGroup) view; + + // Inflate the suggestion items once and for all. + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup, + false); + + if (! (childView instanceof TextView)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion item is not a TextView: " + childView); + } + + childView.setTag(new SuggestionInfo()); + viewGroup.addView(childView); + childView.setOnClickListener(this); + } + + mSuggestionViews[viewIndex] = viewGroup; + } + + return viewGroup; + } + + public void show() { + if (!(mText instanceof Editable)) return; + + final int pos = TextView.this.getSelectionStart(); + Spannable spannable = (Spannable)TextView.this.mText; + SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); + final int nbSpans = suggestionSpans.length; + + ViewGroup viewGroup = getViewGroup(true); + mContainer.setContentView(viewGroup); + + int totalNbSuggestions = 0; + int spanUnionStart = mText.length(); + int spanUnionEnd = 0; + + for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { + SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; + final int spanStart = spannable.getSpanStart(suggestionSpan); + final int spanEnd = spannable.getSpanEnd(suggestionSpan); + spanUnionStart = Math.min(spanStart, spanUnionStart); + spanUnionEnd = Math.max(spanEnd, spanUnionEnd); + + String[] suggestions = suggestionSpan.getSuggestions(); + int nbSuggestions = suggestions.length; + for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { + TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); + textView.setText(suggestions[suggestionIndex]); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = spanStart; + suggestionInfo.spanEnd = spanEnd; + suggestionInfo.suggestionSpan = suggestionSpan; + suggestionInfo.suggestionIndex = suggestionIndex; + + totalNbSuggestions++; + if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { + // Also end outer for loop + spanIndex = nbSpans; + break; + } + } + } + + if (totalNbSuggestions == 0) { + // TODO Replace by final text, use a dedicated layout, add a fade out timer... + TextView textView = (TextView) viewGroup.getChildAt(0); + textView.setText("No suggestions available"); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = NO_SUGGESTIONS; + totalNbSuggestions++; + } else { + if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); + ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + for (int i = 0; i < totalNbSuggestions; i++) { + final TextView textView = (TextView) viewGroup.getChildAt(i); + highlightTextDifferences(textView, spanUnionStart, spanUnionEnd); + } + } + + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE); + } + + final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + viewGroup.measure(size, size); + + positionAtCursor(); + } + + private long[] getWordLimits(CharSequence text) { + // TODO locale for mSuggestionWordIterator + if (mSuggestionWordIterator == null) mSuggestionWordIterator = new WordIterator(); + mSuggestionWordIterator.setCharSequence(text); + + // First pass will simply count the number of words to be able to create an array + // Not too expensive since previous break positions are cached by the BreakIterator + int nbWords = 0; + int position = mSuggestionWordIterator.following(0); + while (position != BreakIterator.DONE) { + nbWords++; + position = mSuggestionWordIterator.following(position); + } + + int index = 0; + long[] result = new long[nbWords]; + + position = mSuggestionWordIterator.following(0); + while (position != BreakIterator.DONE) { + int wordStart = mSuggestionWordIterator.getBeginning(position); + result[index++] = packRangeInLong(wordStart, position); + position = mSuggestionWordIterator.following(position); + } + + return result; + } + + private TextAppearanceSpan highlightSpan(int index) { + final int length = mHighlightSpans.length; + if (index < length) { + return mHighlightSpans[index]; + } + + // Assumes indexes are requested in sequence: simply append one more item + TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1]; + System.arraycopy(mHighlightSpans, 0, newArray, 0, length); + TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext, + android.R.style.TextAppearance_SuggestionHighlight); + newArray[length] = highlightSpan; + mHighlightSpans = newArray; + return highlightSpan; + } + + private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) { + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + + // Remove all text formating by converting to Strings + final String text = textView.getText().toString(); + final String sourceText = mText.subSequence(spanStart, spanEnd).toString(); + + long[] sourceWordLimits = getWordLimits(sourceText); + long[] wordLimits = getWordLimits(text); + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd] + // The final result is made of 3 parts: the text before, between and after the span + // This is the text before, provided for context + ssb.append(mText.subSequence(unionStart, spanStart).toString()); + + // shift is used to offset spans positions wrt span's beginning + final int shift = spanStart - unionStart; + suggestionInfo.suggestionStart = shift; + suggestionInfo.suggestionEnd = shift + text.length(); + + // This is the actual suggestion text, which will be highlighted by the following code + ssb.append(text); + + String[] words = new String[wordLimits.length]; + for (int i = 0; i < wordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(wordLimits[i]); + int wordEnd = extractRangeEndFromLong(wordLimits[i]); + words[i] = text.substring(wordStart, wordEnd); + } + + // Highlighted word algorithm is based on word matching between source and text + // Matching words are found from left to right. TODO: change for RTL languages + // Characters between matching words are highlighted + int previousCommonWordIndex = -1; + int nbHighlightSpans = 0; + for (int i = 0; i < sourceWordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(sourceWordLimits[i]); + int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]); + String sourceWord = sourceText.substring(wordStart, wordEnd); + + for (int j = previousCommonWordIndex + 1; j < words.length; j++) { + if (sourceWord.equals(words[j])) { + if (j != previousCommonWordIndex + 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + // Compare characters between words + int previousSourceWordEnd = i == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[i - 1]); + int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]); + String sourceSpaces = sourceText.substring(previousSourceWordEnd, + sourceWordStart); + + int previousWordEnd = j == 0 ? 0 : + extractRangeEndFromLong(wordLimits[j - 1]); + int currentWordStart = extractRangeStartFromLong(wordLimits[j]); + String textSpaces = text.substring(previousWordEnd, currentWordStart); + + if (!sourceSpaces.equals(textSpaces)) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + previousWordEnd, shift + currentWordStart, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + previousCommonWordIndex = j; + break; + } + } + } + + // Finally, compare ends of Strings + if (previousCommonWordIndex < words.length - 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = textView.length(); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]); + String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length()); + + int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + String textSpaces = text.substring(lastCommonTextWordEnd, textView.length()); + + if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + lastCommonTextWordEnd, shift + textView.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // Final part, text after the current suggestion range. + ssb.append(mText.subSequence(spanEnd, unionEnd).toString()); + textView.setText(ssb); + } + + public void hide() { + if ((mText instanceof Editable) && mSuggestionRangeSpan != null) { + ((Editable) mText).removeSpan(mSuggestionRangeSpan); + } + mContainer.dismiss(); + } + + @Override + public void onClick(View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + if (spanStart != NO_SUGGESTIONS) { + // SuggestionSpans are removed by replace: save them before + Editable editable = ((Editable) mText); + 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); + } + + final int suggestionStart = suggestionInfo.suggestionStart; + final int suggestionEnd = suggestionInfo.suggestionEnd; + final String suggestion = textView.getText().subSequence( + suggestionStart, suggestionEnd).toString(); + final String originalText = mText.subSequence(spanStart, spanEnd).toString(); + ((Editable) mText).replace(spanStart, spanEnd, suggestion); + + // Swap text content between actual text and Suggestion span + String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); + suggestions[suggestionInfo.suggestionIndex] = originalText; + + // Notify source IME of the suggestion pick + if (!TextUtils.isEmpty( + suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { + InputMethodManager imm = InputMethodManager.peekInstance(); + imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, + suggestionInfo.suggestionIndex); + } + + // Restore previous SuggestionSpans + final int lengthDifference = 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) { + editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i], + suggestionSpansEnds[i] + lengthDifference, + suggestionSpansFlags[i]); + } + } + } + } + hide(); + } + + void positionAtCursor() { + View contentView = mContainer.getContentView(); + int width = contentView.getMeasuredWidth(); + int height = contentView.getMeasuredHeight(); + final int offset = TextView.this.getSelectionStart(); + final int line = mLayout.getLineForOffset(offset); + final int lineBottom = mLayout.getLineBottom(line); + float primaryHorizontal = mLayout.getPrimaryHorizontal(offset); + + final Rect bounds = sCursorControllerTempRect; + bounds.left = (int) (primaryHorizontal - width / 2.0f); + bounds.top = lineBottom; + + bounds.right = bounds.left + width; + bounds.bottom = bounds.top + height; + + convertFromViewportToContentCoordinates(bounds); + + final int[] coords = mTempCoords; + TextView.this.getLocationInWindow(coords); + coords[0] += bounds.left; + coords[1] += bounds.top; + + final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + final int screenHeight = displayMetrics.heightPixels; + + // Vertical clipping + if (coords[1] + height > screenHeight) { + // Try to position above current line instead + // TODO use top layout instead, reverse suggestion order, + // try full screen vertical down if it still does not fit. TBD with designers. + + // Update dimensions from new view + contentView = mContainer.getContentView(); + width = contentView.getMeasuredWidth(); + height = contentView.getMeasuredHeight(); + + final int lineTop = mLayout.getLineTop(line); + final int lineHeight = lineBottom - lineTop; + coords[1] -= height + lineHeight; + } + + // Horizontal clipping + coords[0] = Math.max(0, coords[0]); + coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]); + + mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]); + } + } + + void showSuggestions() { + if (!mSuggestionsEnabled || !isTextEditable()) return; + + if (mSuggestionsPopupWindow == null) { + mSuggestionsPopupWindow = new SuggestionsPopupWindow(); + } + hideControllers(); + mSuggestionsPopupWindow.show(); + } + + void hideSuggestions() { + if (mSuggestionsPopupWindow != null) { + mSuggestionsPopupWindow.hide(); + } + } + + /** + * Some parts of the text can have alternate suggestion text attached. This is typically done by + * the IME by adding {@link SuggestionSpan}s to the text. + * + * When suggestions are enabled (default), this list of suggestions will be displayed when the + * user double taps on these parts of the text. No suggestions are displayed when this value is + * false. Use {@link #setSuggestionsEnabled(boolean)} to change this value. + * + * @return true if the suggestions popup window is enabled. + * + * @attr ref android.R.styleable#TextView_suggestionsEnabled + */ + public boolean isSuggestionsEnabled() { + return mSuggestionsEnabled; + } + + /** + * Enables or disables the suggestion popup. See {@link #isSuggestionsEnabled()}. + * + * @param enabled Whether or not suggestions are enabled. + */ + public void setSuggestionsEnabled(boolean enabled) { + mSuggestionsEnabled = enabled; + } + /** * If provided, this ActionMode.Callback will be used to create the ActionMode when text * selection is initiated in this View. @@ -8296,9 +8715,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - if (!hasSelection()) { - // If selection mode is started after a device rotation, there is already a selection. - selectCurrentWord(); + boolean currentWordSelected = selectCurrentWord(); + if (!currentWordSelected) { + // No word found under cursor or text selection not permitted. + return false; } ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); @@ -8329,16 +8749,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = clipboard.getPrimaryClip(); if (clip != null) { - boolean didfirst = false; + boolean didFirst = false; for (int i=0; i<clip.getItemCount(); i++) { CharSequence paste = clip.getItemAt(i).coerceToText(getContext()); if (paste != null) { - if (!didfirst) { + if (!didFirst) { long minMax = prepareSpacesAroundPaste(min, max, paste); min = extractRangeStartFromLong(minMax); max = extractRangeEndFromLong(minMax); Selection.setSelection((Spannable) mText, max); ((Editable) mText).replace(min, max, paste); + didFirst = true; } else { ((Editable) mText).insert(getSelectionEnd(), "\n"); ((Editable) mText).insert(getSelectionEnd(), paste); @@ -8369,10 +8790,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onCreateActionMode(ActionMode mode, Menu menu) { TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme); - mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle)); + boolean allowText = getContext().getResources().getBoolean( + com.android.internal.R.bool.allow_action_menu_item_text_with_icon); + + mode.setTitle(allowText ? + mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null); mode.setSubtitle(null); + int selectAllIconId = 0; // No icon by default + if (!allowText) { + // Provide an icon, text will not be displayed on smaller screens. + selectAllIconId = styledAttributes.getResourceId( + R.styleable.Theme_actionModeSelectAllDrawable, 0); + } + menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setIcon(selectAllIconId). setAlphabeticShortcut('a'). setShowAsAction( MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); @@ -8453,65 +8886,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - /** - * A CursorController instance can be used to control a cursor in the text. - * It is not used outside of {@link TextView}. - * @hide - */ - private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { - /** - * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. - * See also {@link #hide()}. - */ - public void show(); - - /** - * Hide the cursor controller from screen. - * See also {@link #show()}. - */ - public void hide(); - - /** - * @return true if the CursorController is currently visible - */ - public boolean isShowing(); - - /** - * Update the controller's position. - */ - public void updatePosition(HandleView handle, int x, int y); - - public void updateOffset(HandleView handle, int offset); - - public void updatePosition(); - - public int getCurrentOffset(HandleView handle); - - /** - * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller - * a chance to become active and/or visible. - * @param event The touch event - */ - public boolean onTouchEvent(MotionEvent event); - - /** - * 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 to be recycled. - */ - public void onDetached(); - } - - private class PastePopupMenu implements OnClickListener { + private class PastePopupWindow implements OnClickListener { private final PopupWindow mContainer; - private int mPositionX; - private int mPositionY; private final View[] mPasteViews = new View[4]; private final int[] mPasteViewLayouts = new int[] { mTextEditPasteWindowLayout, mTextEditNoPasteWindowLayout, mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout }; - public PastePopupMenu() { + public PastePopupWindow() { mContainer = new PopupWindow(TextView.this.mContext, null, com.android.internal.R.attr.textSelectHandleWindowStyle); mContainer.setSplitTouchEnabled(true); @@ -8594,14 +8976,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener convertFromViewportToContentCoordinates(bounds); - mPositionX = bounds.left; - mPositionY = bounds.top; - - final int[] coords = mTempCoords; TextView.this.getLocationInWindow(coords); - coords[0] += mPositionX; - coords[1] += mPositionY; + coords[0] += bounds.left; + coords[1] += bounds.top; final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; if (coords[1] < 0) { @@ -8637,32 +9015,43 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private class HandleView extends View implements ViewTreeObserver.OnPreDrawListener { - private Drawable mDrawable; + private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener { + protected Drawable mDrawable; private final PopupWindow mContainer; // Position with respect to the parent TextView private int mPositionX, mPositionY; - private final CursorController mController; private boolean mIsDragging; // Offset from touch position to mPosition private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; - private float mHotspotX; + protected float mHotspotX; // 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) position in window + // Parent's (TextView) previous position in window private int mLastParentX, mLastParentY; - private float mDownPositionX, mDownPositionY; // PopupWindow container absolute position with respect to the enclosing window private int mContainerPositionX, mContainerPositionY; // Visible or not (scrolled off screen), whether or not this handle should be visible private boolean mIsActive = false; - // The insertion handle can have an associated PastePopupMenu - private boolean mIsInsertionHandle = false; - // Used to detect taps on the insertion handle, which will affect the PastePopupMenu - private long mTouchTimer; - private PastePopupMenu mPastePopupWindow; + + public HandleView() { + super(TextView.this.mContext); + mContainer = new PopupWindow(TextView.this.mContext, null, + com.android.internal.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mContainer.setContentView(this); + + initDrawable(); + + final int handleHeight = mDrawable.getIntrinsicHeight(); + mTouchOffsetY = -0.3f * handleHeight; + mIdealVerticalOffset = 0.7f * handleHeight; + } + + protected abstract void initDrawable(); // Touch-up filter: number of previous positions remembered private static final int HISTORY_SIZE = 5; @@ -8703,71 +9092,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (i > 0 && i < iMax && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { - mController.updateOffset(this, mPreviousOffsets[index]); - } - } - - public static final int LEFT = 0; - public static final int CENTER = 1; - public static final int RIGHT = 2; - - public HandleView(CursorController controller, int pos) { - super(TextView.this.mContext); - mController = controller; - mContainer = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mContainer.setSplitTouchEnabled(true); - mContainer.setClippingEnabled(false); - mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mContainer.setContentView(this); - - setPosition(pos); - } - - private void setPosition(int pos) { - int handleWidth; - switch (pos) { - case LEFT: { - if (mSelectHandleLeft == null) { - mSelectHandleLeft = mContext.getResources().getDrawable( - mTextSelectHandleLeftRes); - } - mDrawable = mSelectHandleLeft; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth * 3.0f / 4.0f; - break; - } - - case RIGHT: { - if (mSelectHandleRight == null) { - mSelectHandleRight = mContext.getResources().getDrawable( - mTextSelectHandleRightRes); - } - mDrawable = mSelectHandleRight; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 4.0f; - break; - } - - case CENTER: - default: { - if (mSelectHandleCenter == null) { - mSelectHandleCenter = mContext.getResources().getDrawable( - mTextSelectHandleRes); - } - mDrawable = mSelectHandleCenter; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 2.0f; - mIsInsertionHandle = true; - break; - } + updateOffset(mPreviousOffsets[index]); } - - final int handleHeight = mDrawable.getIntrinsicHeight(); - mTouchOffsetY = -0.3f * handleHeight; - mIdealVerticalOffset = 0.7f * handleHeight; - - invalidate(); } @Override @@ -8776,12 +9102,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } public void show() { - updateContainerPosition(); if (isShowing()) { mContainer.update(mContainerPositionX, mContainerPositionY, mRight - mLeft, mBottom - mTop); - - hidePastePopupWindow(); } else { mContainer.showAtLocation(TextView.this, 0, mContainerPositionX, mContainerPositionY); @@ -8793,10 +9116,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void dismiss() { + protected void dismiss() { mIsDragging = false; mContainer.dismiss(); - hidePastePopupWindow(); } public void hide() { @@ -8827,24 +9149,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); - final TextView hostView = TextView.this; + final TextView textView = TextView.this; - if (mTempRect == null) { - mTempRect = new Rect(); - } + if (mTempRect == null) mTempRect = new Rect(); final Rect clip = mTempRect; clip.left = compoundPaddingLeft; clip.top = extendedPaddingTop; - clip.right = hostView.getWidth() - compoundPaddingRight; - clip.bottom = hostView.getHeight() - extendedPaddingBottom; + clip.right = textView.getWidth() - compoundPaddingRight; + clip.bottom = textView.getHeight() - extendedPaddingBottom; - final ViewParent parent = hostView.getParent(); - if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + final ViewParent parent = textView.getParent(); + if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) { return false; } final int[] coords = mTempCoords; - hostView.getLocationInWindow(coords); + textView.getLocationInWindow(coords); final int posX = coords[0] + mPositionX + (int) mHotspotX; final int posY = coords[1] + mPositionY; @@ -8853,45 +9173,59 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener posY >= clip.top && posY <= clip.bottom; } - private void moveTo(int x, int y) { - mPositionX = x - TextView.this.mScrollX; - mPositionY = y - TextView.this.mScrollY; + public abstract int getCurrentCursorOffset(); - if (mIsDragging) { - TextView.this.getLocationInWindow(mTempCoords); - if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) { - mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX; - mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY; - mLastParentX = mTempCoords[0]; - mLastParentY = mTempCoords[1]; - } - // Hide paste popup window as soon as the handle is dragged. - hidePastePopupWindow(); + public abstract void updateOffset(int offset); + + public abstract void updatePosition(float x, float y); + + protected void positionAtCursorOffset(int offset) { + // A HandleView relies on the layout, which may be nulled by external methods. + if (mLayout == null) { + // Will update controllers' state, hiding them and stopping selection mode if needed + prepareCursorControllers(); + return; } + + addPositionToTouchUpFilter(offset); + final int line = mLayout.getLineForOffset(offset); + final int lineBottom = mLayout.getLineBottom(line); + + mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); + mPositionY = lineBottom; + + // Take TextView's padding into account. + mPositionX += viewportToContentHorizontalOffset(); + mPositionY += viewportToContentVerticalOffset(); } - /** - * Updates the global container's position. - * @return whether or not the position has actually changed - */ - private boolean updateContainerPosition() { - // TODO Prevent this using different HandleView subclasses - mController.updateOffset(this, mController.getCurrentOffset(this)); + protected boolean updateContainerPosition() { + positionAtCursorOffset(getCurrentCursorOffset()); + + final int previousContainerPositionX = mContainerPositionX; + final int previousContainerPositionY = mContainerPositionY; + TextView.this.getLocationInWindow(mTempCoords); - final int containerPositionX = mTempCoords[0] + mPositionX; - final int containerPositionY = mTempCoords[1] + mPositionY; + mContainerPositionX = mTempCoords[0] + mPositionX; + mContainerPositionY = mTempCoords[1] + mPositionY; - if (containerPositionX != mContainerPositionX || - containerPositionY != mContainerPositionY) { - mContainerPositionX = containerPositionX; - mContainerPositionY = containerPositionY; - return true; - } - return false; + return (previousContainerPositionX != mContainerPositionX || + previousContainerPositionY != mContainerPositionY); } public boolean onPreDraw() { if (updateContainerPosition()) { + if (mIsDragging) { + if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) { + mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX; + mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY; + mLastParentX = mTempCoords[0]; + mLastParentY = mTempCoords[1]; + } + } + + onHandleMoved(); + if (isPositionVisible()) { mContainer.update(mContainerPositionX, mContainerPositionY, mRight - mLeft, mBottom - mTop); @@ -8904,9 +9238,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener dismiss(); } } - - // Hide paste popup as soon as the view is scrolled or moved - hidePastePopupWindow(); } return true; } @@ -8921,20 +9252,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - startTouchUpFilter(mController.getCurrentOffset(this)); - mDownPositionX = ev.getRawX(); - mDownPositionY = ev.getRawY(); - mTouchToWindowOffsetX = mDownPositionX - mPositionX; - mTouchToWindowOffsetY = mDownPositionY - mPositionY; + startTouchUpFilter(getCurrentCursorOffset()); + mTouchToWindowOffsetX = ev.getRawX() - mPositionX; + mTouchToWindowOffsetY = ev.getRawY() - mPositionY; final int[] coords = mTempCoords; TextView.this.getLocationInWindow(coords); mLastParentX = coords[0]; mLastParentY = coords[1]; mIsDragging = true; - if (mIsInsertionHandle) { - mTouchTimer = SystemClock.uptimeMillis(); - } break; } @@ -8958,27 +9284,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; - mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); + updatePosition(newPosX, newPosY); break; } case MotionEvent.ACTION_UP: - if (mIsInsertionHandle) { - long delay = SystemClock.uptimeMillis() - mTouchTimer; - if (delay < ViewConfiguration.getTapTimeout()) { - final float deltaX = mDownPositionX - ev.getRawX(); - final float deltaY = mDownPositionY - ev.getRawY(); - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { - if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { - // Tapping on the handle dismisses the displayed paste view, - mPastePopupWindow.hide(); - } else { - ((InsertionPointCursorController) mController).show(0); - } - } - } - } filterOnTouchUp(); mIsDragging = false; break; @@ -8994,60 +9304,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mIsDragging; } - void positionAtCursor(int offset) { - addPositionToTouchUpFilter(offset); - final int width = mDrawable.getIntrinsicWidth(); - final int height = mDrawable.getIntrinsicHeight(); - final int line = mLayout.getLineForOffset(offset); - final int lineBottom = mLayout.getLineBottom(line); - - final Rect bounds = sCursorControllerTempRect; - bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX) + - TextView.this.mScrollX; - bounds.top = lineBottom + TextView.this.mScrollY; - - bounds.right = bounds.left + width; - bounds.bottom = bounds.top + height; - - convertFromViewportToContentCoordinates(bounds); - moveTo(bounds.left, bounds.top); - } - - void showPastePopupWindow() { - if (mIsInsertionHandle) { - if (mPastePopupWindow == null) { - // Lazy initialisation: create when actually shown only. - mPastePopupWindow = new PastePopupMenu(); - } - mPastePopupWindow.show(); - } + void onHandleMoved() { + // Does nothing by default } - void hidePastePopupWindow() { - if (mPastePopupWindow != null) { - mPastePopupWindow.hide(); - } + public void onDetached() { + // Should be overriden to clean possible Runnable } } - private class InsertionPointCursorController implements CursorController { + private class InsertionHandleView extends HandleView { private static final int DELAY_BEFORE_FADE_OUT = 4000; - private static final int DELAY_BEFORE_PASTE = 2000; - private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; + private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - // The cursor controller image. Lazily created. - private HandleView mHandle; + // Used to detect taps on the insertion handle, which will affect the PastePopupWindow + private long mTouchTimer; + private float mDownPositionX, mDownPositionY; + private PastePopupWindow mPastePopupWindow; private Runnable mHider; private Runnable mPastePopupShower; + @Override public void show() { - show(DELAY_BEFORE_PASTE); + super.show(); + hideDelayed(); + hidePastePopupWindow(); } public void show(int delayBeforePaste) { - getHandle().show(); - hideDelayed(); - removePastePopupCallback(); + show(); + final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime; if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { delayBeforePaste = 0; @@ -9056,81 +9342,256 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mPastePopupShower == null) { mPastePopupShower = new Runnable() { public void run() { - getHandle().showPastePopupWindow(); + showPastePopupWindow(); } }; } - postDelayed(mPastePopupShower, delayBeforePaste); + TextView.this.postDelayed(mPastePopupShower, delayBeforePaste); } } - private void removePastePopupCallback() { - if (mPastePopupShower != null) { - removeCallbacks(mPastePopupShower); + @Override + protected void dismiss() { + super.dismiss(); + onDetached(); + } + + private void hideDelayed() { + removeHiderCallback(); + if (mHider == null) { + mHider = new Runnable() { + public void run() { + hide(); + } + }; } + TextView.this.postDelayed(mHider, DELAY_BEFORE_FADE_OUT); } private void removeHiderCallback() { if (mHider != null) { - removeCallbacks(mHider); + TextView.this.removeCallbacks(mHider); } } - public void hide() { - if (mHandle != null) { - mHandle.hide(); + @Override + protected void initDrawable() { + if (mSelectHandleCenter == null) { + mSelectHandleCenter = mContext.getResources().getDrawable( + mTextSelectHandleRes); } - removeHiderCallback(); - removePastePopupCallback(); + mDrawable = mSelectHandleCenter; + mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f; } - private void hideDelayed() { - removeHiderCallback(); - if (mHider == null) { - mHider = new Runnable() { - public void run() { - hide(); + @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(); + mTouchTimer = SystemClock.uptimeMillis(); + break; + + case MotionEvent.ACTION_UP: + long delay = SystemClock.uptimeMillis() - mTouchTimer; + if (delay < ViewConfiguration.getTapTimeout()) { + final float deltaX = mDownPositionX - ev.getRawX(); + final float deltaY = mDownPositionY - ev.getRawY(); + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + if (distanceSquared < mSquaredTouchSlopDistance) { + if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { + // Tapping on the handle dismisses the displayed paste view, + mPastePopupWindow.hide(); + } else { + show(0); + } + } } - }; + hideDelayed(); + break; + + case MotionEvent.ACTION_CANCEL: + hideDelayed(); + break; + + default: + break; } - postDelayed(mHider, DELAY_BEFORE_FADE_OUT); + + return result; } - public boolean isShowing() { - return mHandle != null && mHandle.isShowing(); + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionStart(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, offset); } - public void updatePosition(HandleView handle, int x, int y) { - final int previousOffset = getSelectionStart(); - final int newOffset = getOffset(x, y); + @Override + public void updatePosition(float x, float y) { + updateOffset(getOffsetForPosition(x, y)); + } - if (newOffset != previousOffset) { - updateOffset(handle, newOffset); - removePastePopupCallback(); + void showPastePopupWindow() { + if (mPastePopupWindow == null) { + mPastePopupWindow = new PastePopupWindow(); } - hideDelayed(); + mPastePopupWindow.show(); } - public void updateOffset(HandleView handle, int offset) { - Selection.setSelection((Spannable) mText, offset); - updatePosition(); + @Override + void onHandleMoved() { + removeHiderCallback(); + hidePastePopupWindow(); + } + + void hidePastePopupWindow() { + if (mPastePopupShower != null) { + TextView.this.removeCallbacks(mPastePopupShower); + } + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } + + @Override + public void onDetached() { + removeHiderCallback(); + hidePastePopupWindow(); } + } - public void updatePosition() { - final int offset = getSelectionStart(); + private class SelectionStartHandleView extends HandleView { + @Override + protected void initDrawable() { + if (mSelectHandleLeft == null) { + mSelectHandleLeft = mContext.getResources().getDrawable( + mTextSelectHandleLeftRes); + } + mDrawable = mSelectHandleLeft; + mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f; + } - if (offset < 0) { - // Should never happen, safety check. - Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); - hide(); - return; + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionStart(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, offset, getSelectionEnd()); + } + + @Override + public void updatePosition(float x, float y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffsetForPosition(x, y); + + // No need to redraw when the offset is unchanged + if (offset == selectionStart) return; + // Handles can not cross and selection is at least one character + if (offset >= selectionEnd) offset = selectionEnd - 1; + + Selection.setSelection((Spannable) mText, offset, selectionEnd); + } + } + + private class SelectionEndHandleView extends HandleView { + @Override + protected void initDrawable() { + if (mSelectHandleRight == null) { + mSelectHandleRight = mContext.getResources().getDrawable( + mTextSelectHandleRightRes); } + mDrawable = mSelectHandleRight; + mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f; + } + + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionEnd(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, getSelectionStart(), offset); + } + + @Override + public void updatePosition(float x, float y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffsetForPosition(x, y); + + // No need to redraw when the offset is unchanged + if (offset == selectionEnd) return; + // Handles can not cross and selection is at least one character + if (offset <= selectionStart) offset = selectionStart + 1; + + Selection.setSelection((Spannable) mText, selectionStart, offset); + } + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * It is not used outside of {@link TextView}. + * @hide + */ + private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + public void show(); + + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * @param event The touch event + */ + public boolean onTouchEvent(MotionEvent event); + + /** + * 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(); + } + + private class InsertionPointCursorController implements CursorController { + private static final int DELAY_BEFORE_PASTE = 2000; + + private InsertionHandleView mHandle; + + public void show() { + ((InsertionHandleView) getHandle()).show(DELAY_BEFORE_PASTE); + } - getHandle().positionAtCursor(offset); + public void showWithPaste() { + ((InsertionHandleView) getHandle()).show(0); } - public int getCurrentOffset(HandleView handle) { - return getSelectionStart(); + public void hide() { + if (mHandle != null) { + mHandle.hide(); + } } public boolean onTouchEvent(MotionEvent ev) { @@ -9145,30 +9606,30 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private HandleView getHandle() { if (mHandle == null) { - mHandle = new HandleView(this, HandleView.CENTER); + mHandle = new InsertionHandleView(); } return mHandle; } @Override public void onDetached() { - removeHiderCallback(); - removePastePopupCallback(); + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mHandle != null) mHandle.onDetached(); } } private class SelectionModifierCursorController implements CursorController { - // The cursor controller images, lazily created when shown. - private HandleView mStartHandle, mEndHandle; + // The cursor controller handles, lazily created when shown. + private SelectionStartHandleView mStartHandle; + private SelectionEndHandleView mEndHandle; // The offsets of that last touch down event. Remembered to start selection there. private int mMinTouchOffset, mMaxTouchOffset; - // Whether selection anchors are active - private boolean mIsShowing; // Double tap detection private long mPreviousTapUpTime = 0; - private int mPreviousTapPositionX; - private int mPreviousTapPositionY; + private float mPreviousTapPositionX, mPreviousTapPositionY; SelectionModifierCursorController() { resetTouchOffsets(); @@ -9180,96 +9641,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Lazy object creation has to be done before updatePosition() is called. - if (mStartHandle == null) mStartHandle = new HandleView(this, HandleView.LEFT); - if (mEndHandle == null) mEndHandle = new HandleView(this, HandleView.RIGHT); - - mIsShowing = true; + if (mStartHandle == null) mStartHandle = new SelectionStartHandleView(); + if (mEndHandle == null) mEndHandle = new SelectionEndHandleView(); mStartHandle.show(); mEndHandle.show(); hideInsertionPointCursorController(); + hideSuggestions(); } public void hide() { if (mStartHandle != null) mStartHandle.hide(); if (mEndHandle != null) mEndHandle.hide(); - mIsShowing = false; - } - - public boolean isShowing() { - return mIsShowing; - } - - public void updatePosition(HandleView handle, int x, int y) { - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); - - int offset = getOffset(x, y); - - // Handle the case where start and end are swapped, making sure start <= end - if (handle == mStartHandle) { - if (selectionStart == offset || offset > selectionEnd) { - return; // no change, no need to redraw; - } - // If the user "closes" the selection entirely they were probably trying to - // select a single character. Help them out. - if (offset == selectionEnd) { - offset = selectionEnd - 1; - } - selectionStart = offset; - } else { - if (selectionEnd == offset || offset < selectionStart) { - return; // no change, no need to redraw; - } - // If the user "closes" the selection entirely they were probably trying to - // select a single character. Help them out. - if (offset == selectionStart) { - offset = selectionStart + 1; - } - selectionEnd = offset; - } - - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - updatePosition(); - } - - public void updateOffset(HandleView handle, int offset) { - int start = getSelectionStart(); - int end = getSelectionEnd(); - - if (mStartHandle == handle) { - start = offset; - } else { - end = offset; - } - - Selection.setSelection((Spannable) mText, start, end); - updatePosition(); - } - - public void updatePosition() { - if (!isShowing()) { - return; - } - - final int selectionStart = getSelectionStart(); - final int selectionEnd = getSelectionEnd(); - - if ((selectionStart < 0) || (selectionEnd < 0)) { - // Should never happen, safety check. - Log.w(LOG_TAG, "Update selection controller position called with no cursor"); - hide(); - return; - } - - // The handles have been created since the controller isShowing(). - mStartHandle.positionAtCursor(selectionStart); - mEndHandle.positionAtCursor(selectionEnd); - } - - public int getCurrentOffset(HandleView handle) { - return mStartHandle == handle ? getSelectionStart() : getSelectionEnd(); } public boolean onTouchEvent(MotionEvent event) { @@ -9278,21 +9662,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (isTextEditable() || mTextIsSelectable) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - final int x = (int) event.getX(); - final int y = (int) event.getY(); + final float x = event.getX(); + final float y = event.getY(); // Remember finger down position, to be able to start selection from there - mMinTouchOffset = mMaxTouchOffset = getOffset(x, y); + mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); // Double tap detection long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; if (duration <= ViewConfiguration.getDoubleTapTimeout() && isPositionOnText(x, y)) { - final int deltaX = x - mPreviousTapPositionX; - final int deltaY = y - mPreviousTapPositionY; - final int distanceSquared = deltaX * deltaX + deltaY * deltaY; + final float deltaX = x - mPreviousTapPositionX; + final float deltaY = y - mPreviousTapPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared < mSquaredTouchSlopDistance) { - startSelectionActionMode(); + showSuggestions(); mDiscardNextActionUp = true; } } @@ -9326,9 +9710,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void updateMinAndMaxOffsets(MotionEvent event) { int pointerCount = event.getPointerCount(); for (int index = 0; index < pointerCount; index++) { - final int x = (int) event.getX(index); - final int y = (int) event.getY(index); - int offset = getOffset(x, y); + int offset = getOffsetForPosition(event.getX(index), event.getY(index)); if (offset < mMinTouchOffset) mMinTouchOffset = offset; if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; } @@ -9360,7 +9742,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public void onDetached() {} + public void onDetached() { + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mStartHandle != null) mStartHandle.onDetached(); + if (mEndHandle != null) mEndHandle.onDetached(); + } } private void hideInsertionPointCursorController() { @@ -9376,44 +9764,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void hideControllers() { hideInsertionPointCursorController(); stopSelectionActionMode(); + hideSuggestions(); } /** - * Get the offset character closest to the specified absolute position. + * 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. - * - * @hide */ - public int getOffset(int x, int y) { + 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; } - private int convertToLocalHorizontalCoordinate(int x) { + private float convertToLocalHorizontalCoordinate(float x) { x -= getTotalPaddingLeft(); // Clamp the position to inside of the view. - x = Math.max(0, x); + x = Math.max(0.0f, x); x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); x += getScrollX(); return x; } - private int getLineAtCoordinate(int y) { + private int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. - y = Math.max(0, y); + y = Math.max(0.0f, y); y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); y += getScrollY(); - return getLayout().getLineForVertical(y); + return getLayout().getLineForVertical((int) y); } - private int getOffsetAtCoordinate(int line, int x) { + private int getOffsetAtCoordinate(int line, float x) { x = convertToLocalHorizontalCoordinate(x); return getLayout().getOffsetForHorizontal(line, x); } @@ -9421,7 +9809,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** 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(int x, int y) { + private boolean isPositionOnText(float x, float y) { if (getLayout() == null) return false; final int line = getLineAtCoordinate(y); @@ -9443,7 +9831,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case DragEvent.ACTION_DRAG_LOCATION: - final int offset = getOffset((int) event.getX(), (int) event.getY()); + final int offset = getOffsetForPosition(event.getX(), event.getY()); Selection.setSelection((Spannable)mText, offset); return true; @@ -9467,7 +9855,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener content.append(item.coerceToText(TextView.this.mContext)); } - final int offset = getOffset((int) event.getX(), (int) event.getY()); + final int offset = getOffsetForPosition(event.getX(), event.getY()); Object localState = event.getLocalState(); DragLocalState dragLocalState = null; diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 029d690..423e735 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -409,7 +409,9 @@ public class TimePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + int flags = DateUtils.FORMAT_SHOW_TIME; if (mIs24HourView) { flags |= DateUtils.FORMAT_24HOUR; @@ -421,7 +423,6 @@ public class TimePicker extends FrameLayout { String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } private void updateHourControl() { diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 450c966..9e37c7b 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -33,7 +33,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; @@ -501,7 +501,7 @@ public class ZoomButtonsController implements View.OnTouchListener { } else { - ViewRoot viewRoot = getOwnerViewRoot(); + ViewAncestor viewRoot = getOwnerViewAncestor(); if (viewRoot != null) { viewRoot.dispatchKey(event); } @@ -526,15 +526,15 @@ public class ZoomButtonsController implements View.OnTouchListener { } } - private ViewRoot getOwnerViewRoot() { + private ViewAncestor getOwnerViewAncestor() { View rootViewOfOwner = mOwnerView.getRootView(); if (rootViewOfOwner == null) { return null; } ViewParent parentOfRootView = rootViewOfOwner.getParent(); - if (parentOfRootView instanceof ViewRoot) { - return (ViewRoot) parentOfRootView; + if (parentOfRootView instanceof ViewAncestor) { + return (ViewAncestor) parentOfRootView; } else { return null; } |