summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/widget')
-rw-r--r--core/java/android/widget/AbsListView.java31
-rw-r--r--core/java/android/widget/AbsSeekBar.java3
-rw-r--r--core/java/android/widget/AbsoluteLayout.java5
-rw-r--r--core/java/android/widget/AdapterView.java44
-rw-r--r--core/java/android/widget/AdapterViewAnimator.java66
-rw-r--r--core/java/android/widget/CheckedTextView.java9
-rw-r--r--core/java/android/widget/CompoundButton.java19
-rw-r--r--core/java/android/widget/CursorAdapter.java3
-rw-r--r--core/java/android/widget/CursorTreeAdapter.java3
-rw-r--r--core/java/android/widget/DatePicker.java89
-rw-r--r--core/java/android/widget/ExpandableListView.java27
-rw-r--r--core/java/android/widget/FrameLayout.java26
-rw-r--r--core/java/android/widget/GridLayout.java2262
-rw-r--r--core/java/android/widget/GridView.java27
-rw-r--r--core/java/android/widget/ImageView.java39
-rw-r--r--core/java/android/widget/LinearLayout.java356
-rw-r--r--core/java/android/widget/ListView.java62
-rw-r--r--core/java/android/widget/MultiAutoCompleteTextView.java6
-rw-r--r--core/java/android/widget/PopupWindow.java8
-rw-r--r--core/java/android/widget/ProgressBar.java60
-rw-r--r--core/java/android/widget/RelativeLayout.java40
-rw-r--r--core/java/android/widget/RemoteViews.java106
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java21
-rw-r--r--core/java/android/widget/RemoteViewsService.java71
-rw-r--r--core/java/android/widget/ScrollView.java5
-rw-r--r--core/java/android/widget/SearchView.java5
-rw-r--r--core/java/android/widget/SimpleCursorAdapter.java14
-rw-r--r--core/java/android/widget/Space.java75
-rw-r--r--core/java/android/widget/StackView.java51
-rw-r--r--core/java/android/widget/TabWidget.java15
-rw-r--r--core/java/android/widget/TableRow.java3
-rw-r--r--core/java/android/widget/TextView.java1650
-rw-r--r--core/java/android/widget/TimePicker.java5
-rw-r--r--core/java/android/widget/ZoomButtonsController.java10
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&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
* android.R.layout.simple_dropdown_item_1line, COUNTRIES);
* MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit);
@@ -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;
}