blob: 90fb10683d32197eb03084d55be023fb8e4c1a76 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
Gilles Debunne78996c92010-10-12 16:01:47 -070019import android.R;
Dianne Hackborn1040dc42010-08-26 22:11:06 -070020import android.content.ClipData;
Gilles Debunnef170a342010-11-11 11:08:59 -080021import android.content.ClipData.Item;
Gilles Debunne64e54a62010-09-07 19:07:17 -070022import android.content.ClipboardManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.content.Context;
Gilles Debunnee90bed12011-08-30 14:28:27 -070024import android.content.Intent;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070025import android.content.pm.PackageManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.content.res.ColorStateList;
Christopher Tate1373a8e2011-11-10 19:59:13 -080027import android.content.res.CompatibilityInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.content.res.XmlResourceParser;
31import android.graphics.Canvas;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080032import android.graphics.Color;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.graphics.Paint;
34import android.graphics.Path;
35import android.graphics.Rect;
36import android.graphics.RectF;
37import android.graphics.Typeface;
38import android.graphics.drawable.Drawable;
Gilles Debunne64e54a62010-09-07 19:07:17 -070039import android.inputmethodservice.ExtractEditText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import android.os.Bundle;
41import android.os.Handler;
svetoslavganov75986cf2009-05-14 22:28:01 -070042import android.os.Message;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043import android.os.Parcel;
44import android.os.Parcelable;
45import android.os.SystemClock;
Gilles Debunnee90bed12011-08-30 14:28:27 -070046import android.provider.Settings;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.text.BoringLayout;
48import android.text.DynamicLayout;
49import android.text.Editable;
50import android.text.GetChars;
51import android.text.GraphicsOperations;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052import android.text.InputFilter;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -070053import android.text.InputType;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054import android.text.Layout;
55import android.text.ParcelableSpan;
56import android.text.Selection;
57import android.text.SpanWatcher;
58import android.text.Spannable;
svetoslavganov75986cf2009-05-14 22:28:01 -070059import android.text.SpannableString;
Gilles Debunne214a8622011-04-26 15:44:37 -070060import android.text.SpannableStringBuilder;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import android.text.Spanned;
62import android.text.SpannedString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063import android.text.StaticLayout;
Doug Feltcb3791202011-07-07 11:57:48 -070064import android.text.TextDirectionHeuristic;
65import android.text.TextDirectionHeuristics;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066import android.text.TextPaint;
67import android.text.TextUtils;
Adam Powell282e3772011-08-30 16:51:11 -070068import android.text.TextUtils.TruncateAt;
Gilles Debunne0eea6682011-08-29 13:30:31 -070069import android.text.TextWatcher;
Adam Powell7f8f79a2011-07-07 18:35:54 -070070import android.text.method.AllCapsTransformationMethod;
Gilles Debunne86b9c782010-11-11 10:43:48 -080071import android.text.method.ArrowKeyMovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072import android.text.method.DateKeyListener;
73import android.text.method.DateTimeKeyListener;
74import android.text.method.DialerKeyListener;
75import android.text.method.DigitsKeyListener;
76import android.text.method.KeyListener;
77import android.text.method.LinkMovementMethod;
78import android.text.method.MetaKeyKeyListener;
79import android.text.method.MovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080080import android.text.method.PasswordTransformationMethod;
81import android.text.method.SingleLineTransformationMethod;
82import android.text.method.TextKeyListener;
svetoslavganov75986cf2009-05-14 22:28:01 -070083import android.text.method.TimeKeyListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080084import android.text.method.TransformationMethod;
Adam Powell7f8f79a2011-07-07 18:35:54 -070085import android.text.method.TransformationMethod2;
Gilles Debunne214a8622011-04-26 15:44:37 -070086import android.text.method.WordIterator;
Gilles Debunnef3895ed2010-12-21 12:53:58 -080087import android.text.style.ClickableSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +010088import android.text.style.EasyEditSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080089import android.text.style.ParagraphStyle;
Gilles Debunne6435a562011-08-04 21:22:30 -070090import android.text.style.SpellCheckSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070091import android.text.style.SuggestionRangeSpan;
Gilles Debunne2037b822011-04-22 13:07:33 -070092import android.text.style.SuggestionSpan;
Gilles Debunne214a8622011-04-26 15:44:37 -070093import android.text.style.TextAppearanceSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080094import android.text.style.URLSpan;
95import android.text.style.UpdateAppearance;
96import android.text.util.Linkify;
97import android.util.AttributeSet;
Gilles Debunne69340442011-03-31 13:37:51 -070098import android.util.DisplayMetrics;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080099import android.util.FloatMath;
svetoslavganov75986cf2009-05-14 22:28:01 -0700100import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101import android.util.TypedValue;
Gilles Debunne27113f82010-08-23 12:09:14 -0700102import android.view.ActionMode;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800103import android.view.ActionMode.Callback;
Gilles Debunnef170a342010-11-11 11:08:59 -0800104import android.view.DragEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800105import android.view.Gravity;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700106import android.view.HapticFeedbackConstants;
Jeff Brown6b53e8d2010-11-10 16:03:06 -0800107import android.view.KeyCharacterMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108import android.view.KeyEvent;
109import android.view.LayoutInflater;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700110import android.view.Menu;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111import android.view.MenuItem;
112import android.view.MotionEvent;
113import android.view.View;
Gilles Debunne65f60412010-10-15 16:18:35 -0700114import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115import android.view.ViewDebug;
Adam Powell8c8293b2010-10-12 14:45:12 -0700116import android.view.ViewGroup;
Gilles Debunne27113f82010-08-23 12:09:14 -0700117import android.view.ViewGroup.LayoutParams;
Adam Powellabcbb1a2010-10-04 21:12:19 -0700118import android.view.ViewParent;
Gilles Debunne3784a7f2011-07-15 13:49:38 -0700119import android.view.ViewRootImpl;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800120import android.view.ViewTreeObserver;
Gilles Debunnecbfbb522010-10-07 16:57:31 -0700121import android.view.WindowManager;
svetoslavganov75986cf2009-05-14 22:28:01 -0700122import android.view.accessibility.AccessibilityEvent;
123import android.view.accessibility.AccessibilityManager;
Svetoslav Ganov8643aa02011-04-20 12:12:33 -0700124import android.view.accessibility.AccessibilityNodeInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125import android.view.animation.AnimationUtils;
126import android.view.inputmethod.BaseInputConnection;
127import android.view.inputmethod.CompletionInfo;
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -0800128import android.view.inputmethod.CorrectionInfo;
svetoslavganov75986cf2009-05-14 22:28:01 -0700129import android.view.inputmethod.EditorInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130import android.view.inputmethod.ExtractedText;
131import android.view.inputmethod.ExtractedTextRequest;
132import android.view.inputmethod.InputConnection;
133import android.view.inputmethod.InputMethodManager;
satok05f24702011-11-02 19:29:35 +0900134import android.view.textservice.SpellCheckerSubtype;
135import android.view.textservice.TextServicesManager;
Gilles Debunne0eea6682011-08-29 13:30:31 -0700136import android.widget.AdapterView.OnItemClickListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137import android.widget.RemoteViews.RemoteView;
138
Gilles Debunne22378292011-08-12 10:38:52 -0700139import com.android.internal.util.FastMath;
140import com.android.internal.widget.EditableInputConnection;
141
142import org.xmlpull.v1.XmlPullParserException;
143
Gilles Debunne27113f82010-08-23 12:09:14 -0700144import java.io.IOException;
145import java.lang.ref.WeakReference;
Gilles Debunne214a8622011-04-26 15:44:37 -0700146import java.text.BreakIterator;
Gilles Debunne27113f82010-08-23 12:09:14 -0700147import java.util.ArrayList;
Luca Zanoline3f89c02011-08-01 09:55:17 +0100148import java.util.Arrays;
149import java.util.Comparator;
150import java.util.HashMap;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700151import java.util.Locale;
Gilles Debunne27113f82010-08-23 12:09:14 -0700152
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153/**
154 * Displays text to the user and optionally allows them to edit it. A TextView
155 * is a complete text editor, however the basic class is configured to not
156 * allow editing; see {@link EditText} for a subclass that configures the text
157 * view for editing.
158 *
159 * <p>
160 * <b>XML attributes</b>
161 * <p>
162 * See {@link android.R.styleable#TextView TextView Attributes},
163 * {@link android.R.styleable#View View Attributes}
164 *
165 * @attr ref android.R.styleable#TextView_text
166 * @attr ref android.R.styleable#TextView_bufferType
167 * @attr ref android.R.styleable#TextView_hint
168 * @attr ref android.R.styleable#TextView_textColor
169 * @attr ref android.R.styleable#TextView_textColorHighlight
170 * @attr ref android.R.styleable#TextView_textColorHint
Romain Guyd6a463a2009-05-21 23:10:10 -0700171 * @attr ref android.R.styleable#TextView_textAppearance
172 * @attr ref android.R.styleable#TextView_textColorLink
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173 * @attr ref android.R.styleable#TextView_textSize
174 * @attr ref android.R.styleable#TextView_textScaleX
175 * @attr ref android.R.styleable#TextView_typeface
176 * @attr ref android.R.styleable#TextView_textStyle
177 * @attr ref android.R.styleable#TextView_cursorVisible
178 * @attr ref android.R.styleable#TextView_maxLines
179 * @attr ref android.R.styleable#TextView_maxHeight
180 * @attr ref android.R.styleable#TextView_lines
181 * @attr ref android.R.styleable#TextView_height
182 * @attr ref android.R.styleable#TextView_minLines
183 * @attr ref android.R.styleable#TextView_minHeight
184 * @attr ref android.R.styleable#TextView_maxEms
185 * @attr ref android.R.styleable#TextView_maxWidth
186 * @attr ref android.R.styleable#TextView_ems
187 * @attr ref android.R.styleable#TextView_width
188 * @attr ref android.R.styleable#TextView_minEms
189 * @attr ref android.R.styleable#TextView_minWidth
190 * @attr ref android.R.styleable#TextView_gravity
191 * @attr ref android.R.styleable#TextView_scrollHorizontally
192 * @attr ref android.R.styleable#TextView_password
193 * @attr ref android.R.styleable#TextView_singleLine
194 * @attr ref android.R.styleable#TextView_selectAllOnFocus
195 * @attr ref android.R.styleable#TextView_includeFontPadding
196 * @attr ref android.R.styleable#TextView_maxLength
197 * @attr ref android.R.styleable#TextView_shadowColor
198 * @attr ref android.R.styleable#TextView_shadowDx
199 * @attr ref android.R.styleable#TextView_shadowDy
200 * @attr ref android.R.styleable#TextView_shadowRadius
201 * @attr ref android.R.styleable#TextView_autoLink
202 * @attr ref android.R.styleable#TextView_linksClickable
203 * @attr ref android.R.styleable#TextView_numeric
204 * @attr ref android.R.styleable#TextView_digits
205 * @attr ref android.R.styleable#TextView_phoneNumber
206 * @attr ref android.R.styleable#TextView_inputMethod
207 * @attr ref android.R.styleable#TextView_capitalize
208 * @attr ref android.R.styleable#TextView_autoText
209 * @attr ref android.R.styleable#TextView_editable
Romain Guyd6a463a2009-05-21 23:10:10 -0700210 * @attr ref android.R.styleable#TextView_freezesText
211 * @attr ref android.R.styleable#TextView_ellipsize
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800212 * @attr ref android.R.styleable#TextView_drawableTop
213 * @attr ref android.R.styleable#TextView_drawableBottom
214 * @attr ref android.R.styleable#TextView_drawableRight
215 * @attr ref android.R.styleable#TextView_drawableLeft
Romain Guyd6a463a2009-05-21 23:10:10 -0700216 * @attr ref android.R.styleable#TextView_drawablePadding
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800217 * @attr ref android.R.styleable#TextView_lineSpacingExtra
218 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
219 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
Romain Guyd6a463a2009-05-21 23:10:10 -0700220 * @attr ref android.R.styleable#TextView_inputType
221 * @attr ref android.R.styleable#TextView_imeOptions
222 * @attr ref android.R.styleable#TextView_privateImeOptions
223 * @attr ref android.R.styleable#TextView_imeActionLabel
224 * @attr ref android.R.styleable#TextView_imeActionId
225 * @attr ref android.R.styleable#TextView_editorExtras
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 */
227@RemoteView
228public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700229 static final String LOG_TAG = "TextView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230 static final boolean DEBUG_EXTRACT = false;
Gilles Debunneb7012e842011-02-24 15:40:38 -0800231
Romain Guy8b55f372010-08-18 17:10:07 -0700232 private static final int PRIORITY = 100;
Gilles Debunne3dbf55c2010-12-16 10:31:51 -0800233 private int mCurrentAlpha = 255;
Adam Powell879fb6b2010-09-20 11:23:56 -0700234
Adam Powellabcbb1a2010-10-04 21:12:19 -0700235 final int[] mTempCoords = new int[2];
236 Rect mTempRect;
Adam Powell879fb6b2010-09-20 11:23:56 -0700237
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 private ColorStateList mTextColor;
239 private int mCurTextColor;
240 private ColorStateList mHintTextColor;
241 private ColorStateList mLinkTextColor;
242 private int mCurHintTextColor;
243 private boolean mFreezesText;
244 private boolean mFrozenWithFocus;
245 private boolean mTemporaryDetach;
Romain Guya440b002010-02-24 15:57:54 -0800246 private boolean mDispatchTemporaryDetach;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800247
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800248 private boolean mDiscardNextActionUp = false;
249 private boolean mIgnoreActionUpEvent = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800250
251 private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
252 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
253
254 private float mShadowRadius, mShadowDx, mShadowDy;
255
256 private static final int PREDRAW_NOT_REGISTERED = 0;
257 private static final int PREDRAW_PENDING = 1;
258 private static final int PREDRAW_DONE = 2;
259 private int mPreDrawState = PREDRAW_NOT_REGISTERED;
260
261 private TextUtils.TruncateAt mEllipsize = null;
262
263 // Enum for the "typeface" XML parameter.
264 // TODO: How can we get this from the XML instead of hardcoding it here?
265 private static final int SANS = 1;
266 private static final int SERIF = 2;
267 private static final int MONOSPACE = 3;
268
269 // Bitfield for the "numeric" XML parameter.
270 // TODO: How can we get this from the XML instead of hardcoding it here?
271 private static final int SIGNED = 2;
272 private static final int DECIMAL = 4;
273
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700274 static class Drawables {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275 final Rect mCompoundRect = new Rect();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700276 Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight,
277 mDrawableStart, mDrawableEnd;
278 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
279 mDrawableSizeStart, mDrawableSizeEnd;
280 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
281 mDrawableHeightStart, mDrawableHeightEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 int mDrawablePadding;
283 }
284 private Drawables mDrawables;
285
286 private CharSequence mError;
287 private boolean mErrorWasChanged;
The Android Open Source Project10592532009-03-18 17:39:46 -0700288 private ErrorPopup mPopup;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 /**
290 * This flag is set if the TextView tries to display an error before it
291 * is attached to the window (so its position is still unknown).
292 * It causes the error to be shown later, when onAttachedToWindow()
293 * is called.
294 */
295 private boolean mShowErrorAfterAttach;
296
297 private CharWrapper mCharWrapper = null;
298
299 private boolean mSelectionMoved = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700300 private boolean mTouchFocusSelected = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800301
302 private Marquee mMarquee;
303 private boolean mRestartMarquee;
304
305 private int mMarqueeRepeatLimit = 3;
306
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700307 static class InputContentType {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700308 int imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800309 String privateImeOptions;
310 CharSequence imeActionLabel;
311 int imeActionId;
312 Bundle extras;
313 OnEditorActionListener onEditorActionListener;
314 boolean enterDown;
315 }
316 InputContentType mInputContentType;
317
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700318 static class InputMethodState {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 Rect mCursorRectInWindow = new Rect();
320 RectF mTmpRectF = new RectF();
321 float[] mTmpOffset = new float[2];
322 ExtractedTextRequest mExtracting;
323 final ExtractedText mTmpExtracted = new ExtractedText();
324 int mBatchEditNesting;
325 boolean mCursorChanged;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700326 boolean mSelectionModeChanged;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 boolean mContentChanged;
328 int mChangedStart, mChangedEnd, mChangedDelta;
329 }
330 InputMethodState mInputMethodState;
331
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800332 private int mTextSelectHandleLeftRes;
333 private int mTextSelectHandleRightRes;
334 private int mTextSelectHandleRes;
Adam Powellfbb3b472010-10-06 21:04:35 -0700335
Gilles Debunne69340442011-03-31 13:37:51 -0700336 private int mTextEditSuggestionItemLayout;
337 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunne214a8622011-04-26 15:44:37 -0700338 private SuggestionRangeSpan mSuggestionRangeSpan;
Gilles Debunne69340442011-03-31 13:37:51 -0700339
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800340 private int mCursorDrawableRes;
341 private final Drawable[] mCursorDrawable = new Drawable[2];
342 private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
343
344 private Drawable mSelectHandleLeft;
345 private Drawable mSelectHandleRight;
346 private Drawable mSelectHandleCenter;
Adam Powellb08013c2010-09-16 16:28:11 -0700347
Gilles Debunne21078e42011-08-02 10:22:35 -0700348 // Global listener that detects changes in the global position of the TextView
349 private PositionListener mPositionListener;
350
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700351 private float mLastDownPositionX, mLastDownPositionY;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800352 private Callback mCustomSelectionActionModeCallback;
Gilles Debunne9948ad72010-11-24 14:00:46 -0800353
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800354 private final int mSquaredTouchSlopDistance;
Gilles Debunnec01f3fe2010-12-22 17:07:36 -0800355 // Set when this TextView gained focus with some text selected. Will start selection mode.
356 private boolean mCreatedWithASelection = false;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800357
Gilles Debunnec59269f2011-04-22 11:46:09 -0700358 private WordIterator mWordIterator;
359
Gilles Debunne6435a562011-08-04 21:22:30 -0700360 private SpellChecker mSpellChecker;
361
Gilles Debunne0f4109e2011-10-19 11:26:21 -0700362 private boolean mSoftInputShownOnFocus = true;
Gilles Debunne550efbf2011-10-10 16:49:02 -0700363
Doug Feltc0ccf0c2011-06-23 16:13:18 -0700364 // The alignment to pass to Layout, or null if not resolved.
365 private Layout.Alignment mLayoutAlignment;
366
367 // The default value for mTextAlign.
368 private TextAlign mTextAlign = TextAlign.INHERIT;
369
370 private static enum TextAlign {
371 INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END;
372 }
373
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -0700374 private boolean mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700375
Adam Powell282e3772011-08-30 16:51:11 -0700376 /**
377 * On some devices the fading edges add a performance penalty if used
378 * extensively in the same layout. This mode indicates how the marquee
379 * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
380 */
381 private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
382
383 /**
384 * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
385 * the layout that should be used when the mode switches.
386 */
387 private Layout mSavedMarqueeModeLayout;
388
389 /**
390 * Draw marquee text with fading edges as usual
391 */
392 private static final int MARQUEE_FADE_NORMAL = 0;
393
394 /**
395 * Draw marquee text as ellipsize end while inactive instead of with the fade.
396 * (Useful for devices where the fade can be expensive if overdone)
397 */
398 private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
399
400 /**
401 * Draw marquee text with fading edges because it is currently active/animating.
402 */
403 private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
404
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800405 /*
406 * Kick-start the font cache for the zygote process (to pay the cost of
407 * initializing freetype for our default font only once).
408 */
409 static {
410 Paint p = new Paint();
411 p.setAntiAlias(true);
412 // We don't care about the result, just the side-effect of measuring.
413 p.measureText("H");
414 }
415
416 /**
417 * Interface definition for a callback to be invoked when an action is
418 * performed on the editor.
419 */
420 public interface OnEditorActionListener {
421 /**
422 * Called when an action is being performed.
423 *
424 * @param v The view that was clicked.
425 * @param actionId Identifier of the action. This will be either the
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700426 * identifier you supplied, or {@link EditorInfo#IME_NULL
427 * EditorInfo.IME_NULL} if being called due to the enter key
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800428 * being pressed.
429 * @param event If triggered by an enter key, this is the event;
430 * otherwise, this is null.
431 * @return Return true if you have consumed the action, else false.
432 */
433 boolean onEditorAction(TextView v, int actionId, KeyEvent event);
434 }
Gilles Debunne21078e42011-08-02 10:22:35 -0700435
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800436 public TextView(Context context) {
437 this(context, null);
438 }
439
440 public TextView(Context context,
441 AttributeSet attrs) {
442 this(context, attrs, com.android.internal.R.attr.textViewStyle);
443 }
444
Gilles Debunnee15b3582010-06-16 15:17:21 -0700445 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800446 public TextView(Context context,
447 AttributeSet attrs,
448 int defStyle) {
449 super(context, attrs, defStyle);
450 mText = "";
451
Christopher Tate1373a8e2011-11-10 19:59:13 -0800452 final Resources res = getResources();
453 final CompatibilityInfo compat = res.getCompatibilityInfo();
454
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800455 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Christopher Tate1373a8e2011-11-10 19:59:13 -0800456 mTextPaint.density = res.getDisplayMetrics().density;
457 mTextPaint.setCompatibilityScaling(compat.applicationScale);
Gilles Debunne8cbb4c62011-01-24 12:33:56 -0800458
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800459 // If we get the paint from the skin, we should set it to left, since
460 // the layout always wants it to be left.
461 // mTextPaint.setTextAlign(Paint.Align.LEFT);
462
463 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Christopher Tate1373a8e2011-11-10 19:59:13 -0800464 mHighlightPaint.setCompatibilityScaling(compat.applicationScale);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800465
466 mMovement = getDefaultMovementMethod();
467 mTransformation = null;
468
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800469 int textColorHighlight = 0;
470 ColorStateList textColor = null;
471 ColorStateList textColorHint = null;
472 ColorStateList textColorLink = null;
473 int textSize = 15;
474 int typefaceIndex = -1;
475 int styleIndex = -1;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700476 boolean allCaps = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800477
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700478 final Resources.Theme theme = context.getTheme();
479
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480 /*
481 * Look the appearance up without checking first if it exists because
482 * almost every TextView has one and it greatly simplifies the logic
483 * to be able to parse the appearance first and then let specific tags
484 * for this View override it.
485 */
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700486 TypedArray a = theme.obtainStyledAttributes(
487 attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 TypedArray appearance = null;
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700489 int ap = a.getResourceId(
490 com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
491 a.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800492 if (ap != -1) {
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700493 appearance = theme.obtainStyledAttributes(
494 ap, com.android.internal.R.styleable.TextAppearance);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800495 }
496 if (appearance != null) {
497 int n = appearance.getIndexCount();
498 for (int i = 0; i < n; i++) {
499 int attr = appearance.getIndex(i);
500
501 switch (attr) {
502 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
503 textColorHighlight = appearance.getColor(attr, textColorHighlight);
504 break;
505
506 case com.android.internal.R.styleable.TextAppearance_textColor:
507 textColor = appearance.getColorStateList(attr);
508 break;
509
510 case com.android.internal.R.styleable.TextAppearance_textColorHint:
511 textColorHint = appearance.getColorStateList(attr);
512 break;
513
514 case com.android.internal.R.styleable.TextAppearance_textColorLink:
515 textColorLink = appearance.getColorStateList(attr);
516 break;
517
518 case com.android.internal.R.styleable.TextAppearance_textSize:
519 textSize = appearance.getDimensionPixelSize(attr, textSize);
520 break;
521
522 case com.android.internal.R.styleable.TextAppearance_typeface:
523 typefaceIndex = appearance.getInt(attr, -1);
524 break;
525
526 case com.android.internal.R.styleable.TextAppearance_textStyle:
527 styleIndex = appearance.getInt(attr, -1);
528 break;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700529
530 case com.android.internal.R.styleable.TextAppearance_textAllCaps:
531 allCaps = appearance.getBoolean(attr, false);
532 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800533 }
534 }
535
536 appearance.recycle();
537 }
538
539 boolean editable = getDefaultEditable();
540 CharSequence inputMethod = null;
541 int numeric = 0;
542 CharSequence digits = null;
543 boolean phone = false;
544 boolean autotext = false;
545 int autocap = -1;
546 int buffertype = 0;
547 boolean selectallonfocus = false;
548 Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700549 drawableBottom = null, drawableStart = null, drawableEnd = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800550 int drawablePadding = 0;
551 int ellipsize = -1;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700552 boolean singleLine = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800553 int maxlength = -1;
554 CharSequence text = "";
Romain Guy4dc4f732009-06-19 15:16:40 -0700555 CharSequence hint = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800556 int shadowcolor = 0;
557 float dx = 0, dy = 0, r = 0;
558 boolean password = false;
559 int inputType = EditorInfo.TYPE_NULL;
560
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700561 a = theme.obtainStyledAttributes(
562 attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
563
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800564 int n = a.getIndexCount();
565 for (int i = 0; i < n; i++) {
566 int attr = a.getIndex(i);
567
568 switch (attr) {
569 case com.android.internal.R.styleable.TextView_editable:
570 editable = a.getBoolean(attr, editable);
571 break;
572
573 case com.android.internal.R.styleable.TextView_inputMethod:
574 inputMethod = a.getText(attr);
575 break;
576
577 case com.android.internal.R.styleable.TextView_numeric:
578 numeric = a.getInt(attr, numeric);
579 break;
580
581 case com.android.internal.R.styleable.TextView_digits:
582 digits = a.getText(attr);
583 break;
584
585 case com.android.internal.R.styleable.TextView_phoneNumber:
586 phone = a.getBoolean(attr, phone);
587 break;
588
589 case com.android.internal.R.styleable.TextView_autoText:
590 autotext = a.getBoolean(attr, autotext);
591 break;
592
593 case com.android.internal.R.styleable.TextView_capitalize:
594 autocap = a.getInt(attr, autocap);
595 break;
596
597 case com.android.internal.R.styleable.TextView_bufferType:
598 buffertype = a.getInt(attr, buffertype);
599 break;
600
601 case com.android.internal.R.styleable.TextView_selectAllOnFocus:
602 selectallonfocus = a.getBoolean(attr, selectallonfocus);
603 break;
604
605 case com.android.internal.R.styleable.TextView_autoLink:
606 mAutoLinkMask = a.getInt(attr, 0);
607 break;
608
609 case com.android.internal.R.styleable.TextView_linksClickable:
610 mLinksClickable = a.getBoolean(attr, true);
611 break;
612
Gilles Debunne550efbf2011-10-10 16:49:02 -0700613// TODO uncomment when this attribute is made public in the next release
614// also add TextView_showSoftInputOnFocus to the list of attributes above
615// case com.android.internal.R.styleable.TextView_showSoftInputOnFocus:
616// setShowSoftInputOnFocus(a.getBoolean(attr, true));
617// break;
618
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800619 case com.android.internal.R.styleable.TextView_drawableLeft:
620 drawableLeft = a.getDrawable(attr);
621 break;
622
623 case com.android.internal.R.styleable.TextView_drawableTop:
624 drawableTop = a.getDrawable(attr);
625 break;
626
627 case com.android.internal.R.styleable.TextView_drawableRight:
628 drawableRight = a.getDrawable(attr);
629 break;
630
631 case com.android.internal.R.styleable.TextView_drawableBottom:
632 drawableBottom = a.getDrawable(attr);
633 break;
634
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700635 case com.android.internal.R.styleable.TextView_drawableStart:
636 drawableStart = a.getDrawable(attr);
637 break;
638
639 case com.android.internal.R.styleable.TextView_drawableEnd:
640 drawableEnd = a.getDrawable(attr);
641 break;
642
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800643 case com.android.internal.R.styleable.TextView_drawablePadding:
644 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
645 break;
646
647 case com.android.internal.R.styleable.TextView_maxLines:
648 setMaxLines(a.getInt(attr, -1));
649 break;
650
651 case com.android.internal.R.styleable.TextView_maxHeight:
652 setMaxHeight(a.getDimensionPixelSize(attr, -1));
653 break;
654
655 case com.android.internal.R.styleable.TextView_lines:
656 setLines(a.getInt(attr, -1));
657 break;
658
659 case com.android.internal.R.styleable.TextView_height:
660 setHeight(a.getDimensionPixelSize(attr, -1));
661 break;
662
663 case com.android.internal.R.styleable.TextView_minLines:
664 setMinLines(a.getInt(attr, -1));
665 break;
666
667 case com.android.internal.R.styleable.TextView_minHeight:
668 setMinHeight(a.getDimensionPixelSize(attr, -1));
669 break;
670
671 case com.android.internal.R.styleable.TextView_maxEms:
672 setMaxEms(a.getInt(attr, -1));
673 break;
674
675 case com.android.internal.R.styleable.TextView_maxWidth:
676 setMaxWidth(a.getDimensionPixelSize(attr, -1));
677 break;
678
679 case com.android.internal.R.styleable.TextView_ems:
680 setEms(a.getInt(attr, -1));
681 break;
682
683 case com.android.internal.R.styleable.TextView_width:
684 setWidth(a.getDimensionPixelSize(attr, -1));
685 break;
686
687 case com.android.internal.R.styleable.TextView_minEms:
688 setMinEms(a.getInt(attr, -1));
689 break;
690
691 case com.android.internal.R.styleable.TextView_minWidth:
692 setMinWidth(a.getDimensionPixelSize(attr, -1));
693 break;
694
695 case com.android.internal.R.styleable.TextView_gravity:
696 setGravity(a.getInt(attr, -1));
697 break;
698
699 case com.android.internal.R.styleable.TextView_hint:
Romain Guy4dc4f732009-06-19 15:16:40 -0700700 hint = a.getText(attr);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800701 break;
702
703 case com.android.internal.R.styleable.TextView_text:
704 text = a.getText(attr);
705 break;
706
707 case com.android.internal.R.styleable.TextView_scrollHorizontally:
708 if (a.getBoolean(attr, false)) {
709 setHorizontallyScrolling(true);
710 }
711 break;
712
713 case com.android.internal.R.styleable.TextView_singleLine:
714 singleLine = a.getBoolean(attr, singleLine);
715 break;
716
717 case com.android.internal.R.styleable.TextView_ellipsize:
718 ellipsize = a.getInt(attr, ellipsize);
719 break;
720
721 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
722 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
723 break;
724
725 case com.android.internal.R.styleable.TextView_includeFontPadding:
726 if (!a.getBoolean(attr, true)) {
727 setIncludeFontPadding(false);
728 }
729 break;
730
731 case com.android.internal.R.styleable.TextView_cursorVisible:
732 if (!a.getBoolean(attr, true)) {
733 setCursorVisible(false);
734 }
735 break;
736
737 case com.android.internal.R.styleable.TextView_maxLength:
738 maxlength = a.getInt(attr, -1);
739 break;
740
741 case com.android.internal.R.styleable.TextView_textScaleX:
742 setTextScaleX(a.getFloat(attr, 1.0f));
743 break;
744
745 case com.android.internal.R.styleable.TextView_freezesText:
746 mFreezesText = a.getBoolean(attr, false);
747 break;
748
749 case com.android.internal.R.styleable.TextView_shadowColor:
750 shadowcolor = a.getInt(attr, 0);
751 break;
752
753 case com.android.internal.R.styleable.TextView_shadowDx:
754 dx = a.getFloat(attr, 0);
755 break;
756
757 case com.android.internal.R.styleable.TextView_shadowDy:
758 dy = a.getFloat(attr, 0);
759 break;
760
761 case com.android.internal.R.styleable.TextView_shadowRadius:
762 r = a.getFloat(attr, 0);
763 break;
764
765 case com.android.internal.R.styleable.TextView_enabled:
766 setEnabled(a.getBoolean(attr, isEnabled()));
767 break;
768
769 case com.android.internal.R.styleable.TextView_textColorHighlight:
770 textColorHighlight = a.getColor(attr, textColorHighlight);
771 break;
772
773 case com.android.internal.R.styleable.TextView_textColor:
774 textColor = a.getColorStateList(attr);
775 break;
776
777 case com.android.internal.R.styleable.TextView_textColorHint:
778 textColorHint = a.getColorStateList(attr);
779 break;
780
781 case com.android.internal.R.styleable.TextView_textColorLink:
782 textColorLink = a.getColorStateList(attr);
783 break;
784
785 case com.android.internal.R.styleable.TextView_textSize:
786 textSize = a.getDimensionPixelSize(attr, textSize);
787 break;
788
789 case com.android.internal.R.styleable.TextView_typeface:
790 typefaceIndex = a.getInt(attr, typefaceIndex);
791 break;
792
793 case com.android.internal.R.styleable.TextView_textStyle:
794 styleIndex = a.getInt(attr, styleIndex);
795 break;
796
797 case com.android.internal.R.styleable.TextView_password:
798 password = a.getBoolean(attr, password);
799 break;
800
801 case com.android.internal.R.styleable.TextView_lineSpacingExtra:
802 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
803 break;
804
805 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
806 mSpacingMult = a.getFloat(attr, mSpacingMult);
807 break;
808
809 case com.android.internal.R.styleable.TextView_inputType:
810 inputType = a.getInt(attr, mInputType);
811 break;
812
813 case com.android.internal.R.styleable.TextView_imeOptions:
814 if (mInputContentType == null) {
815 mInputContentType = new InputContentType();
816 }
817 mInputContentType.imeOptions = a.getInt(attr,
818 mInputContentType.imeOptions);
819 break;
820
821 case com.android.internal.R.styleable.TextView_imeActionLabel:
822 if (mInputContentType == null) {
823 mInputContentType = new InputContentType();
824 }
825 mInputContentType.imeActionLabel = a.getText(attr);
826 break;
827
828 case com.android.internal.R.styleable.TextView_imeActionId:
829 if (mInputContentType == null) {
830 mInputContentType = new InputContentType();
831 }
832 mInputContentType.imeActionId = a.getInt(attr,
833 mInputContentType.imeActionId);
834 break;
835
836 case com.android.internal.R.styleable.TextView_privateImeOptions:
837 setPrivateImeOptions(a.getString(attr));
838 break;
839
840 case com.android.internal.R.styleable.TextView_editorExtras:
841 try {
842 setInputExtras(a.getResourceId(attr, 0));
843 } catch (XmlPullParserException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700844 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800845 } catch (IOException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700846 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800847 }
848 break;
Adam Powellb08013c2010-09-16 16:28:11 -0700849
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800850 case com.android.internal.R.styleable.TextView_textCursorDrawable:
851 mCursorDrawableRes = a.getResourceId(attr, 0);
852 break;
853
Adam Powellb08013c2010-09-16 16:28:11 -0700854 case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
855 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
856 break;
857
858 case com.android.internal.R.styleable.TextView_textSelectHandleRight:
859 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
860 break;
861
862 case com.android.internal.R.styleable.TextView_textSelectHandle:
863 mTextSelectHandleRes = a.getResourceId(attr, 0);
864 break;
Gilles Debunne7b9652b2010-10-26 16:27:12 -0700865
Gilles Debunne69340442011-03-31 13:37:51 -0700866 case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
867 mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
868 break;
869
Gilles Debunne86b9c782010-11-11 10:43:48 -0800870 case com.android.internal.R.styleable.TextView_textIsSelectable:
871 mTextIsSelectable = a.getBoolean(attr, false);
872 break;
Gilles Debunnef3a135b2011-05-23 16:28:47 -0700873
Adam Powell7f8f79a2011-07-07 18:35:54 -0700874 case com.android.internal.R.styleable.TextView_textAllCaps:
875 allCaps = a.getBoolean(attr, false);
876 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800877 }
878 }
879 a.recycle();
880
881 BufferType bufferType = BufferType.EDITABLE;
882
Gilles Debunned7483bf2010-11-10 10:47:45 -0800883 final int variation =
884 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
885 final boolean passwordInputType = variation
886 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
887 final boolean webPasswordInputType = variation
888 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
Ken Wakasa82d731a2010-12-24 23:42:41 +0900889 final boolean numberPasswordInputType = variation
890 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Gilles Debunned7483bf2010-11-10 10:47:45 -0800891
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800892 if (inputMethod != null) {
Gilles Debunnee15b3582010-06-16 15:17:21 -0700893 Class<?> c;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800894
895 try {
896 c = Class.forName(inputMethod.toString());
897 } catch (ClassNotFoundException ex) {
898 throw new RuntimeException(ex);
899 }
900
901 try {
902 mInput = (KeyListener) c.newInstance();
903 } catch (InstantiationException ex) {
904 throw new RuntimeException(ex);
905 } catch (IllegalAccessException ex) {
906 throw new RuntimeException(ex);
907 }
908 try {
909 mInputType = inputType != EditorInfo.TYPE_NULL
910 ? inputType
911 : mInput.getInputType();
912 } catch (IncompatibleClassChangeError e) {
913 mInputType = EditorInfo.TYPE_CLASS_TEXT;
914 }
915 } else if (digits != null) {
916 mInput = DigitsKeyListener.getInstance(digits.toString());
Dianne Hackborn7ed6ee52009-09-10 18:41:28 -0700917 // If no input type was specified, we will default to generic
918 // text, since we can't tell the IME about the set of digits
919 // that was selected.
920 mInputType = inputType != EditorInfo.TYPE_NULL
921 ? inputType : EditorInfo.TYPE_CLASS_TEXT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800922 } else if (inputType != EditorInfo.TYPE_NULL) {
923 setInputType(inputType, true);
Gilles Debunne91a08cf2010-11-08 17:34:49 -0800924 // If set, the input type overrides what was set using the deprecated singleLine flag.
925 singleLine = !isMultilineInputType(inputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800926 } else if (phone) {
927 mInput = DialerKeyListener.getInstance();
Dianne Hackborn49a1a9b52009-03-24 20:06:11 -0700928 mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800929 } else if (numeric != 0) {
930 mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
931 (numeric & DECIMAL) != 0);
932 inputType = EditorInfo.TYPE_CLASS_NUMBER;
933 if ((numeric & SIGNED) != 0) {
934 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
935 }
936 if ((numeric & DECIMAL) != 0) {
937 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
938 }
939 mInputType = inputType;
940 } else if (autotext || autocap != -1) {
941 TextKeyListener.Capitalize cap;
942
943 inputType = EditorInfo.TYPE_CLASS_TEXT;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700944
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800945 switch (autocap) {
946 case 1:
947 cap = TextKeyListener.Capitalize.SENTENCES;
948 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
949 break;
950
951 case 2:
952 cap = TextKeyListener.Capitalize.WORDS;
953 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
954 break;
955
956 case 3:
957 cap = TextKeyListener.Capitalize.CHARACTERS;
958 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
959 break;
960
961 default:
962 cap = TextKeyListener.Capitalize.NONE;
963 break;
964 }
965
966 mInput = TextKeyListener.getInstance(autotext, cap);
967 mInputType = inputType;
Gilles Debunne86b9c782010-11-11 10:43:48 -0800968 } else if (mTextIsSelectable) {
969 // Prevent text changes from keyboard.
970 mInputType = EditorInfo.TYPE_NULL;
971 mInput = null;
972 bufferType = BufferType.SPANNABLE;
Gilles Debunnecbcb3452010-12-17 15:31:02 -0800973 // Required to request focus while in touch mode.
974 setFocusableInTouchMode(true);
Gilles Debunne86b9c782010-11-11 10:43:48 -0800975 // So that selection can be changed using arrow keys and touch is handled.
976 setMovementMethod(ArrowKeyMovementMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800977 } else if (editable) {
978 mInput = TextKeyListener.getInstance();
979 mInputType = EditorInfo.TYPE_CLASS_TEXT;
980 } else {
981 mInput = null;
982
983 switch (buffertype) {
984 case 0:
985 bufferType = BufferType.NORMAL;
986 break;
987 case 1:
988 bufferType = BufferType.SPANNABLE;
989 break;
990 case 2:
991 bufferType = BufferType.EDITABLE;
992 break;
993 }
994 }
995
Gilles Debunned7483bf2010-11-10 10:47:45 -0800996 // mInputType has been set from inputType, possibly modified by mInputMethod.
997 // Specialize mInputType to [web]password if we have a text class and the original input
998 // type was a password.
999 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
1000 if (password || passwordInputType) {
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -04001001 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
Gilles Debunned7483bf2010-11-10 10:47:45 -08001002 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -04001003 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001004 if (webPasswordInputType) {
1005 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1006 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
1007 }
Ken Wakasa82d731a2010-12-24 23:42:41 +09001008 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
1009 if (numberPasswordInputType) {
1010 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1011 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
1012 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013 }
1014
1015 if (selectallonfocus) {
1016 mSelectAllOnFocus = true;
1017
1018 if (bufferType == BufferType.NORMAL)
1019 bufferType = BufferType.SPANNABLE;
1020 }
1021
1022 setCompoundDrawablesWithIntrinsicBounds(
1023 drawableLeft, drawableTop, drawableRight, drawableBottom);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001024 setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001025 setCompoundDrawablePadding(drawablePadding);
1026
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08001027 // Same as setSingleLine(), but make sure the transformation method and the maximum number
Gilles Debunne066460f2010-12-15 17:31:51 -08001028 // of lines of height are unchanged for multi-line TextViews.
Gilles Debunned7483bf2010-11-10 10:47:45 -08001029 setInputTypeSingleLine(singleLine);
Gilles Debunne066460f2010-12-15 17:31:51 -08001030 applySingleLine(singleLine, singleLine, singleLine);
Gilles Debunned7483bf2010-11-10 10:47:45 -08001031
Gilles Debunne91a08cf2010-11-08 17:34:49 -08001032 if (singleLine && mInput == null && ellipsize < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001033 ellipsize = 3; // END
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001034 }
1035
1036 switch (ellipsize) {
1037 case 1:
1038 setEllipsize(TextUtils.TruncateAt.START);
1039 break;
1040 case 2:
1041 setEllipsize(TextUtils.TruncateAt.MIDDLE);
1042 break;
1043 case 3:
1044 setEllipsize(TextUtils.TruncateAt.END);
1045 break;
1046 case 4:
Adam Powell282e3772011-08-30 16:51:11 -07001047 if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
1048 setHorizontalFadingEdgeEnabled(true);
1049 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
1050 } else {
1051 setHorizontalFadingEdgeEnabled(false);
1052 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
1053 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001054 setEllipsize(TextUtils.TruncateAt.MARQUEE);
1055 break;
1056 }
1057
1058 setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
1059 setHintTextColor(textColorHint);
1060 setLinkTextColor(textColorLink);
1061 if (textColorHighlight != 0) {
1062 setHighlightColor(textColorHighlight);
1063 }
1064 setRawTextSize(textSize);
1065
Adam Powell7f8f79a2011-07-07 18:35:54 -07001066 if (allCaps) {
1067 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
1068 }
1069
Ken Wakasa82d731a2010-12-24 23:42:41 +09001070 if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001071 setTransformationMethod(PasswordTransformationMethod.getInstance());
1072 typefaceIndex = MONOSPACE;
Gilles Debunned7483bf2010-11-10 10:47:45 -08001073 } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1074 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08001075 typefaceIndex = MONOSPACE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001076 }
1077
1078 setTypefaceByIndex(typefaceIndex, styleIndex);
1079
1080 if (shadowcolor != 0) {
1081 setShadowLayer(r, dx, dy, shadowcolor);
1082 }
1083
1084 if (maxlength >= 0) {
1085 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1086 } else {
1087 setFilters(NO_FILTERS);
1088 }
1089
1090 setText(text, bufferType);
Romain Guy4dc4f732009-06-19 15:16:40 -07001091 if (hint != null) setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001092
1093 /*
1094 * Views are not normally focusable unless specified to be.
1095 * However, TextViews that have input or movement methods *are*
1096 * focusable by default.
1097 */
1098 a = context.obtainStyledAttributes(attrs,
1099 com.android.internal.R.styleable.View,
1100 defStyle, 0);
1101
1102 boolean focusable = mMovement != null || mInput != null;
1103 boolean clickable = focusable;
1104 boolean longClickable = focusable;
1105
1106 n = a.getIndexCount();
1107 for (int i = 0; i < n; i++) {
1108 int attr = a.getIndex(i);
1109
1110 switch (attr) {
1111 case com.android.internal.R.styleable.View_focusable:
1112 focusable = a.getBoolean(attr, focusable);
1113 break;
1114
1115 case com.android.internal.R.styleable.View_clickable:
1116 clickable = a.getBoolean(attr, clickable);
1117 break;
1118
1119 case com.android.internal.R.styleable.View_longClickable:
1120 longClickable = a.getBoolean(attr, longClickable);
1121 break;
1122 }
1123 }
1124 a.recycle();
1125
1126 setFocusable(focusable);
1127 setClickable(clickable);
1128 setLongClickable(longClickable);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07001129
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001130 prepareCursorControllers();
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08001131
1132 final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1133 final int touchSlop = viewConfiguration.getScaledTouchSlop();
1134 mSquaredTouchSlopDistance = touchSlop * touchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001135 }
1136
1137 private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1138 Typeface tf = null;
1139 switch (typefaceIndex) {
1140 case SANS:
1141 tf = Typeface.SANS_SERIF;
1142 break;
1143
1144 case SERIF:
1145 tf = Typeface.SERIF;
1146 break;
1147
1148 case MONOSPACE:
1149 tf = Typeface.MONOSPACE;
1150 break;
1151 }
1152
1153 setTypeface(tf, styleIndex);
1154 }
1155
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001156 private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
1157 boolean hasRelativeDrawables = (start != null) || (end != null);
1158 if (hasRelativeDrawables) {
1159 Drawables dr = mDrawables;
1160 if (dr == null) {
1161 mDrawables = dr = new Drawables();
1162 }
1163 final Rect compoundRect = dr.mCompoundRect;
1164 int[] state = getDrawableState();
1165 if (start != null) {
1166 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1167 start.setState(state);
1168 start.copyBounds(compoundRect);
1169 start.setCallback(this);
1170
1171 dr.mDrawableStart = start;
1172 dr.mDrawableSizeStart = compoundRect.width();
1173 dr.mDrawableHeightStart = compoundRect.height();
1174 } else {
1175 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1176 }
1177 if (end != null) {
1178 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1179 end.setState(state);
1180 end.copyBounds(compoundRect);
1181 end.setCallback(this);
1182
1183 dr.mDrawableEnd = end;
1184 dr.mDrawableSizeEnd = compoundRect.width();
1185 dr.mDrawableHeightEnd = compoundRect.height();
1186 } else {
1187 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1188 }
1189 }
1190 }
1191
Janos Levai042856c2010-10-15 02:53:58 +03001192 @Override
1193 public void setEnabled(boolean enabled) {
1194 if (enabled == isEnabled()) {
1195 return;
1196 }
1197
1198 if (!enabled) {
1199 // Hide the soft input if the currently active TextView is disabled
1200 InputMethodManager imm = InputMethodManager.peekInstance();
1201 if (imm != null && imm.isActive(this)) {
1202 imm.hideSoftInputFromWindow(getWindowToken(), 0);
1203 }
1204 }
1205 super.setEnabled(enabled);
Gilles Debunnebb588da2011-07-11 18:26:19 -07001206 prepareCursorControllers();
Dianne Hackbornbc823852011-09-18 17:19:50 -07001207 if (enabled) {
1208 // Make sure IME is updated with current editor info.
1209 InputMethodManager imm = InputMethodManager.peekInstance();
1210 if (imm != null) imm.restartInput(this);
1211 }
Mark Wagnerf8185112011-10-25 16:33:41 -07001212
1213 // start or stop the cursor blinking as appropriate
1214 makeBlink();
Janos Levai042856c2010-10-15 02:53:58 +03001215 }
1216
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001217 /**
1218 * Sets the typeface and style in which the text should be displayed,
1219 * and turns on the fake bold and italic bits in the Paint if the
1220 * Typeface that you provided does not have all the bits in the
1221 * style that you specified.
1222 *
1223 * @attr ref android.R.styleable#TextView_typeface
1224 * @attr ref android.R.styleable#TextView_textStyle
1225 */
1226 public void setTypeface(Typeface tf, int style) {
1227 if (style > 0) {
1228 if (tf == null) {
1229 tf = Typeface.defaultFromStyle(style);
1230 } else {
1231 tf = Typeface.create(tf, style);
1232 }
1233
1234 setTypeface(tf);
1235 // now compute what (if any) algorithmic styling is needed
1236 int typefaceStyle = tf != null ? tf.getStyle() : 0;
1237 int need = style & ~typefaceStyle;
1238 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1239 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1240 } else {
1241 mTextPaint.setFakeBoldText(false);
1242 mTextPaint.setTextSkewX(0);
1243 setTypeface(tf);
1244 }
1245 }
1246
1247 /**
1248 * Subclasses override this to specify that they have a KeyListener
1249 * by default even if not specifically called for in the XML options.
1250 */
1251 protected boolean getDefaultEditable() {
1252 return false;
1253 }
1254
1255 /**
1256 * Subclasses override this to specify a default movement method.
1257 */
1258 protected MovementMethod getDefaultMovementMethod() {
1259 return null;
1260 }
1261
1262 /**
1263 * Return the text the TextView is displaying. If setText() was called with
1264 * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1265 * the return value from this method to Spannable or Editable, respectively.
1266 *
1267 * Note: The content of the return value should not be modified. If you want
1268 * a modifiable one, you should make your own copy first.
1269 */
1270 @ViewDebug.CapturedViewProperty
1271 public CharSequence getText() {
1272 return mText;
1273 }
1274
1275 /**
1276 * Returns the length, in characters, of the text managed by this TextView
1277 */
1278 public int length() {
1279 return mText.length();
1280 }
1281
1282 /**
1283 * Return the text the TextView is displaying as an Editable object. If
1284 * the text is not editable, null is returned.
1285 *
1286 * @see #getText
1287 */
1288 public Editable getEditableText() {
1289 return (mText instanceof Editable) ? (Editable)mText : null;
1290 }
1291
1292 /**
1293 * @return the height of one standard line in pixels. Note that markup
1294 * within the text can cause individual lines to be taller or shorter
1295 * than this height, and the layout may contain additional first-
1296 * or last-line padding.
1297 */
1298 public int getLineHeight() {
Gilles Debunne96e6b8b2010-12-14 13:43:45 -08001299 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001300 }
1301
1302 /**
1303 * @return the Layout that is currently being used to display the text.
1304 * This can be null if the text or width has recently changes.
1305 */
1306 public final Layout getLayout() {
1307 return mLayout;
1308 }
1309
1310 /**
1311 * @return the current key listener for this TextView.
1312 * This will frequently be null for non-EditText TextViews.
1313 */
1314 public final KeyListener getKeyListener() {
1315 return mInput;
1316 }
1317
1318 /**
1319 * Sets the key listener to be used with this TextView. This can be null
1320 * to disallow user input. Note that this method has significant and
1321 * subtle interactions with soft keyboards and other input method:
1322 * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1323 * for important details. Calling this method will replace the current
1324 * content type of the text view with the content type returned by the
1325 * key listener.
1326 * <p>
1327 * Be warned that if you want a TextView with a key listener or movement
1328 * method not to be focusable, or if you want a TextView without a
1329 * key listener or movement method to be focusable, you must call
1330 * {@link #setFocusable} again after calling this to get the focusability
1331 * back the way you want it.
1332 *
1333 * @attr ref android.R.styleable#TextView_numeric
1334 * @attr ref android.R.styleable#TextView_digits
1335 * @attr ref android.R.styleable#TextView_phoneNumber
1336 * @attr ref android.R.styleable#TextView_inputMethod
1337 * @attr ref android.R.styleable#TextView_capitalize
1338 * @attr ref android.R.styleable#TextView_autoText
1339 */
1340 public void setKeyListener(KeyListener input) {
1341 setKeyListenerOnly(input);
1342 fixFocusableAndClickableSettings();
1343
1344 if (input != null) {
1345 try {
1346 mInputType = mInput.getInputType();
1347 } catch (IncompatibleClassChangeError e) {
1348 mInputType = EditorInfo.TYPE_CLASS_TEXT;
1349 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001350 // Change inputType, without affecting transformation.
1351 // No need to applySingleLine since mSingleLine is unchanged.
1352 setInputTypeSingleLine(mSingleLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001353 } else {
1354 mInputType = EditorInfo.TYPE_NULL;
1355 }
1356
1357 InputMethodManager imm = InputMethodManager.peekInstance();
1358 if (imm != null) imm.restartInput(this);
1359 }
1360
1361 private void setKeyListenerOnly(KeyListener input) {
1362 mInput = input;
1363 if (mInput != null && !(mText instanceof Editable))
1364 setText(mText);
1365
1366 setFilters((Editable) mText, mFilters);
1367 }
1368
1369 /**
1370 * @return the movement method being used for this TextView.
1371 * This will frequently be null for non-EditText TextViews.
1372 */
1373 public final MovementMethod getMovementMethod() {
1374 return mMovement;
1375 }
1376
1377 /**
1378 * Sets the movement method (arrow key handler) to be used for
1379 * this TextView. This can be null to disallow using the arrow keys
1380 * to move the cursor or scroll the view.
1381 * <p>
1382 * Be warned that if you want a TextView with a key listener or movement
1383 * method not to be focusable, or if you want a TextView without a
1384 * key listener or movement method to be focusable, you must call
1385 * {@link #setFocusable} again after calling this to get the focusability
1386 * back the way you want it.
1387 */
1388 public final void setMovementMethod(MovementMethod movement) {
1389 mMovement = movement;
1390
1391 if (mMovement != null && !(mText instanceof Spannable))
1392 setText(mText);
1393
1394 fixFocusableAndClickableSettings();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001395
Gilles Debunnebaaace52010-10-01 15:47:13 -07001396 // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001397 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001398 }
1399
1400 private void fixFocusableAndClickableSettings() {
1401 if ((mMovement != null) || mInput != null) {
1402 setFocusable(true);
1403 setClickable(true);
1404 setLongClickable(true);
1405 } else {
1406 setFocusable(false);
1407 setClickable(false);
1408 setLongClickable(false);
1409 }
1410 }
1411
1412 /**
1413 * @return the current transformation method for this TextView.
1414 * This will frequently be null except for single-line and password
1415 * fields.
1416 */
1417 public final TransformationMethod getTransformationMethod() {
1418 return mTransformation;
1419 }
1420
1421 /**
1422 * Sets the transformation that is applied to the text that this
1423 * TextView is displaying.
1424 *
1425 * @attr ref android.R.styleable#TextView_password
1426 * @attr ref android.R.styleable#TextView_singleLine
1427 */
1428 public final void setTransformationMethod(TransformationMethod method) {
1429 if (method == mTransformation) {
1430 // Avoid the setText() below if the transformation is
1431 // the same.
1432 return;
1433 }
1434 if (mTransformation != null) {
1435 if (mText instanceof Spannable) {
1436 ((Spannable) mText).removeSpan(mTransformation);
1437 }
1438 }
1439
1440 mTransformation = method;
1441
Adam Powell7f8f79a2011-07-07 18:35:54 -07001442 if (method instanceof TransformationMethod2) {
1443 TransformationMethod2 method2 = (TransformationMethod2) method;
1444 mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable);
1445 method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
1446 } else {
1447 mAllowTransformationLengthChange = false;
1448 }
1449
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001450 setText(mText);
1451 }
1452
1453 /**
1454 * Returns the top padding of the view, plus space for the top
1455 * Drawable if any.
1456 */
1457 public int getCompoundPaddingTop() {
1458 final Drawables dr = mDrawables;
1459 if (dr == null || dr.mDrawableTop == null) {
1460 return mPaddingTop;
1461 } else {
1462 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1463 }
1464 }
1465
1466 /**
1467 * Returns the bottom padding of the view, plus space for the bottom
1468 * Drawable if any.
1469 */
1470 public int getCompoundPaddingBottom() {
1471 final Drawables dr = mDrawables;
1472 if (dr == null || dr.mDrawableBottom == null) {
1473 return mPaddingBottom;
1474 } else {
1475 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1476 }
1477 }
1478
1479 /**
1480 * Returns the left padding of the view, plus space for the left
1481 * Drawable if any.
1482 */
1483 public int getCompoundPaddingLeft() {
1484 final Drawables dr = mDrawables;
1485 if (dr == null || dr.mDrawableLeft == null) {
1486 return mPaddingLeft;
1487 } else {
1488 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1489 }
1490 }
1491
1492 /**
1493 * Returns the right padding of the view, plus space for the right
1494 * Drawable if any.
1495 */
1496 public int getCompoundPaddingRight() {
1497 final Drawables dr = mDrawables;
1498 if (dr == null || dr.mDrawableRight == null) {
1499 return mPaddingRight;
1500 } else {
1501 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1502 }
1503 }
1504
1505 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001506 * Returns the start padding of the view, plus space for the start
1507 * Drawable if any.
1508 *
1509 * @hide
1510 */
1511 public int getCompoundPaddingStart() {
1512 resolveDrawables();
1513 switch(getResolvedLayoutDirection()) {
1514 default:
1515 case LAYOUT_DIRECTION_LTR:
1516 return getCompoundPaddingLeft();
1517 case LAYOUT_DIRECTION_RTL:
1518 return getCompoundPaddingRight();
1519 }
1520 }
1521
1522 /**
1523 * Returns the end padding of the view, plus space for the end
1524 * Drawable if any.
1525 *
1526 * @hide
1527 */
1528 public int getCompoundPaddingEnd() {
1529 resolveDrawables();
1530 switch(getResolvedLayoutDirection()) {
1531 default:
1532 case LAYOUT_DIRECTION_LTR:
1533 return getCompoundPaddingRight();
1534 case LAYOUT_DIRECTION_RTL:
1535 return getCompoundPaddingLeft();
1536 }
1537 }
1538
1539 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001540 * Returns the extended top padding of the view, including both the
1541 * top Drawable if any and any extra space to keep more than maxLines
1542 * of text from showing. It is only valid to call this after measuring.
1543 */
1544 public int getExtendedPaddingTop() {
1545 if (mMaxMode != LINES) {
1546 return getCompoundPaddingTop();
1547 }
1548
1549 if (mLayout.getLineCount() <= mMaximum) {
1550 return getCompoundPaddingTop();
1551 }
1552
1553 int top = getCompoundPaddingTop();
1554 int bottom = getCompoundPaddingBottom();
1555 int viewht = getHeight() - top - bottom;
1556 int layoutht = mLayout.getLineTop(mMaximum);
1557
1558 if (layoutht >= viewht) {
1559 return top;
1560 }
1561
1562 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1563 if (gravity == Gravity.TOP) {
1564 return top;
1565 } else if (gravity == Gravity.BOTTOM) {
1566 return top + viewht - layoutht;
1567 } else { // (gravity == Gravity.CENTER_VERTICAL)
1568 return top + (viewht - layoutht) / 2;
1569 }
1570 }
1571
1572 /**
1573 * Returns the extended bottom padding of the view, including both the
1574 * bottom Drawable if any and any extra space to keep more than maxLines
1575 * of text from showing. It is only valid to call this after measuring.
1576 */
1577 public int getExtendedPaddingBottom() {
1578 if (mMaxMode != LINES) {
1579 return getCompoundPaddingBottom();
1580 }
1581
1582 if (mLayout.getLineCount() <= mMaximum) {
1583 return getCompoundPaddingBottom();
1584 }
1585
1586 int top = getCompoundPaddingTop();
1587 int bottom = getCompoundPaddingBottom();
1588 int viewht = getHeight() - top - bottom;
1589 int layoutht = mLayout.getLineTop(mMaximum);
1590
1591 if (layoutht >= viewht) {
1592 return bottom;
1593 }
1594
1595 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1596 if (gravity == Gravity.TOP) {
1597 return bottom + viewht - layoutht;
1598 } else if (gravity == Gravity.BOTTOM) {
1599 return bottom;
1600 } else { // (gravity == Gravity.CENTER_VERTICAL)
1601 return bottom + (viewht - layoutht) / 2;
1602 }
1603 }
1604
1605 /**
1606 * Returns the total left padding of the view, including the left
1607 * Drawable if any.
1608 */
1609 public int getTotalPaddingLeft() {
1610 return getCompoundPaddingLeft();
1611 }
1612
1613 /**
1614 * Returns the total right padding of the view, including the right
1615 * Drawable if any.
1616 */
1617 public int getTotalPaddingRight() {
1618 return getCompoundPaddingRight();
1619 }
1620
1621 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001622 * Returns the total start padding of the view, including the start
1623 * Drawable if any.
1624 *
1625 * @hide
1626 */
1627 public int getTotalPaddingStart() {
1628 return getCompoundPaddingStart();
1629 }
1630
1631 /**
1632 * Returns the total end padding of the view, including the end
1633 * Drawable if any.
1634 *
1635 * @hide
1636 */
1637 public int getTotalPaddingEnd() {
1638 return getCompoundPaddingEnd();
1639 }
1640
1641 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001642 * Returns the total top padding of the view, including the top
1643 * Drawable if any, the extra space to keep more than maxLines
1644 * from showing, and the vertical offset for gravity, if any.
1645 */
1646 public int getTotalPaddingTop() {
1647 return getExtendedPaddingTop() + getVerticalOffset(true);
1648 }
1649
1650 /**
1651 * Returns the total bottom padding of the view, including the bottom
1652 * Drawable if any, the extra space to keep more than maxLines
1653 * from showing, and the vertical offset for gravity, if any.
1654 */
1655 public int getTotalPaddingBottom() {
1656 return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1657 }
1658
1659 /**
1660 * Sets the Drawables (if any) to appear to the left of, above,
1661 * to the right of, and below the text. Use null if you do not
1662 * want a Drawable there. The Drawables must already have had
1663 * {@link Drawable#setBounds} called.
1664 *
1665 * @attr ref android.R.styleable#TextView_drawableLeft
1666 * @attr ref android.R.styleable#TextView_drawableTop
1667 * @attr ref android.R.styleable#TextView_drawableRight
1668 * @attr ref android.R.styleable#TextView_drawableBottom
1669 */
1670 public void setCompoundDrawables(Drawable left, Drawable top,
1671 Drawable right, Drawable bottom) {
1672 Drawables dr = mDrawables;
1673
1674 final boolean drawables = left != null || top != null
1675 || right != null || bottom != null;
1676
1677 if (!drawables) {
1678 // Clearing drawables... can we free the data structure?
1679 if (dr != null) {
1680 if (dr.mDrawablePadding == 0) {
1681 mDrawables = null;
1682 } else {
1683 // We need to retain the last set padding, so just clear
1684 // out all of the fields in the existing structure.
Romain Guy48540eb2009-05-19 16:44:57 -07001685 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001686 dr.mDrawableLeft = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001687 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001688 dr.mDrawableTop = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001689 if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001690 dr.mDrawableRight = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001691 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001692 dr.mDrawableBottom = null;
1693 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1694 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1695 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1696 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1697 }
1698 }
1699 } else {
1700 if (dr == null) {
1701 mDrawables = dr = new Drawables();
1702 }
1703
Romain Guy48540eb2009-05-19 16:44:57 -07001704 if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1705 dr.mDrawableLeft.setCallback(null);
1706 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001707 dr.mDrawableLeft = left;
Romain Guy8e618e52010-03-08 12:18:20 -08001708
1709 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001710 dr.mDrawableTop.setCallback(null);
1711 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001712 dr.mDrawableTop = top;
Romain Guy8e618e52010-03-08 12:18:20 -08001713
1714 if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001715 dr.mDrawableRight.setCallback(null);
1716 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001717 dr.mDrawableRight = right;
Romain Guy8e618e52010-03-08 12:18:20 -08001718
1719 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001720 dr.mDrawableBottom.setCallback(null);
1721 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001722 dr.mDrawableBottom = bottom;
1723
1724 final Rect compoundRect = dr.mCompoundRect;
Romain Guy48540eb2009-05-19 16:44:57 -07001725 int[] state;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001726
1727 state = getDrawableState();
1728
1729 if (left != null) {
1730 left.setState(state);
1731 left.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001732 left.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001733 dr.mDrawableSizeLeft = compoundRect.width();
1734 dr.mDrawableHeightLeft = compoundRect.height();
1735 } else {
1736 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1737 }
1738
1739 if (right != null) {
1740 right.setState(state);
1741 right.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001742 right.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001743 dr.mDrawableSizeRight = compoundRect.width();
1744 dr.mDrawableHeightRight = compoundRect.height();
1745 } else {
1746 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1747 }
1748
1749 if (top != null) {
1750 top.setState(state);
1751 top.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001752 top.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001753 dr.mDrawableSizeTop = compoundRect.height();
1754 dr.mDrawableWidthTop = compoundRect.width();
1755 } else {
1756 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1757 }
1758
1759 if (bottom != null) {
1760 bottom.setState(state);
1761 bottom.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001762 bottom.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001763 dr.mDrawableSizeBottom = compoundRect.height();
1764 dr.mDrawableWidthBottom = compoundRect.width();
1765 } else {
1766 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1767 }
1768 }
1769
1770 invalidate();
1771 requestLayout();
1772 }
1773
1774 /**
1775 * Sets the Drawables (if any) to appear to the left of, above,
1776 * to the right of, and below the text. Use 0 if you do not
1777 * want a Drawable there. The Drawables' bounds will be set to
1778 * their intrinsic bounds.
1779 *
1780 * @param left Resource identifier of the left Drawable.
1781 * @param top Resource identifier of the top Drawable.
1782 * @param right Resource identifier of the right Drawable.
1783 * @param bottom Resource identifier of the bottom Drawable.
1784 *
1785 * @attr ref android.R.styleable#TextView_drawableLeft
1786 * @attr ref android.R.styleable#TextView_drawableTop
1787 * @attr ref android.R.styleable#TextView_drawableRight
1788 * @attr ref android.R.styleable#TextView_drawableBottom
1789 */
1790 public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1791 final Resources resources = getContext().getResources();
1792 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1793 top != 0 ? resources.getDrawable(top) : null,
1794 right != 0 ? resources.getDrawable(right) : null,
1795 bottom != 0 ? resources.getDrawable(bottom) : null);
1796 }
1797
1798 /**
1799 * Sets the Drawables (if any) to appear to the left of, above,
1800 * to the right of, and below the text. Use null if you do not
1801 * want a Drawable there. The Drawables' bounds will be set to
1802 * their intrinsic bounds.
1803 *
1804 * @attr ref android.R.styleable#TextView_drawableLeft
1805 * @attr ref android.R.styleable#TextView_drawableTop
1806 * @attr ref android.R.styleable#TextView_drawableRight
1807 * @attr ref android.R.styleable#TextView_drawableBottom
1808 */
1809 public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1810 Drawable right, Drawable bottom) {
1811
1812 if (left != null) {
1813 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1814 }
1815 if (right != null) {
1816 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1817 }
1818 if (top != null) {
1819 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1820 }
1821 if (bottom != null) {
1822 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1823 }
1824 setCompoundDrawables(left, top, right, bottom);
1825 }
1826
1827 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001828 * Sets the Drawables (if any) to appear to the start of, above,
1829 * to the end of, and below the text. Use null if you do not
1830 * want a Drawable there. The Drawables must already have had
1831 * {@link Drawable#setBounds} called.
1832 *
1833 * @attr ref android.R.styleable#TextView_drawableStart
1834 * @attr ref android.R.styleable#TextView_drawableTop
1835 * @attr ref android.R.styleable#TextView_drawableEnd
1836 * @attr ref android.R.styleable#TextView_drawableBottom
1837 *
1838 * @hide
1839 */
1840 public void setCompoundDrawablesRelative(Drawable start, Drawable top,
1841 Drawable end, Drawable bottom) {
1842 Drawables dr = mDrawables;
1843
1844 final boolean drawables = start != null || top != null
1845 || end != null || bottom != null;
1846
1847 if (!drawables) {
1848 // Clearing drawables... can we free the data structure?
1849 if (dr != null) {
1850 if (dr.mDrawablePadding == 0) {
1851 mDrawables = null;
1852 } else {
1853 // We need to retain the last set padding, so just clear
1854 // out all of the fields in the existing structure.
1855 if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
1856 dr.mDrawableStart = null;
1857 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1858 dr.mDrawableTop = null;
1859 if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
1860 dr.mDrawableEnd = null;
1861 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1862 dr.mDrawableBottom = null;
1863 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1864 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1865 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1866 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1867 }
1868 }
1869 } else {
1870 if (dr == null) {
1871 mDrawables = dr = new Drawables();
1872 }
1873
1874 if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
1875 dr.mDrawableStart.setCallback(null);
1876 }
1877 dr.mDrawableStart = start;
1878
1879 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1880 dr.mDrawableTop.setCallback(null);
1881 }
1882 dr.mDrawableTop = top;
1883
1884 if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
1885 dr.mDrawableEnd.setCallback(null);
1886 }
1887 dr.mDrawableEnd = end;
1888
1889 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1890 dr.mDrawableBottom.setCallback(null);
1891 }
1892 dr.mDrawableBottom = bottom;
1893
1894 final Rect compoundRect = dr.mCompoundRect;
1895 int[] state;
1896
1897 state = getDrawableState();
1898
1899 if (start != null) {
1900 start.setState(state);
1901 start.copyBounds(compoundRect);
1902 start.setCallback(this);
1903 dr.mDrawableSizeStart = compoundRect.width();
1904 dr.mDrawableHeightStart = compoundRect.height();
1905 } else {
1906 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1907 }
1908
1909 if (end != null) {
1910 end.setState(state);
1911 end.copyBounds(compoundRect);
1912 end.setCallback(this);
1913 dr.mDrawableSizeEnd = compoundRect.width();
1914 dr.mDrawableHeightEnd = compoundRect.height();
1915 } else {
1916 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1917 }
1918
1919 if (top != null) {
1920 top.setState(state);
1921 top.copyBounds(compoundRect);
1922 top.setCallback(this);
1923 dr.mDrawableSizeTop = compoundRect.height();
1924 dr.mDrawableWidthTop = compoundRect.width();
1925 } else {
1926 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1927 }
1928
1929 if (bottom != null) {
1930 bottom.setState(state);
1931 bottom.copyBounds(compoundRect);
1932 bottom.setCallback(this);
1933 dr.mDrawableSizeBottom = compoundRect.height();
1934 dr.mDrawableWidthBottom = compoundRect.width();
1935 } else {
1936 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1937 }
1938 }
1939
1940 resolveDrawables();
1941 invalidate();
1942 requestLayout();
1943 }
1944
1945 /**
1946 * Sets the Drawables (if any) to appear to the start of, above,
1947 * to the end of, and below the text. Use 0 if you do not
1948 * want a Drawable there. The Drawables' bounds will be set to
1949 * their intrinsic bounds.
1950 *
1951 * @param start Resource identifier of the start Drawable.
1952 * @param top Resource identifier of the top Drawable.
1953 * @param end Resource identifier of the end Drawable.
1954 * @param bottom Resource identifier of the bottom Drawable.
1955 *
1956 * @attr ref android.R.styleable#TextView_drawableStart
1957 * @attr ref android.R.styleable#TextView_drawableTop
1958 * @attr ref android.R.styleable#TextView_drawableEnd
1959 * @attr ref android.R.styleable#TextView_drawableBottom
1960 *
1961 * @hide
1962 */
1963 public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end,
1964 int bottom) {
1965 resetResolvedDrawables();
1966 final Resources resources = getContext().getResources();
1967 setCompoundDrawablesRelativeWithIntrinsicBounds(
1968 start != 0 ? resources.getDrawable(start) : null,
1969 top != 0 ? resources.getDrawable(top) : null,
1970 end != 0 ? resources.getDrawable(end) : null,
1971 bottom != 0 ? resources.getDrawable(bottom) : null);
1972 }
1973
1974 /**
1975 * Sets the Drawables (if any) to appear to the start of, above,
1976 * to the end of, and below the text. Use null if you do not
1977 * want a Drawable there. The Drawables' bounds will be set to
1978 * their intrinsic bounds.
1979 *
1980 * @attr ref android.R.styleable#TextView_drawableStart
1981 * @attr ref android.R.styleable#TextView_drawableTop
1982 * @attr ref android.R.styleable#TextView_drawableEnd
1983 * @attr ref android.R.styleable#TextView_drawableBottom
1984 *
1985 * @hide
1986 */
1987 public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top,
1988 Drawable end, Drawable bottom) {
1989
1990 resetResolvedDrawables();
1991 if (start != null) {
1992 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1993 }
1994 if (end != null) {
1995 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1996 }
1997 if (top != null) {
1998 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1999 }
2000 if (bottom != null) {
2001 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
2002 }
2003 setCompoundDrawablesRelative(start, top, end, bottom);
2004 }
2005
2006 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002007 * Returns drawables for the left, top, right, and bottom borders.
2008 */
2009 public Drawable[] getCompoundDrawables() {
2010 final Drawables dr = mDrawables;
2011 if (dr != null) {
2012 return new Drawable[] {
2013 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
2014 };
2015 } else {
2016 return new Drawable[] { null, null, null, null };
2017 }
2018 }
2019
2020 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002021 * Returns drawables for the start, top, end, and bottom borders.
2022 *
2023 * @hide
2024 */
2025 public Drawable[] getCompoundDrawablesRelative() {
2026 final Drawables dr = mDrawables;
2027 if (dr != null) {
2028 return new Drawable[] {
2029 dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom
2030 };
2031 } else {
2032 return new Drawable[] { null, null, null, null };
2033 }
2034 }
2035
2036 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002037 * Sets the size of the padding between the compound drawables and
2038 * the text.
2039 *
2040 * @attr ref android.R.styleable#TextView_drawablePadding
2041 */
2042 public void setCompoundDrawablePadding(int pad) {
2043 Drawables dr = mDrawables;
2044 if (pad == 0) {
2045 if (dr != null) {
2046 dr.mDrawablePadding = pad;
2047 }
2048 } else {
2049 if (dr == null) {
2050 mDrawables = dr = new Drawables();
2051 }
2052 dr.mDrawablePadding = pad;
2053 }
2054
2055 invalidate();
2056 requestLayout();
2057 }
2058
2059 /**
2060 * Returns the padding between the compound drawables and the text.
2061 */
2062 public int getCompoundDrawablePadding() {
2063 final Drawables dr = mDrawables;
2064 return dr != null ? dr.mDrawablePadding : 0;
2065 }
2066
2067 @Override
2068 public void setPadding(int left, int top, int right, int bottom) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07002069 if (left != mPaddingLeft ||
2070 right != mPaddingRight ||
2071 top != mPaddingTop ||
2072 bottom != mPaddingBottom) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002073 nullLayouts();
2074 }
2075
2076 // the super call will requestLayout()
2077 super.setPadding(left, top, right, bottom);
2078 invalidate();
2079 }
2080
2081 /**
2082 * Gets the autolink mask of the text. See {@link
2083 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2084 * possible values.
2085 *
2086 * @attr ref android.R.styleable#TextView_autoLink
2087 */
2088 public final int getAutoLinkMask() {
2089 return mAutoLinkMask;
2090 }
2091
2092 /**
2093 * Sets the text color, size, style, hint color, and highlight color
2094 * from the specified TextAppearance resource.
2095 */
2096 public void setTextAppearance(Context context, int resid) {
2097 TypedArray appearance =
2098 context.obtainStyledAttributes(resid,
2099 com.android.internal.R.styleable.TextAppearance);
2100
2101 int color;
2102 ColorStateList colors;
2103 int ts;
2104
2105 color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
2106 if (color != 0) {
2107 setHighlightColor(color);
2108 }
2109
2110 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2111 TextAppearance_textColor);
2112 if (colors != null) {
2113 setTextColor(colors);
2114 }
2115
2116 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
2117 TextAppearance_textSize, 0);
2118 if (ts != 0) {
2119 setRawTextSize(ts);
2120 }
2121
2122 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2123 TextAppearance_textColorHint);
2124 if (colors != null) {
2125 setHintTextColor(colors);
2126 }
2127
2128 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2129 TextAppearance_textColorLink);
2130 if (colors != null) {
2131 setLinkTextColor(colors);
2132 }
2133
2134 int typefaceIndex, styleIndex;
2135
2136 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
2137 TextAppearance_typeface, -1);
2138 styleIndex = appearance.getInt(com.android.internal.R.styleable.
2139 TextAppearance_textStyle, -1);
2140
2141 setTypefaceByIndex(typefaceIndex, styleIndex);
Gilles Debunne21078e42011-08-02 10:22:35 -07002142
Adam Powell7f8f79a2011-07-07 18:35:54 -07002143 if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
2144 false)) {
2145 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
2146 }
2147
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002148 appearance.recycle();
2149 }
2150
2151 /**
2152 * @return the size (in pixels) of the default text size in this TextView.
2153 */
2154 public float getTextSize() {
2155 return mTextPaint.getTextSize();
2156 }
2157
2158 /**
2159 * Set the default text size to the given value, interpreted as "scaled
2160 * pixel" units. This size is adjusted based on the current density and
2161 * user font size preference.
2162 *
2163 * @param size The scaled pixel size.
2164 *
2165 * @attr ref android.R.styleable#TextView_textSize
2166 */
2167 @android.view.RemotableViewMethod
2168 public void setTextSize(float size) {
2169 setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
2170 }
2171
2172 /**
2173 * Set the default text size to a given unit and value. See {@link
2174 * TypedValue} for the possible dimension units.
2175 *
2176 * @param unit The desired dimension unit.
2177 * @param size The desired size in the given units.
2178 *
2179 * @attr ref android.R.styleable#TextView_textSize
2180 */
2181 public void setTextSize(int unit, float size) {
2182 Context c = getContext();
2183 Resources r;
2184
2185 if (c == null)
2186 r = Resources.getSystem();
2187 else
2188 r = c.getResources();
2189
2190 setRawTextSize(TypedValue.applyDimension(
2191 unit, size, r.getDisplayMetrics()));
2192 }
2193
2194 private void setRawTextSize(float size) {
2195 if (size != mTextPaint.getTextSize()) {
2196 mTextPaint.setTextSize(size);
2197
2198 if (mLayout != null) {
2199 nullLayouts();
2200 requestLayout();
2201 invalidate();
2202 }
2203 }
2204 }
2205
2206 /**
2207 * @return the extent by which text is currently being stretched
2208 * horizontally. This will usually be 1.
2209 */
2210 public float getTextScaleX() {
2211 return mTextPaint.getTextScaleX();
2212 }
2213
2214 /**
2215 * Sets the extent by which text should be stretched horizontally.
2216 *
2217 * @attr ref android.R.styleable#TextView_textScaleX
2218 */
2219 @android.view.RemotableViewMethod
2220 public void setTextScaleX(float size) {
2221 if (size != mTextPaint.getTextScaleX()) {
Romain Guy939151f2009-04-08 14:22:40 -07002222 mUserSetTextScaleX = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002223 mTextPaint.setTextScaleX(size);
2224
2225 if (mLayout != null) {
2226 nullLayouts();
2227 requestLayout();
2228 invalidate();
2229 }
2230 }
2231 }
2232
2233 /**
2234 * Sets the typeface and style in which the text should be displayed.
2235 * Note that not all Typeface families actually have bold and italic
2236 * variants, so you may need to use
2237 * {@link #setTypeface(Typeface, int)} to get the appearance
2238 * that you actually want.
2239 *
2240 * @attr ref android.R.styleable#TextView_typeface
2241 * @attr ref android.R.styleable#TextView_textStyle
2242 */
2243 public void setTypeface(Typeface tf) {
2244 if (mTextPaint.getTypeface() != tf) {
2245 mTextPaint.setTypeface(tf);
2246
2247 if (mLayout != null) {
2248 nullLayouts();
2249 requestLayout();
2250 invalidate();
2251 }
2252 }
2253 }
2254
2255 /**
2256 * @return the current typeface and style in which the text is being
2257 * displayed.
2258 */
2259 public Typeface getTypeface() {
2260 return mTextPaint.getTypeface();
2261 }
2262
2263 /**
2264 * Sets the text color for all the states (normal, selected,
2265 * focused) to be this color.
2266 *
2267 * @attr ref android.R.styleable#TextView_textColor
2268 */
2269 @android.view.RemotableViewMethod
2270 public void setTextColor(int color) {
2271 mTextColor = ColorStateList.valueOf(color);
2272 updateTextColors();
2273 }
2274
2275 /**
2276 * Sets the text color.
2277 *
2278 * @attr ref android.R.styleable#TextView_textColor
2279 */
2280 public void setTextColor(ColorStateList colors) {
2281 if (colors == null) {
2282 throw new NullPointerException();
2283 }
2284
2285 mTextColor = colors;
2286 updateTextColors();
2287 }
2288
2289 /**
2290 * Return the set of text colors.
2291 *
2292 * @return Returns the set of text colors.
2293 */
2294 public final ColorStateList getTextColors() {
2295 return mTextColor;
2296 }
2297
2298 /**
2299 * <p>Return the current color selected for normal text.</p>
2300 *
2301 * @return Returns the current text color.
2302 */
2303 public final int getCurrentTextColor() {
2304 return mCurTextColor;
2305 }
2306
2307 /**
2308 * Sets the color used to display the selection highlight.
2309 *
2310 * @attr ref android.R.styleable#TextView_textColorHighlight
2311 */
2312 @android.view.RemotableViewMethod
2313 public void setHighlightColor(int color) {
2314 if (mHighlightColor != color) {
2315 mHighlightColor = color;
2316 invalidate();
2317 }
2318 }
2319
2320 /**
2321 * Gives the text a shadow of the specified radius and color, the specified
2322 * distance from its normal position.
2323 *
2324 * @attr ref android.R.styleable#TextView_shadowColor
2325 * @attr ref android.R.styleable#TextView_shadowDx
2326 * @attr ref android.R.styleable#TextView_shadowDy
2327 * @attr ref android.R.styleable#TextView_shadowRadius
2328 */
2329 public void setShadowLayer(float radius, float dx, float dy, int color) {
2330 mTextPaint.setShadowLayer(radius, dx, dy, color);
2331
2332 mShadowRadius = radius;
2333 mShadowDx = dx;
2334 mShadowDy = dy;
2335
2336 invalidate();
2337 }
2338
2339 /**
2340 * @return the base paint used for the text. Please use this only to
2341 * consult the Paint's properties and not to change them.
2342 */
2343 public TextPaint getPaint() {
2344 return mTextPaint;
2345 }
2346
2347 /**
2348 * Sets the autolink mask of the text. See {@link
2349 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2350 * possible values.
2351 *
2352 * @attr ref android.R.styleable#TextView_autoLink
2353 */
2354 @android.view.RemotableViewMethod
2355 public final void setAutoLinkMask(int mask) {
2356 mAutoLinkMask = mask;
2357 }
2358
2359 /**
2360 * Sets whether the movement method will automatically be set to
2361 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2362 * set to nonzero and links are detected in {@link #setText}.
2363 * The default is true.
2364 *
2365 * @attr ref android.R.styleable#TextView_linksClickable
2366 */
2367 @android.view.RemotableViewMethod
2368 public final void setLinksClickable(boolean whether) {
2369 mLinksClickable = whether;
2370 }
2371
2372 /**
2373 * Returns whether the movement method will automatically be set to
2374 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2375 * set to nonzero and links are detected in {@link #setText}.
2376 * The default is true.
2377 *
2378 * @attr ref android.R.styleable#TextView_linksClickable
2379 */
2380 public final boolean getLinksClickable() {
2381 return mLinksClickable;
2382 }
2383
2384 /**
Gilles Debunne550efbf2011-10-10 16:49:02 -07002385 * Sets whether the soft input method will be made visible when this
2386 * TextView gets focused. The default is true.
2387 *
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002388 * @attr ref android.R.styleable#TextView_softInputShownOnFocus
Gilles Debunne550efbf2011-10-10 16:49:02 -07002389 * @hide
2390 */
2391 @android.view.RemotableViewMethod
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002392 public final void setSoftInputShownOnFocus(boolean show) {
2393 mSoftInputShownOnFocus = show;
Gilles Debunne550efbf2011-10-10 16:49:02 -07002394 }
2395
2396 /**
2397 * Returns whether the soft input method will be made visible when this
2398 * TextView gets focused. The default is true.
2399 *
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002400 * @attr ref android.R.styleable#TextView_softInputShownOnFocus
Gilles Debunne550efbf2011-10-10 16:49:02 -07002401 * @hide
2402 */
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002403 public final boolean getSoftInputShownOnFocus() {
2404 return mSoftInputShownOnFocus;
Gilles Debunne550efbf2011-10-10 16:49:02 -07002405 }
2406
2407 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002408 * Returns the list of URLSpans attached to the text
2409 * (by {@link Linkify} or otherwise) if any. You can call
2410 * {@link URLSpan#getURL} on them to find where they link to
2411 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
2412 * to find the region of the text they are attached to.
2413 */
2414 public URLSpan[] getUrls() {
2415 if (mText instanceof Spanned) {
2416 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
2417 } else {
2418 return new URLSpan[0];
2419 }
2420 }
2421
2422 /**
2423 * Sets the color of the hint text.
2424 *
2425 * @attr ref android.R.styleable#TextView_textColorHint
2426 */
2427 @android.view.RemotableViewMethod
2428 public final void setHintTextColor(int color) {
2429 mHintTextColor = ColorStateList.valueOf(color);
2430 updateTextColors();
2431 }
2432
2433 /**
2434 * Sets the color of the hint text.
2435 *
2436 * @attr ref android.R.styleable#TextView_textColorHint
2437 */
2438 public final void setHintTextColor(ColorStateList colors) {
2439 mHintTextColor = colors;
2440 updateTextColors();
2441 }
2442
2443 /**
2444 * <p>Return the color used to paint the hint text.</p>
2445 *
2446 * @return Returns the list of hint text colors.
2447 */
2448 public final ColorStateList getHintTextColors() {
2449 return mHintTextColor;
2450 }
2451
2452 /**
2453 * <p>Return the current color selected to paint the hint text.</p>
2454 *
2455 * @return Returns the current hint text color.
2456 */
2457 public final int getCurrentHintTextColor() {
2458 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2459 }
2460
2461 /**
2462 * Sets the color of links in the text.
2463 *
2464 * @attr ref android.R.styleable#TextView_textColorLink
2465 */
2466 @android.view.RemotableViewMethod
2467 public final void setLinkTextColor(int color) {
2468 mLinkTextColor = ColorStateList.valueOf(color);
2469 updateTextColors();
2470 }
2471
2472 /**
2473 * Sets the color of links in the text.
2474 *
2475 * @attr ref android.R.styleable#TextView_textColorLink
2476 */
2477 public final void setLinkTextColor(ColorStateList colors) {
2478 mLinkTextColor = colors;
2479 updateTextColors();
2480 }
2481
2482 /**
2483 * <p>Returns the color used to paint links in the text.</p>
2484 *
2485 * @return Returns the list of link text colors.
2486 */
2487 public final ColorStateList getLinkTextColors() {
2488 return mLinkTextColor;
2489 }
2490
2491 /**
2492 * Sets the horizontal alignment of the text and the
2493 * vertical gravity that will be used when there is extra space
2494 * in the TextView beyond what is required for the text itself.
2495 *
2496 * @see android.view.Gravity
2497 * @attr ref android.R.styleable#TextView_gravity
2498 */
2499 public void setGravity(int gravity) {
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002500 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -07002501 gravity |= Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002502 }
2503 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2504 gravity |= Gravity.TOP;
2505 }
2506
2507 boolean newLayout = false;
2508
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002509 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=
2510 (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002511 newLayout = true;
2512 }
2513
2514 if (gravity != mGravity) {
2515 invalidate();
Fabrice Di Meglio9f513842011-10-12 11:43:27 -07002516 mLayoutAlignment = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002517 }
2518
2519 mGravity = gravity;
2520
2521 if (mLayout != null && newLayout) {
2522 // XXX this is heavy-handed because no actual content changes.
2523 int want = mLayout.getWidth();
2524 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2525
2526 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2527 mRight - mLeft - getCompoundPaddingLeft() -
2528 getCompoundPaddingRight(), true);
2529 }
2530 }
2531
2532 /**
2533 * Returns the horizontal and vertical alignment of this TextView.
2534 *
2535 * @see android.view.Gravity
2536 * @attr ref android.R.styleable#TextView_gravity
2537 */
2538 public int getGravity() {
2539 return mGravity;
2540 }
2541
2542 /**
2543 * @return the flags on the Paint being used to display the text.
2544 * @see Paint#getFlags
2545 */
2546 public int getPaintFlags() {
2547 return mTextPaint.getFlags();
2548 }
2549
2550 /**
2551 * Sets flags on the Paint being used to display the text and
2552 * reflows the text if they are different from the old flags.
2553 * @see Paint#setFlags
2554 */
2555 @android.view.RemotableViewMethod
2556 public void setPaintFlags(int flags) {
2557 if (mTextPaint.getFlags() != flags) {
2558 mTextPaint.setFlags(flags);
2559
2560 if (mLayout != null) {
2561 nullLayouts();
2562 requestLayout();
2563 invalidate();
2564 }
2565 }
2566 }
2567
2568 /**
2569 * Sets whether the text should be allowed to be wider than the
2570 * View is. If false, it will be wrapped to the width of the View.
2571 *
2572 * @attr ref android.R.styleable#TextView_scrollHorizontally
2573 */
2574 public void setHorizontallyScrolling(boolean whether) {
Gilles Debunne22378292011-08-12 10:38:52 -07002575 if (mHorizontallyScrolling != whether) {
2576 mHorizontallyScrolling = whether;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002577
Gilles Debunne22378292011-08-12 10:38:52 -07002578 if (mLayout != null) {
2579 nullLayouts();
2580 requestLayout();
2581 invalidate();
2582 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002583 }
2584 }
2585
2586 /**
Gilles Debunnef2a02012011-10-27 11:10:14 -07002587 * Returns whether the text is allowed to be wider than the View is.
2588 * If false, the text will be wrapped to the width of the View.
2589 *
2590 * @attr ref android.R.styleable#TextView_scrollHorizontally
2591 * @hide
2592 */
2593 public boolean getHorizontallyScrolling() {
2594 return mHorizontallyScrolling;
2595 }
2596
2597 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002598 * Makes the TextView at least this many lines tall.
2599 *
2600 * Setting this value overrides any other (minimum) height setting. A single line TextView will
2601 * set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002602 *
2603 * @attr ref android.R.styleable#TextView_minLines
2604 */
2605 @android.view.RemotableViewMethod
2606 public void setMinLines(int minlines) {
2607 mMinimum = minlines;
2608 mMinMode = LINES;
2609
2610 requestLayout();
2611 invalidate();
2612 }
2613
2614 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002615 * Makes the TextView at least this many pixels tall.
2616 *
2617 * Setting this value overrides any other (minimum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002618 *
2619 * @attr ref android.R.styleable#TextView_minHeight
2620 */
2621 @android.view.RemotableViewMethod
2622 public void setMinHeight(int minHeight) {
2623 mMinimum = minHeight;
2624 mMinMode = PIXELS;
2625
2626 requestLayout();
2627 invalidate();
2628 }
2629
2630 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002631 * Makes the TextView at most this many lines tall.
2632 *
2633 * Setting this value overrides any other (maximum) height setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002634 *
2635 * @attr ref android.R.styleable#TextView_maxLines
2636 */
2637 @android.view.RemotableViewMethod
2638 public void setMaxLines(int maxlines) {
2639 mMaximum = maxlines;
2640 mMaxMode = LINES;
2641
2642 requestLayout();
2643 invalidate();
2644 }
2645
2646 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002647 * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the
2648 * {@link #setMaxLines(int)} method.
2649 *
2650 * Setting this value overrides any other (maximum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002651 *
2652 * @attr ref android.R.styleable#TextView_maxHeight
2653 */
2654 @android.view.RemotableViewMethod
2655 public void setMaxHeight(int maxHeight) {
2656 mMaximum = maxHeight;
2657 mMaxMode = PIXELS;
2658
2659 requestLayout();
2660 invalidate();
2661 }
2662
2663 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002664 * Makes the TextView exactly this many lines tall.
2665 *
2666 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2667 * height setting. A single line TextView will set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002668 *
2669 * @attr ref android.R.styleable#TextView_lines
2670 */
2671 @android.view.RemotableViewMethod
2672 public void setLines(int lines) {
2673 mMaximum = mMinimum = lines;
2674 mMaxMode = mMinMode = LINES;
2675
2676 requestLayout();
2677 invalidate();
2678 }
2679
2680 /**
2681 * Makes the TextView exactly this many pixels tall.
2682 * You could do the same thing by specifying this number in the
2683 * LayoutParams.
2684 *
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002685 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2686 * height setting.
2687 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002688 * @attr ref android.R.styleable#TextView_height
2689 */
2690 @android.view.RemotableViewMethod
2691 public void setHeight(int pixels) {
2692 mMaximum = mMinimum = pixels;
2693 mMaxMode = mMinMode = PIXELS;
2694
2695 requestLayout();
2696 invalidate();
2697 }
2698
2699 /**
2700 * Makes the TextView at least this many ems wide
2701 *
2702 * @attr ref android.R.styleable#TextView_minEms
2703 */
2704 @android.view.RemotableViewMethod
2705 public void setMinEms(int minems) {
2706 mMinWidth = minems;
2707 mMinWidthMode = EMS;
2708
2709 requestLayout();
2710 invalidate();
2711 }
2712
2713 /**
2714 * Makes the TextView at least this many pixels wide
2715 *
2716 * @attr ref android.R.styleable#TextView_minWidth
2717 */
2718 @android.view.RemotableViewMethod
2719 public void setMinWidth(int minpixels) {
2720 mMinWidth = minpixels;
2721 mMinWidthMode = PIXELS;
2722
2723 requestLayout();
2724 invalidate();
2725 }
2726
2727 /**
2728 * Makes the TextView at most this many ems wide
2729 *
2730 * @attr ref android.R.styleable#TextView_maxEms
2731 */
2732 @android.view.RemotableViewMethod
2733 public void setMaxEms(int maxems) {
2734 mMaxWidth = maxems;
2735 mMaxWidthMode = EMS;
2736
2737 requestLayout();
2738 invalidate();
2739 }
2740
2741 /**
2742 * Makes the TextView at most this many pixels wide
2743 *
2744 * @attr ref android.R.styleable#TextView_maxWidth
2745 */
2746 @android.view.RemotableViewMethod
2747 public void setMaxWidth(int maxpixels) {
2748 mMaxWidth = maxpixels;
2749 mMaxWidthMode = PIXELS;
2750
2751 requestLayout();
2752 invalidate();
2753 }
2754
2755 /**
2756 * Makes the TextView exactly this many ems wide
2757 *
2758 * @attr ref android.R.styleable#TextView_ems
2759 */
2760 @android.view.RemotableViewMethod
2761 public void setEms(int ems) {
2762 mMaxWidth = mMinWidth = ems;
2763 mMaxWidthMode = mMinWidthMode = EMS;
2764
2765 requestLayout();
2766 invalidate();
2767 }
2768
2769 /**
2770 * Makes the TextView exactly this many pixels wide.
2771 * You could do the same thing by specifying this number in the
2772 * LayoutParams.
2773 *
2774 * @attr ref android.R.styleable#TextView_width
2775 */
2776 @android.view.RemotableViewMethod
2777 public void setWidth(int pixels) {
2778 mMaxWidth = mMinWidth = pixels;
2779 mMaxWidthMode = mMinWidthMode = PIXELS;
2780
2781 requestLayout();
2782 invalidate();
2783 }
2784
2785
2786 /**
2787 * Sets line spacing for this TextView. Each line will have its height
2788 * multiplied by <code>mult</code> and have <code>add</code> added to it.
2789 *
2790 * @attr ref android.R.styleable#TextView_lineSpacingExtra
2791 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2792 */
2793 public void setLineSpacing(float add, float mult) {
Gilles Debunne22378292011-08-12 10:38:52 -07002794 if (mSpacingAdd != add || mSpacingMult != mult) {
2795 mSpacingAdd = add;
2796 mSpacingMult = mult;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002797
Gilles Debunne22378292011-08-12 10:38:52 -07002798 if (mLayout != null) {
2799 nullLayouts();
2800 requestLayout();
2801 invalidate();
2802 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002803 }
2804 }
2805
2806 /**
2807 * Convenience method: Append the specified text to the TextView's
2808 * display buffer, upgrading it to BufferType.EDITABLE if it was
2809 * not already editable.
2810 */
2811 public final void append(CharSequence text) {
2812 append(text, 0, text.length());
2813 }
2814
2815 /**
2816 * Convenience method: Append the specified text slice to the TextView's
2817 * display buffer, upgrading it to BufferType.EDITABLE if it was
2818 * not already editable.
2819 */
2820 public void append(CharSequence text, int start, int end) {
2821 if (!(mText instanceof Editable)) {
2822 setText(mText, BufferType.EDITABLE);
2823 }
2824
2825 ((Editable) mText).append(text, start, end);
2826 }
2827
2828 private void updateTextColors() {
2829 boolean inval = false;
2830 int color = mTextColor.getColorForState(getDrawableState(), 0);
2831 if (color != mCurTextColor) {
2832 mCurTextColor = color;
2833 inval = true;
2834 }
2835 if (mLinkTextColor != null) {
2836 color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2837 if (color != mTextPaint.linkColor) {
2838 mTextPaint.linkColor = color;
2839 inval = true;
2840 }
2841 }
2842 if (mHintTextColor != null) {
2843 color = mHintTextColor.getColorForState(getDrawableState(), 0);
2844 if (color != mCurHintTextColor && mText.length() == 0) {
2845 mCurHintTextColor = color;
2846 inval = true;
2847 }
2848 }
2849 if (inval) {
2850 invalidate();
2851 }
2852 }
2853
2854 @Override
2855 protected void drawableStateChanged() {
2856 super.drawableStateChanged();
2857 if (mTextColor != null && mTextColor.isStateful()
2858 || (mHintTextColor != null && mHintTextColor.isStateful())
2859 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2860 updateTextColors();
2861 }
2862
2863 final Drawables dr = mDrawables;
2864 if (dr != null) {
2865 int[] state = getDrawableState();
2866 if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2867 dr.mDrawableTop.setState(state);
2868 }
2869 if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2870 dr.mDrawableBottom.setState(state);
2871 }
2872 if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2873 dr.mDrawableLeft.setState(state);
2874 }
2875 if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2876 dr.mDrawableRight.setState(state);
2877 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002878 if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) {
2879 dr.mDrawableStart.setState(state);
2880 }
2881 if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) {
2882 dr.mDrawableEnd.setState(state);
2883 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002884 }
2885 }
2886
2887 /**
2888 * User interface state that is stored by TextView for implementing
2889 * {@link View#onSaveInstanceState}.
2890 */
2891 public static class SavedState extends BaseSavedState {
2892 int selStart;
2893 int selEnd;
2894 CharSequence text;
2895 boolean frozenWithFocus;
The Android Open Source Project4df24232009-03-05 14:34:35 -08002896 CharSequence error;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002897
2898 SavedState(Parcelable superState) {
2899 super(superState);
2900 }
2901
2902 @Override
2903 public void writeToParcel(Parcel out, int flags) {
2904 super.writeToParcel(out, flags);
2905 out.writeInt(selStart);
2906 out.writeInt(selEnd);
2907 out.writeInt(frozenWithFocus ? 1 : 0);
2908 TextUtils.writeToParcel(text, out, flags);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002909
2910 if (error == null) {
2911 out.writeInt(0);
2912 } else {
2913 out.writeInt(1);
2914 TextUtils.writeToParcel(error, out, flags);
2915 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002916 }
2917
2918 @Override
2919 public String toString() {
2920 String str = "TextView.SavedState{"
2921 + Integer.toHexString(System.identityHashCode(this))
2922 + " start=" + selStart + " end=" + selEnd;
2923 if (text != null) {
2924 str += " text=" + text;
2925 }
2926 return str + "}";
2927 }
2928
Gilles Debunnee15b3582010-06-16 15:17:21 -07002929 @SuppressWarnings("hiding")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002930 public static final Parcelable.Creator<SavedState> CREATOR
2931 = new Parcelable.Creator<SavedState>() {
2932 public SavedState createFromParcel(Parcel in) {
2933 return new SavedState(in);
2934 }
2935
2936 public SavedState[] newArray(int size) {
2937 return new SavedState[size];
2938 }
2939 };
2940
2941 private SavedState(Parcel in) {
2942 super(in);
2943 selStart = in.readInt();
2944 selEnd = in.readInt();
2945 frozenWithFocus = (in.readInt() != 0);
2946 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002947
2948 if (in.readInt() != 0) {
2949 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2950 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002951 }
2952 }
2953
2954 @Override
2955 public Parcelable onSaveInstanceState() {
2956 Parcelable superState = super.onSaveInstanceState();
2957
2958 // Save state if we are forced to
2959 boolean save = mFreezesText;
2960 int start = 0;
2961 int end = 0;
2962
2963 if (mText != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07002964 start = getSelectionStart();
2965 end = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002966 if (start >= 0 || end >= 0) {
2967 // Or save state if there is a selection
2968 save = true;
2969 }
2970 }
2971
2972 if (save) {
2973 SavedState ss = new SavedState(superState);
2974 // XXX Should also save the current scroll position!
2975 ss.selStart = start;
2976 ss.selEnd = end;
2977
2978 if (mText instanceof Spanned) {
2979 /*
2980 * Calling setText() strips off any ChangeWatchers;
2981 * strip them now to avoid leaking references.
2982 * But do it to a copy so that if there are any
2983 * further changes to the text of this view, it
2984 * won't get into an inconsistent state.
2985 */
2986
2987 Spannable sp = new SpannableString(mText);
2988
Gilles Debunne176cd0d2011-09-29 16:37:27 -07002989 for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002990 sp.removeSpan(cw);
2991 }
2992
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07002993 removeMisspelledSpans(sp);
Gilles Debunneaa67eef2011-06-01 18:03:37 -07002994 sp.removeSpan(mSuggestionRangeSpan);
2995
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002996 ss.text = sp;
2997 } else {
2998 ss.text = mText.toString();
2999 }
3000
3001 if (isFocused() && start >= 0 && end >= 0) {
3002 ss.frozenWithFocus = true;
3003 }
3004
The Android Open Source Project4df24232009-03-05 14:34:35 -08003005 ss.error = mError;
3006
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003007 return ss;
3008 }
3009
3010 return superState;
3011 }
3012
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07003013 void removeMisspelledSpans(Spannable spannable) {
3014 SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
3015 SuggestionSpan.class);
3016 for (int i = 0; i < suggestionSpans.length; i++) {
3017 int flags = suggestionSpans[i].getFlags();
3018 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
3019 && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3020 spannable.removeSpan(suggestionSpans[i]);
3021 }
3022 }
3023 }
3024
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003025 @Override
3026 public void onRestoreInstanceState(Parcelable state) {
3027 if (!(state instanceof SavedState)) {
3028 super.onRestoreInstanceState(state);
3029 return;
3030 }
3031
3032 SavedState ss = (SavedState)state;
3033 super.onRestoreInstanceState(ss.getSuperState());
3034
3035 // XXX restore buffer type too, as well as lots of other stuff
3036 if (ss.text != null) {
3037 setText(ss.text);
3038 }
3039
3040 if (ss.selStart >= 0 && ss.selEnd >= 0) {
3041 if (mText instanceof Spannable) {
3042 int len = mText.length();
3043
3044 if (ss.selStart > len || ss.selEnd > len) {
3045 String restored = "";
3046
3047 if (ss.text != null) {
3048 restored = "(restored) ";
3049 }
3050
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003051 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003052 "/" + ss.selEnd + " out of range for " + restored +
3053 "text " + mText);
3054 } else {
3055 Selection.setSelection((Spannable) mText, ss.selStart,
3056 ss.selEnd);
3057
3058 if (ss.frozenWithFocus) {
3059 mFrozenWithFocus = true;
3060 }
3061 }
3062 }
3063 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003064
3065 if (ss.error != null) {
Romain Guy9bc9fa12009-07-21 16:57:29 -07003066 final CharSequence error = ss.error;
3067 // Display the error later, after the first layout pass
3068 post(new Runnable() {
3069 public void run() {
3070 setError(error);
3071 }
3072 });
The Android Open Source Project4df24232009-03-05 14:34:35 -08003073 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003074 }
3075
3076 /**
3077 * Control whether this text view saves its entire text contents when
3078 * freezing to an icicle, in addition to dynamic state such as cursor
3079 * position. By default this is false, not saving the text. Set to true
3080 * if the text in the text view is not being saved somewhere else in
3081 * persistent storage (such as in a content provider) so that if the
3082 * view is later thawed the user will not lose their data.
3083 *
3084 * @param freezesText Controls whether a frozen icicle should include the
3085 * entire text data: true to include it, false to not.
3086 *
3087 * @attr ref android.R.styleable#TextView_freezesText
3088 */
3089 @android.view.RemotableViewMethod
3090 public void setFreezesText(boolean freezesText) {
3091 mFreezesText = freezesText;
3092 }
3093
3094 /**
3095 * Return whether this text view is including its entire text contents
3096 * in frozen icicles.
3097 *
3098 * @return Returns true if text is included, false if it isn't.
3099 *
3100 * @see #setFreezesText
3101 */
3102 public boolean getFreezesText() {
3103 return mFreezesText;
3104 }
3105
3106 ///////////////////////////////////////////////////////////////////////////
3107
3108 /**
3109 * Sets the Factory used to create new Editables.
3110 */
3111 public final void setEditableFactory(Editable.Factory factory) {
3112 mEditableFactory = factory;
3113 setText(mText);
3114 }
3115
3116 /**
3117 * Sets the Factory used to create new Spannables.
3118 */
3119 public final void setSpannableFactory(Spannable.Factory factory) {
3120 mSpannableFactory = factory;
3121 setText(mText);
3122 }
3123
3124 /**
3125 * Sets the string value of the TextView. TextView <em>does not</em> accept
3126 * HTML-like formatting, which you can do with text strings in XML resource files.
3127 * To style your strings, attach android.text.style.* objects to a
3128 * {@link android.text.SpannableString SpannableString}, or see the
3129 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
Gilles Debunne21078e42011-08-02 10:22:35 -07003130 * Available Resource Types</a> documentation for an example of setting
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003131 * formatted text in the XML resource file.
3132 *
3133 * @attr ref android.R.styleable#TextView_text
3134 */
3135 @android.view.RemotableViewMethod
3136 public final void setText(CharSequence text) {
3137 setText(text, mBufferType);
3138 }
3139
3140 /**
3141 * Like {@link #setText(CharSequence)},
3142 * except that the cursor position (if any) is retained in the new text.
3143 *
3144 * @param text The new text to place in the text view.
3145 *
3146 * @see #setText(CharSequence)
3147 */
3148 @android.view.RemotableViewMethod
3149 public final void setTextKeepState(CharSequence text) {
3150 setTextKeepState(text, mBufferType);
3151 }
3152
3153 /**
3154 * Sets the text that this TextView is to display (see
3155 * {@link #setText(CharSequence)}) and also sets whether it is stored
3156 * in a styleable/spannable buffer and whether it is editable.
3157 *
3158 * @attr ref android.R.styleable#TextView_text
3159 * @attr ref android.R.styleable#TextView_bufferType
3160 */
3161 public void setText(CharSequence text, BufferType type) {
3162 setText(text, type, true, 0);
3163
3164 if (mCharWrapper != null) {
3165 mCharWrapper.mChars = null;
3166 }
3167 }
3168
3169 private void setText(CharSequence text, BufferType type,
3170 boolean notifyBefore, int oldlen) {
3171 if (text == null) {
3172 text = "";
3173 }
3174
Luca Zanoline0760452011-09-08 12:03:37 +01003175 // If suggestions are not enabled, remove the suggestion spans from the text
3176 if (!isSuggestionsEnabled()) {
3177 text = removeSuggestionSpans(text);
3178 }
3179
Romain Guy939151f2009-04-08 14:22:40 -07003180 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
3181
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003182 if (text instanceof Spanned &&
3183 ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
Adam Powell282e3772011-08-30 16:51:11 -07003184 if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
3185 setHorizontalFadingEdgeEnabled(true);
3186 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
3187 } else {
3188 setHorizontalFadingEdgeEnabled(false);
3189 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
3190 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003191 setEllipsize(TextUtils.TruncateAt.MARQUEE);
3192 }
3193
3194 int n = mFilters.length;
3195 for (int i = 0; i < n; i++) {
3196 CharSequence out = mFilters[i].filter(text, 0, text.length(),
3197 EMPTY_SPANNED, 0, 0);
3198 if (out != null) {
3199 text = out;
3200 }
3201 }
3202
3203 if (notifyBefore) {
3204 if (mText != null) {
3205 oldlen = mText.length();
3206 sendBeforeTextChanged(mText, 0, oldlen, text.length());
3207 } else {
3208 sendBeforeTextChanged("", 0, 0, text.length());
3209 }
3210 }
3211
3212 boolean needEditableForNotification = false;
3213
3214 if (mListeners != null && mListeners.size() != 0) {
3215 needEditableForNotification = true;
3216 }
3217
Gilles Debunne6435a562011-08-04 21:22:30 -07003218 if (type == BufferType.EDITABLE || mInput != null || needEditableForNotification) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003219 Editable t = mEditableFactory.newEditable(text);
3220 text = t;
3221 setFilters(t, mFilters);
3222 InputMethodManager imm = InputMethodManager.peekInstance();
3223 if (imm != null) imm.restartInput(this);
3224 } else if (type == BufferType.SPANNABLE || mMovement != null) {
3225 text = mSpannableFactory.newSpannable(text);
3226 } else if (!(text instanceof CharWrapper)) {
3227 text = TextUtils.stringOrSpannedString(text);
3228 }
3229
3230 if (mAutoLinkMask != 0) {
3231 Spannable s2;
3232
3233 if (type == BufferType.EDITABLE || text instanceof Spannable) {
3234 s2 = (Spannable) text;
3235 } else {
3236 s2 = mSpannableFactory.newSpannable(text);
3237 }
3238
3239 if (Linkify.addLinks(s2, mAutoLinkMask)) {
3240 text = s2;
3241 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
3242
3243 /*
3244 * We must go ahead and set the text before changing the
3245 * movement method, because setMovementMethod() may call
3246 * setText() again to try to upgrade the buffer type.
3247 */
3248 mText = text;
3249
Gilles Debunnecbcb3452010-12-17 15:31:02 -08003250 // Do not change the movement method for text that support text selection as it
3251 // would prevent an arbitrary cursor displacement.
Gilles Debunnebb588da2011-07-11 18:26:19 -07003252 if (mLinksClickable && !textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003253 setMovementMethod(LinkMovementMethod.getInstance());
3254 }
3255 }
3256 }
3257
3258 mBufferType = type;
3259 mText = text;
3260
Adam Powell7f8f79a2011-07-07 18:35:54 -07003261 if (mTransformation == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003262 mTransformed = text;
Adam Powell7f8f79a2011-07-07 18:35:54 -07003263 } else {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003264 mTransformed = mTransformation.getTransformation(text, this);
Adam Powell7f8f79a2011-07-07 18:35:54 -07003265 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003266
3267 final int textLength = text.length();
3268
Adam Powell7f8f79a2011-07-07 18:35:54 -07003269 if (text instanceof Spannable && !mAllowTransformationLengthChange) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003270 Spannable sp = (Spannable) text;
3271
3272 // Remove any ChangeWatchers that might have come
3273 // from other TextViews.
3274 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
3275 final int count = watchers.length;
3276 for (int i = 0; i < count; i++)
3277 sp.removeSpan(watchers[i]);
3278
3279 if (mChangeWatcher == null)
3280 mChangeWatcher = new ChangeWatcher();
3281
3282 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
3283 (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
3284
3285 if (mInput != null) {
3286 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
3287 }
3288
3289 if (mTransformation != null) {
3290 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003291 }
3292
3293 if (mMovement != null) {
3294 mMovement.initialize(this, (Spannable) text);
3295
3296 /*
3297 * Initializing the movement method will have set the
3298 * selection, so reset mSelectionMoved to keep that from
3299 * interfering with the normal on-focus selection-setting.
3300 */
3301 mSelectionMoved = false;
3302 }
3303 }
3304
3305 if (mLayout != null) {
3306 checkForRelayout();
3307 }
3308
3309 sendOnTextChanged(text, 0, oldlen, textLength);
3310 onTextChanged(text, 0, oldlen, textLength);
3311
3312 if (needEditableForNotification) {
3313 sendAfterTextChanged((Editable) text);
3314 }
Gilles Debunne05336272010-07-09 20:13:45 -07003315
Gilles Debunnebaaace52010-10-01 15:47:13 -07003316 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
Gilles Debunnef788a9f2010-07-22 10:17:23 -07003317 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003318 }
3319
3320 /**
3321 * Sets the TextView to display the specified slice of the specified
3322 * char array. You must promise that you will not change the contents
3323 * of the array except for right before another call to setText(),
3324 * since the TextView has no way to know that the text
3325 * has changed and that it needs to invalidate and re-layout.
3326 */
3327 public final void setText(char[] text, int start, int len) {
3328 int oldlen = 0;
3329
3330 if (start < 0 || len < 0 || start + len > text.length) {
3331 throw new IndexOutOfBoundsException(start + ", " + len);
3332 }
3333
3334 /*
3335 * We must do the before-notification here ourselves because if
3336 * the old text is a CharWrapper we destroy it before calling
3337 * into the normal path.
3338 */
3339 if (mText != null) {
3340 oldlen = mText.length();
3341 sendBeforeTextChanged(mText, 0, oldlen, len);
3342 } else {
3343 sendBeforeTextChanged("", 0, 0, len);
3344 }
3345
3346 if (mCharWrapper == null) {
3347 mCharWrapper = new CharWrapper(text, start, len);
3348 } else {
3349 mCharWrapper.set(text, start, len);
3350 }
3351
3352 setText(mCharWrapper, mBufferType, false, oldlen);
3353 }
3354
Gilles Debunne3bca69b2011-05-23 18:20:22 -07003355 private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003356 private char[] mChars;
3357 private int mStart, mLength;
3358
3359 public CharWrapper(char[] chars, int start, int len) {
3360 mChars = chars;
3361 mStart = start;
3362 mLength = len;
3363 }
3364
3365 /* package */ void set(char[] chars, int start, int len) {
3366 mChars = chars;
3367 mStart = start;
3368 mLength = len;
3369 }
3370
3371 public int length() {
3372 return mLength;
3373 }
3374
3375 public char charAt(int off) {
3376 return mChars[off + mStart];
3377 }
3378
Gilles Debunnee15b3582010-06-16 15:17:21 -07003379 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003380 public String toString() {
3381 return new String(mChars, mStart, mLength);
3382 }
3383
3384 public CharSequence subSequence(int start, int end) {
3385 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3386 throw new IndexOutOfBoundsException(start + ", " + end);
3387 }
3388
3389 return new String(mChars, start + mStart, end - start);
3390 }
3391
3392 public void getChars(int start, int end, char[] buf, int off) {
3393 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3394 throw new IndexOutOfBoundsException(start + ", " + end);
3395 }
3396
3397 System.arraycopy(mChars, start + mStart, buf, off, end - start);
3398 }
3399
3400 public void drawText(Canvas c, int start, int end,
3401 float x, float y, Paint p) {
3402 c.drawText(mChars, start + mStart, end - start, x, y, p);
3403 }
3404
Doug Feltf47d7402010-04-21 16:01:52 -07003405 public void drawTextRun(Canvas c, int start, int end,
Doug Felt0c702b82010-05-14 10:55:42 -07003406 int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
3407 int count = end - start;
3408 int contextCount = contextEnd - contextStart;
3409 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
3410 contextCount, x, y, flags, p);
Doug Feltf47d7402010-04-21 16:01:52 -07003411 }
3412
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003413 public float measureText(int start, int end, Paint p) {
3414 return p.measureText(mChars, start + mStart, end - start);
3415 }
3416
3417 public int getTextWidths(int start, int end, float[] widths, Paint p) {
3418 return p.getTextWidths(mChars, start + mStart, end - start, widths);
3419 }
Doug Felt0c702b82010-05-14 10:55:42 -07003420
3421 public float getTextRunAdvances(int start, int end, int contextStart,
3422 int contextEnd, int flags, float[] advances, int advancesIndex,
3423 Paint p) {
3424 int count = end - start;
3425 int contextCount = contextEnd - contextStart;
3426 return p.getTextRunAdvances(mChars, start + mStart, count,
3427 contextStart + mStart, contextCount, flags, advances,
3428 advancesIndex);
3429 }
3430
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003431 public float getTextRunAdvances(int start, int end, int contextStart,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003432 int contextEnd, int flags, float[] advances, int advancesIndex,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003433 Paint p, int reserved) {
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003434 int count = end - start;
3435 int contextCount = contextEnd - contextStart;
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003436 return p.getTextRunAdvances(mChars, start + mStart, count,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003437 contextStart + mStart, contextCount, flags, advances,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003438 advancesIndex, reserved);
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003439 }
3440
Doug Felt0c702b82010-05-14 10:55:42 -07003441 public int getTextRunCursor(int contextStart, int contextEnd, int flags,
3442 int offset, int cursorOpt, Paint p) {
3443 int contextCount = contextEnd - contextStart;
3444 return p.getTextRunCursor(mChars, contextStart + mStart,
3445 contextCount, flags, offset + mStart, cursorOpt);
3446 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003447 }
3448
3449 /**
3450 * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3451 * except that the cursor position (if any) is retained in the new text.
3452 *
3453 * @see #setText(CharSequence, android.widget.TextView.BufferType)
3454 */
3455 public final void setTextKeepState(CharSequence text, BufferType type) {
3456 int start = getSelectionStart();
3457 int end = getSelectionEnd();
3458 int len = text.length();
3459
3460 setText(text, type);
3461
3462 if (start >= 0 || end >= 0) {
3463 if (mText instanceof Spannable) {
3464 Selection.setSelection((Spannable) mText,
3465 Math.max(0, Math.min(start, len)),
3466 Math.max(0, Math.min(end, len)));
3467 }
3468 }
3469 }
3470
3471 @android.view.RemotableViewMethod
3472 public final void setText(int resid) {
3473 setText(getContext().getResources().getText(resid));
3474 }
3475
3476 public final void setText(int resid, BufferType type) {
3477 setText(getContext().getResources().getText(resid), type);
3478 }
3479
3480 /**
3481 * Sets the text to be displayed when the text of the TextView is empty.
3482 * Null means to use the normal empty text. The hint does not currently
3483 * participate in determining the size of the view.
3484 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003485 * @attr ref android.R.styleable#TextView_hint
3486 */
3487 @android.view.RemotableViewMethod
3488 public final void setHint(CharSequence hint) {
3489 mHint = TextUtils.stringOrSpannedString(hint);
3490
3491 if (mLayout != null) {
3492 checkForRelayout();
3493 }
3494
Romain Guy4dc4f732009-06-19 15:16:40 -07003495 if (mText.length() == 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003496 invalidate();
Romain Guy4dc4f732009-06-19 15:16:40 -07003497 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003498 }
3499
3500 /**
3501 * Sets the text to be displayed when the text of the TextView is empty,
3502 * from a resource.
3503 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003504 * @attr ref android.R.styleable#TextView_hint
3505 */
3506 @android.view.RemotableViewMethod
3507 public final void setHint(int resid) {
3508 setHint(getContext().getResources().getText(resid));
3509 }
3510
3511 /**
3512 * Returns the hint that is displayed when the text of the TextView
3513 * is empty.
3514 *
3515 * @attr ref android.R.styleable#TextView_hint
3516 */
3517 @ViewDebug.CapturedViewProperty
3518 public CharSequence getHint() {
3519 return mHint;
3520 }
3521
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003522 private static boolean isMultilineInputType(int type) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003523 return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3524 (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3525 }
3526
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003527 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003528 * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3529 * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3530 * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
3531 * then a soft keyboard will not be displayed for this text view.
3532 *
3533 * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3534 * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3535 * type.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003536 *
3537 * @see #getInputType()
3538 * @see #setRawInputType(int)
3539 * @see android.text.InputType
3540 * @attr ref android.R.styleable#TextView_inputType
3541 */
3542 public void setInputType(int type) {
Bjorn Bringertad8da912009-09-17 10:47:35 +01003543 final boolean wasPassword = isPasswordInputType(mInputType);
3544 final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003545 setInputType(type, false);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003546 final boolean isPassword = isPasswordInputType(type);
3547 final boolean isVisiblePassword = isVisiblePasswordInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003548 boolean forceUpdate = false;
3549 if (isPassword) {
3550 setTransformationMethod(PasswordTransformationMethod.getInstance());
3551 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003552 } else if (isVisiblePassword) {
Amith Yamasania8c0edb2009-09-27 16:51:21 -07003553 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3554 forceUpdate = true;
3555 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003556 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003557 } else if (wasPassword || wasVisiblePassword) {
3558 // not in password mode, clean up typeface and transformation
3559 setTypefaceByIndex(-1, -1);
3560 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3561 forceUpdate = true;
3562 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003563 }
3564
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003565 boolean singleLine = !isMultilineInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003566
3567 // We need to update the single line mode if it has changed or we
3568 // were previously in password mode.
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003569 if (mSingleLine != singleLine || forceUpdate) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003570 // Change single line mode, but only change the transformation if
3571 // we are not in password mode.
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003572 applySingleLine(singleLine, !isPassword, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003573 }
3574
Luca Zanoline0760452011-09-08 12:03:37 +01003575 if (!isSuggestionsEnabled()) {
3576 mText = removeSuggestionSpans(mText);
3577 }
3578
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003579 InputMethodManager imm = InputMethodManager.peekInstance();
3580 if (imm != null) imm.restartInput(this);
3581 }
3582
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07003583 /**
3584 * It would be better to rely on the input type for everything. A password inputType should have
3585 * a password transformation. We should hence use isPasswordInputType instead of this method.
3586 *
3587 * We should:
3588 * - Call setInputType in setKeyListener instead of changing the input type directly (which
3589 * would install the correct transformation).
3590 * - Refuse the installation of a non-password transformation in setTransformation if the input
3591 * type is password.
3592 *
3593 * However, this is like this for legacy reasons and we cannot break existing apps. This method
3594 * is useful since it matches what the user can see (obfuscated text or not).
3595 *
3596 * @return true if the current transformation method is of the password type.
3597 */
3598 private boolean hasPasswordTransformationMethod() {
3599 return mTransformation instanceof PasswordTransformationMethod;
3600 }
3601
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003602 private static boolean isPasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003603 final int variation =
3604 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003605 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003606 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3607 || variation
Ken Wakasa82d731a2010-12-24 23:42:41 +09003608 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3609 || variation
3610 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003611 }
3612
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003613 private static boolean isVisiblePasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003614 final int variation =
3615 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003616 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003617 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003618 }
3619
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003620 /**
3621 * Directly change the content type integer of the text view, without
3622 * modifying any other state.
3623 * @see #setInputType(int)
3624 * @see android.text.InputType
3625 * @attr ref android.R.styleable#TextView_inputType
3626 */
3627 public void setRawInputType(int type) {
3628 mInputType = type;
3629 }
3630
3631 private void setInputType(int type, boolean direct) {
3632 final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3633 KeyListener input;
3634 if (cls == EditorInfo.TYPE_CLASS_TEXT) {
Gilles Debunnee67b58a2010-08-31 15:55:31 -07003635 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003636 TextKeyListener.Capitalize cap;
3637 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3638 cap = TextKeyListener.Capitalize.CHARACTERS;
3639 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3640 cap = TextKeyListener.Capitalize.WORDS;
3641 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3642 cap = TextKeyListener.Capitalize.SENTENCES;
3643 } else {
3644 cap = TextKeyListener.Capitalize.NONE;
3645 }
3646 input = TextKeyListener.getInstance(autotext, cap);
3647 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3648 input = DigitsKeyListener.getInstance(
3649 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3650 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3651 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3652 switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3653 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3654 input = DateKeyListener.getInstance();
3655 break;
3656 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3657 input = TimeKeyListener.getInstance();
3658 break;
3659 default:
3660 input = DateTimeKeyListener.getInstance();
3661 break;
3662 }
3663 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3664 input = DialerKeyListener.getInstance();
3665 } else {
3666 input = TextKeyListener.getInstance();
3667 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003668 setRawInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003669 if (direct) mInput = input;
3670 else {
3671 setKeyListenerOnly(input);
3672 }
3673 }
3674
3675 /**
3676 * Get the type of the content.
3677 *
3678 * @see #setInputType(int)
3679 * @see android.text.InputType
3680 */
3681 public int getInputType() {
3682 return mInputType;
3683 }
3684
3685 /**
3686 * Change the editor type integer associated with the text view, which
3687 * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3688 * has focus.
3689 * @see #getImeOptions
3690 * @see android.view.inputmethod.EditorInfo
3691 * @attr ref android.R.styleable#TextView_imeOptions
3692 */
3693 public void setImeOptions(int imeOptions) {
3694 if (mInputContentType == null) {
3695 mInputContentType = new InputContentType();
3696 }
3697 mInputContentType.imeOptions = imeOptions;
3698 }
3699
3700 /**
3701 * Get the type of the IME editor.
3702 *
3703 * @see #setImeOptions(int)
3704 * @see android.view.inputmethod.EditorInfo
3705 */
3706 public int getImeOptions() {
3707 return mInputContentType != null
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003708 ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003709 }
3710
3711 /**
3712 * Change the custom IME action associated with the text view, which
3713 * will be reported to an IME with {@link EditorInfo#actionLabel}
3714 * and {@link EditorInfo#actionId} when it has focus.
3715 * @see #getImeActionLabel
3716 * @see #getImeActionId
3717 * @see android.view.inputmethod.EditorInfo
3718 * @attr ref android.R.styleable#TextView_imeActionLabel
3719 * @attr ref android.R.styleable#TextView_imeActionId
3720 */
3721 public void setImeActionLabel(CharSequence label, int actionId) {
3722 if (mInputContentType == null) {
3723 mInputContentType = new InputContentType();
3724 }
3725 mInputContentType.imeActionLabel = label;
3726 mInputContentType.imeActionId = actionId;
3727 }
3728
3729 /**
3730 * Get the IME action label previous set with {@link #setImeActionLabel}.
3731 *
3732 * @see #setImeActionLabel
3733 * @see android.view.inputmethod.EditorInfo
3734 */
3735 public CharSequence getImeActionLabel() {
3736 return mInputContentType != null
3737 ? mInputContentType.imeActionLabel : null;
3738 }
3739
3740 /**
3741 * Get the IME action ID previous set with {@link #setImeActionLabel}.
3742 *
3743 * @see #setImeActionLabel
3744 * @see android.view.inputmethod.EditorInfo
3745 */
3746 public int getImeActionId() {
3747 return mInputContentType != null
3748 ? mInputContentType.imeActionId : 0;
3749 }
3750
3751 /**
3752 * Set a special listener to be called when an action is performed
3753 * on the text view. This will be called when the enter key is pressed,
3754 * or when an action supplied to the IME is selected by the user. Setting
3755 * this means that the normal hard key event will not insert a newline
3756 * into the text view, even if it is multi-line; holding down the ALT
3757 * modifier will, however, allow the user to insert a newline character.
3758 */
3759 public void setOnEditorActionListener(OnEditorActionListener l) {
3760 if (mInputContentType == null) {
3761 mInputContentType = new InputContentType();
3762 }
3763 mInputContentType.onEditorActionListener = l;
3764 }
3765
3766 /**
3767 * Called when an attached input method calls
3768 * {@link InputConnection#performEditorAction(int)
3769 * InputConnection.performEditorAction()}
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003770 * for this text view. The default implementation will call your action
3771 * listener supplied to {@link #setOnEditorActionListener}, or perform
3772 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003773 * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3774 * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003775 * EditorInfo.IME_ACTION_DONE}.
3776 *
3777 * <p>For backwards compatibility, if no IME options have been set and the
3778 * text view would not normally advance focus on enter, then
3779 * the NEXT and DONE actions received here will be turned into an enter
3780 * key down/up pair to go through the normal key handling.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003781 *
3782 * @param actionCode The code of the action being performed.
3783 *
3784 * @see #setOnEditorActionListener
3785 */
3786 public void onEditorAction(int actionCode) {
3787 final InputContentType ict = mInputContentType;
3788 if (ict != null) {
3789 if (ict.onEditorActionListener != null) {
3790 if (ict.onEditorActionListener.onEditorAction(this,
3791 actionCode, null)) {
3792 return;
3793 }
3794 }
Gilles Debunne64794482011-11-30 15:45:28 -08003795
The Android Open Source Project4df24232009-03-05 14:34:35 -08003796 // This is the handling for some default action.
3797 // Note that for backwards compatibility we don't do this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003798 // default handling if explicit ime options have not been given,
The Android Open Source Project10592532009-03-18 17:39:46 -07003799 // instead turning this into the normal enter key codes that an
The Android Open Source Project4df24232009-03-05 14:34:35 -08003800 // app may be expecting.
3801 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003802 View v = focusSearch(FOCUS_FORWARD);
The Android Open Source Project4df24232009-03-05 14:34:35 -08003803 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003804 if (!v.requestFocus(FOCUS_FORWARD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003805 throw new IllegalStateException("focus search returned a view " +
3806 "that wasn't able to take focus!");
3807 }
3808 }
3809 return;
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003810
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003811 } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003812 View v = focusSearch(FOCUS_BACKWARD);
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003813 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003814 if (!v.requestFocus(FOCUS_BACKWARD)) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003815 throw new IllegalStateException("focus search returned a view " +
3816 "that wasn't able to take focus!");
3817 }
3818 }
3819 return;
3820
The Android Open Source Project4df24232009-03-05 14:34:35 -08003821 } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3822 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08003823 if (imm != null && imm.isActive(this)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003824 imm.hideSoftInputFromWindow(getWindowToken(), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003825 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003826 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003827 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003828 }
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003829
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003830 Handler h = getHandler();
The Android Open Source Project10592532009-03-18 17:39:46 -07003831 if (h != null) {
3832 long eventTime = SystemClock.uptimeMillis();
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003833 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003834 new KeyEvent(eventTime, eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003835 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3836 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003837 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3838 | KeyEvent.FLAG_EDITOR_ACTION)));
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003839 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003840 new KeyEvent(SystemClock.uptimeMillis(), eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003841 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3842 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003843 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3844 | KeyEvent.FLAG_EDITOR_ACTION)));
3845 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003846 }
Gilles Debunne64794482011-11-30 15:45:28 -08003847
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003848 /**
3849 * Set the private content type of the text, which is the
3850 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3851 * field that will be filled in when creating an input connection.
3852 *
3853 * @see #getPrivateImeOptions()
3854 * @see EditorInfo#privateImeOptions
3855 * @attr ref android.R.styleable#TextView_privateImeOptions
3856 */
3857 public void setPrivateImeOptions(String type) {
3858 if (mInputContentType == null) mInputContentType = new InputContentType();
3859 mInputContentType.privateImeOptions = type;
3860 }
3861
3862 /**
3863 * Get the private type of the content.
3864 *
3865 * @see #setPrivateImeOptions(String)
3866 * @see EditorInfo#privateImeOptions
3867 */
3868 public String getPrivateImeOptions() {
3869 return mInputContentType != null
3870 ? mInputContentType.privateImeOptions : null;
3871 }
3872
3873 /**
3874 * Set the extra input data of the text, which is the
3875 * {@link EditorInfo#extras TextBoxAttribute.extras}
3876 * Bundle that will be filled in when creating an input connection. The
3877 * given integer is the resource ID of an XML resource holding an
3878 * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3879 *
3880 * @see #getInputExtras(boolean)
3881 * @see EditorInfo#extras
3882 * @attr ref android.R.styleable#TextView_editorExtras
3883 */
3884 public void setInputExtras(int xmlResId)
3885 throws XmlPullParserException, IOException {
3886 XmlResourceParser parser = getResources().getXml(xmlResId);
3887 if (mInputContentType == null) mInputContentType = new InputContentType();
3888 mInputContentType.extras = new Bundle();
3889 getResources().parseBundleExtras(parser, mInputContentType.extras);
3890 }
3891
3892 /**
3893 * Retrieve the input extras currently associated with the text view, which
3894 * can be viewed as well as modified.
3895 *
3896 * @param create If true, the extras will be created if they don't already
3897 * exist. Otherwise, null will be returned if none have been created.
Gilles Debunnee15b3582010-06-16 15:17:21 -07003898 * @see #setInputExtras(int)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003899 * @see EditorInfo#extras
3900 * @attr ref android.R.styleable#TextView_editorExtras
3901 */
3902 public Bundle getInputExtras(boolean create) {
3903 if (mInputContentType == null) {
3904 if (!create) return null;
3905 mInputContentType = new InputContentType();
3906 }
3907 if (mInputContentType.extras == null) {
3908 if (!create) return null;
3909 mInputContentType.extras = new Bundle();
3910 }
3911 return mInputContentType.extras;
3912 }
3913
3914 /**
3915 * Returns the error message that was set to be displayed with
3916 * {@link #setError}, or <code>null</code> if no error was set
3917 * or if it the error was cleared by the widget after user input.
3918 */
3919 public CharSequence getError() {
3920 return mError;
3921 }
3922
3923 /**
3924 * Sets the right-hand compound drawable of the TextView to the "error"
3925 * icon and sets an error message that will be displayed in a popup when
3926 * the TextView has focus. The icon and error message will be reset to
3927 * null when any key events cause changes to the TextView's text. If the
3928 * <code>error</code> is <code>null</code>, the error message and icon
3929 * will be cleared.
3930 */
3931 @android.view.RemotableViewMethod
3932 public void setError(CharSequence error) {
3933 if (error == null) {
3934 setError(null, null);
3935 } else {
3936 Drawable dr = getContext().getResources().
Gilles Debunnea85467b2011-01-19 16:53:31 -08003937 getDrawable(com.android.internal.R.drawable.indicator_input_error);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003938
3939 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3940 setError(error, dr);
3941 }
3942 }
3943
3944 /**
3945 * Sets the right-hand compound drawable of the TextView to the specified
3946 * icon and sets an error message that will be displayed in a popup when
3947 * the TextView has focus. The icon and error message will be reset to
3948 * null when any key events cause changes to the TextView's text. The
3949 * drawable must already have had {@link Drawable#setBounds} set on it.
3950 * If the <code>error</code> is <code>null</code>, the error message will
3951 * be cleared (and you should provide a <code>null</code> icon as well).
3952 */
3953 public void setError(CharSequence error, Drawable icon) {
3954 error = TextUtils.stringOrSpannedString(error);
3955
3956 mError = error;
3957 mErrorWasChanged = true;
3958 final Drawables dr = mDrawables;
3959 if (dr != null) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07003960 switch (getResolvedLayoutDirection()) {
3961 default:
3962 case LAYOUT_DIRECTION_LTR:
3963 setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
3964 dr.mDrawableBottom);
3965 break;
3966 case LAYOUT_DIRECTION_RTL:
3967 setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
3968 dr.mDrawableBottom);
3969 break;
3970 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003971 } else {
3972 setCompoundDrawables(null, null, icon, null);
3973 }
3974
3975 if (error == null) {
3976 if (mPopup != null) {
3977 if (mPopup.isShowing()) {
3978 mPopup.dismiss();
3979 }
3980
3981 mPopup = null;
3982 }
3983 } else {
3984 if (isFocused()) {
3985 showError();
3986 }
3987 }
3988 }
3989
3990 private void showError() {
3991 if (getWindowToken() == null) {
3992 mShowErrorAfterAttach = true;
3993 return;
3994 }
3995
3996 if (mPopup == null) {
3997 LayoutInflater inflater = LayoutInflater.from(getContext());
Gilles Debunnea85467b2011-01-19 16:53:31 -08003998 final TextView err = (TextView) inflater.inflate(
3999 com.android.internal.R.layout.textview_hint, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004000
Romain Guy9bc9fa12009-07-21 16:57:29 -07004001 final float scale = getResources().getDisplayMetrics().density;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004002 mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004003 mPopup.setFocusable(false);
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -07004004 // The user is entering text, so the input method is needed. We
4005 // don't want the popup to be displayed on top of it.
4006 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004007 }
4008
4009 TextView tv = (TextView) mPopup.getContentView();
4010 chooseSize(mPopup, mError, tv);
4011 tv.setText(mError);
4012
4013 mPopup.showAsDropDown(this, getErrorX(), getErrorY());
The Android Open Source Project10592532009-03-18 17:39:46 -07004014 mPopup.fixDirection(mPopup.isAboveAnchor());
4015 }
4016
4017 private static class ErrorPopup extends PopupWindow {
4018 private boolean mAbove = false;
Gilles Debunnee15b3582010-06-16 15:17:21 -07004019 private final TextView mView;
Gilles Debunne5f059e42011-01-12 17:49:12 -08004020 private int mPopupInlineErrorBackgroundId = 0;
4021 private int mPopupInlineErrorAboveBackgroundId = 0;
The Android Open Source Project10592532009-03-18 17:39:46 -07004022
4023 ErrorPopup(TextView v, int width, int height) {
4024 super(v, width, height);
4025 mView = v;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004026 // Make sure the TextView has a background set as it will be used the first time it is
4027 // shown and positionned. Initialized with below background, which should have
4028 // dimensions identical to the above version for this to work (and is more likely).
4029 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4030 com.android.internal.R.styleable.Theme_errorMessageBackground);
4031 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
The Android Open Source Project10592532009-03-18 17:39:46 -07004032 }
4033
4034 void fixDirection(boolean above) {
4035 mAbove = above;
4036
4037 if (above) {
Gilles Debunne5f059e42011-01-12 17:49:12 -08004038 mPopupInlineErrorAboveBackgroundId =
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004039 getResourceId(mPopupInlineErrorAboveBackgroundId,
4040 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07004041 } else {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004042 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4043 com.android.internal.R.styleable.Theme_errorMessageBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07004044 }
Gilles Debunne5f059e42011-01-12 17:49:12 -08004045
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004046 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4047 mPopupInlineErrorBackgroundId);
Gilles Debunne5f059e42011-01-12 17:49:12 -08004048 }
4049
4050 private int getResourceId(int currentId, int index) {
4051 if (currentId == 0) {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004052 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4053 R.styleable.Theme);
Gilles Debunne5f059e42011-01-12 17:49:12 -08004054 currentId = styledAttributes.getResourceId(index, 0);
4055 styledAttributes.recycle();
4056 }
4057 return currentId;
The Android Open Source Project10592532009-03-18 17:39:46 -07004058 }
4059
4060 @Override
4061 public void update(int x, int y, int w, int h, boolean force) {
4062 super.update(x, y, w, h, force);
4063
4064 boolean above = isAboveAnchor();
4065 if (above != mAbove) {
4066 fixDirection(above);
4067 }
4068 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004069 }
4070
4071 /**
4072 * Returns the Y offset to make the pointy top of the error point
4073 * at the middle of the error icon.
4074 */
4075 private int getErrorX() {
4076 /*
4077 * The "25" is the distance between the point and the right edge
4078 * of the background
4079 */
Romain Guy9bc9fa12009-07-21 16:57:29 -07004080 final float scale = getResources().getDisplayMetrics().density;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004081
4082 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004083 return getWidth() - mPopup.getWidth() - getPaddingRight() -
4084 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004085 }
4086
4087 /**
4088 * Returns the Y offset to make the pointy top of the error point
4089 * at the bottom of the error icon.
4090 */
4091 private int getErrorY() {
4092 /*
4093 * Compound, not extended, because the icon is not clipped
4094 * if the text height is smaller.
4095 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004096 final int compoundPaddingTop = getCompoundPaddingTop();
4097 int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004098
4099 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004100 int icontop = compoundPaddingTop +
4101 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004102
4103 /*
4104 * The "2" is the distance between the point and the top edge
4105 * of the background.
4106 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004107 final float scale = getResources().getDisplayMetrics().density;
4108 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
4109 (int) (2 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004110 }
4111
4112 private void hideError() {
4113 if (mPopup != null) {
4114 if (mPopup.isShowing()) {
4115 mPopup.dismiss();
4116 }
4117 }
4118
4119 mShowErrorAfterAttach = false;
4120 }
4121
4122 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
4123 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
4124 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
4125
Fabrice Di Meglioe4231462011-09-08 18:15:50 -07004126 int defaultWidthInPixels = getResources().getDimensionPixelSize(
4127 com.android.internal.R.dimen.textview_error_popup_default_width);
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004128 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004129 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
4130 float max = 0;
4131 for (int i = 0; i < l.getLineCount(); i++) {
4132 max = Math.max(max, l.getLineWidth(i));
4133 }
4134
4135 /*
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004136 * Now set the popup size to be big enough for the text plus the border capped
4137 * to DEFAULT_MAX_POPUP_WIDTH
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004138 */
4139 pop.setWidth(wid + (int) Math.ceil(max));
4140 pop.setHeight(ht + l.getHeight());
4141 }
4142
4143
4144 @Override
4145 protected boolean setFrame(int l, int t, int r, int b) {
4146 boolean result = super.setFrame(l, t, r, b);
4147
4148 if (mPopup != null) {
4149 TextView tv = (TextView) mPopup.getContentView();
4150 chooseSize(mPopup, mError, tv);
Eric Fischerfa0d2532009-09-17 17:01:59 -07004151 mPopup.update(this, getErrorX(), getErrorY(),
4152 mPopup.getWidth(), mPopup.getHeight());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004153 }
4154
Romain Guy986003d2009-03-25 17:42:35 -07004155 restartMarqueeIfNeeded();
4156
4157 return result;
4158 }
4159
4160 private void restartMarqueeIfNeeded() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004161 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4162 mRestartMarquee = false;
4163 startMarquee();
4164 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004165 }
4166
4167 /**
4168 * Sets the list of input filters that will be used if the buffer is
4169 * Editable. Has no effect otherwise.
4170 *
4171 * @attr ref android.R.styleable#TextView_maxLength
4172 */
4173 public void setFilters(InputFilter[] filters) {
4174 if (filters == null) {
4175 throw new IllegalArgumentException();
4176 }
4177
4178 mFilters = filters;
4179
4180 if (mText instanceof Editable) {
4181 setFilters((Editable) mText, filters);
4182 }
4183 }
4184
4185 /**
4186 * Sets the list of input filters on the specified Editable,
4187 * and includes mInput in the list if it is an InputFilter.
4188 */
4189 private void setFilters(Editable e, InputFilter[] filters) {
4190 if (mInput instanceof InputFilter) {
4191 InputFilter[] nf = new InputFilter[filters.length + 1];
4192
4193 System.arraycopy(filters, 0, nf, 0, filters.length);
4194 nf[filters.length] = (InputFilter) mInput;
4195
4196 e.setFilters(nf);
4197 } else {
4198 e.setFilters(filters);
4199 }
4200 }
4201
4202 /**
4203 * Returns the current list of input filters.
4204 */
4205 public InputFilter[] getFilters() {
4206 return mFilters;
4207 }
4208
4209 /////////////////////////////////////////////////////////////////////////
4210
4211 private int getVerticalOffset(boolean forceNormal) {
4212 int voffset = 0;
4213 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4214
4215 Layout l = mLayout;
4216 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4217 l = mHintLayout;
4218 }
4219
4220 if (gravity != Gravity.TOP) {
4221 int boxht;
4222
4223 if (l == mHintLayout) {
4224 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4225 getCompoundPaddingBottom();
4226 } else {
4227 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4228 getExtendedPaddingBottom();
4229 }
4230 int textht = l.getHeight();
4231
4232 if (textht < boxht) {
4233 if (gravity == Gravity.BOTTOM)
4234 voffset = boxht - textht;
4235 else // (gravity == Gravity.CENTER_VERTICAL)
4236 voffset = (boxht - textht) >> 1;
4237 }
4238 }
4239 return voffset;
4240 }
4241
4242 private int getBottomVerticalOffset(boolean forceNormal) {
4243 int voffset = 0;
4244 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4245
4246 Layout l = mLayout;
4247 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4248 l = mHintLayout;
4249 }
4250
4251 if (gravity != Gravity.BOTTOM) {
4252 int boxht;
4253
4254 if (l == mHintLayout) {
4255 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4256 getCompoundPaddingBottom();
4257 } else {
4258 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4259 getExtendedPaddingBottom();
4260 }
4261 int textht = l.getHeight();
4262
4263 if (textht < boxht) {
4264 if (gravity == Gravity.TOP)
4265 voffset = boxht - textht;
4266 else // (gravity == Gravity.CENTER_VERTICAL)
4267 voffset = (boxht - textht) >> 1;
4268 }
4269 }
4270 return voffset;
4271 }
4272
4273 private void invalidateCursorPath() {
4274 if (mHighlightPathBogus) {
4275 invalidateCursor();
4276 } else {
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004277 final int horizontalPadding = getCompoundPaddingLeft();
4278 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004279
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004280 if (mCursorCount == 0) {
4281 synchronized (sTempRect) {
4282 /*
4283 * The reason for this concern about the thickness of the
4284 * cursor and doing the floor/ceil on the coordinates is that
4285 * some EditTexts (notably textfields in the Browser) have
4286 * anti-aliased text where not all the characters are
4287 * necessarily at integer-multiple locations. This should
4288 * make sure the entire cursor gets invalidated instead of
4289 * sometimes missing half a pixel.
4290 */
4291 float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
4292 if (thick < 1.0f) {
4293 thick = 1.0f;
4294 }
4295
4296 thick /= 2.0f;
4297
4298 mHighlightPath.computeBounds(sTempRect, false);
4299
4300 invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
4301 (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
4302 (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
4303 (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004304 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004305 } else {
4306 for (int i = 0; i < mCursorCount; i++) {
4307 Rect bounds = mCursorDrawable[i].getBounds();
4308 invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
4309 bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
4310 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004311 }
4312 }
4313 }
4314
4315 private void invalidateCursor() {
Gilles Debunne05336272010-07-09 20:13:45 -07004316 int where = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004317
4318 invalidateCursor(where, where, where);
4319 }
4320
4321 private void invalidateCursor(int a, int b, int c) {
Gilles Debunne8615ac92011-11-29 15:25:03 -08004322 if (a >= 0 || b >= 0 || c >= 0) {
4323 int start = Math.min(Math.min(a, b), c);
4324 int end = Math.max(Math.max(a, b), c);
4325 invalidateRegion(start, end);
4326 }
4327 }
4328
4329 /**
4330 * Invalidates the region of text enclosed between the start and end text offsets.
4331 *
4332 * @hide
4333 */
4334 void invalidateRegion(int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004335 if (mLayout == null) {
4336 invalidate();
4337 } else {
Gilles Debunne8615ac92011-11-29 15:25:03 -08004338 int lineStart = mLayout.getLineForOffset(start);
4339 int top = mLayout.getLineTop(lineStart);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004340
4341 // This is ridiculous, but the descent from the line above
4342 // can hang down into the line we really want to redraw,
4343 // so we have to invalidate part of the line above to make
4344 // sure everything that needs to be redrawn really is.
4345 // (But not the whole line above, because that would cause
4346 // the same problem with the descenders on the line above it!)
Gilles Debunne8615ac92011-11-29 15:25:03 -08004347 if (lineStart > 0) {
4348 top -= mLayout.getLineDescent(lineStart - 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004349 }
4350
Gilles Debunne8615ac92011-11-29 15:25:03 -08004351 int lineEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004352
Gilles Debunne8615ac92011-11-29 15:25:03 -08004353 if (start == end)
4354 lineEnd = lineStart;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004355 else
Gilles Debunne8615ac92011-11-29 15:25:03 -08004356 lineEnd = mLayout.getLineForOffset(end);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004357
Gilles Debunne8615ac92011-11-29 15:25:03 -08004358 int bottom = mLayout.getLineBottom(lineEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004359
Gilles Debunne8615ac92011-11-29 15:25:03 -08004360 final int compoundPaddingLeft = getCompoundPaddingLeft();
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004361 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
Gilles Debunne8615ac92011-11-29 15:25:03 -08004362
4363 int left, right;
4364 if (lineStart == lineEnd) {
4365 left = (int) mLayout.getPrimaryHorizontal(start);
4366 right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
4367 left += compoundPaddingLeft;
4368 right += compoundPaddingLeft;
4369 } else {
4370 // Rectangle bounding box when the region spans several lines
4371 left = compoundPaddingLeft;
4372 right = getWidth() - getCompoundPaddingRight();
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004373 }
4374
Gilles Debunne8615ac92011-11-29 15:25:03 -08004375 invalidate(mScrollX + left, verticalPadding + top,
4376 mScrollX + right, verticalPadding + bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004377 }
4378 }
4379
4380 private void registerForPreDraw() {
4381 final ViewTreeObserver observer = getViewTreeObserver();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004382
4383 if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
4384 observer.addOnPreDrawListener(this);
4385 mPreDrawState = PREDRAW_PENDING;
4386 } else if (mPreDrawState == PREDRAW_DONE) {
4387 mPreDrawState = PREDRAW_PENDING;
4388 }
4389
4390 // else state is PREDRAW_PENDING, so keep waiting.
4391 }
4392
4393 /**
4394 * {@inheritDoc}
4395 */
4396 public boolean onPreDraw() {
4397 if (mPreDrawState != PREDRAW_PENDING) {
4398 return true;
4399 }
4400
4401 if (mLayout == null) {
4402 assumeLayout();
4403 }
4404
4405 boolean changed = false;
4406
4407 if (mMovement != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07004408 /* This code also provides auto-scrolling when a cursor is moved using a
4409 * CursorController (insertion point or selection limits).
4410 * For selection, ensure start or end is visible depending on controller's state.
4411 */
4412 int curs = getSelectionEnd();
Gilles Debunnee587d832010-11-23 20:20:11 -08004413 // Do not create the controller if it is not already created.
4414 if (mSelectionModifierCursorController != null &&
4415 mSelectionModifierCursorController.isSelectionStartDragged()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07004416 curs = getSelectionStart();
Gilles Debunne05336272010-07-09 20:13:45 -07004417 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004418
4419 /*
4420 * TODO: This should really only keep the end in view if
4421 * it already was before the text changed. I'm not sure
4422 * of a good way to tell from here if it was.
4423 */
4424 if (curs < 0 &&
4425 (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
4426 curs = mText.length();
4427 }
4428
4429 if (curs >= 0) {
4430 changed = bringPointIntoView(curs);
4431 }
4432 } else {
4433 changed = bringTextIntoView();
4434 }
4435
Gilles Debunne64e54a62010-09-07 19:07:17 -07004436 // This has to be checked here since:
4437 // - onFocusChanged cannot start it when focus is given to a view with selected text (after
4438 // a screen rotation) since layout is not yet initialized at that point.
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08004439 if (mCreatedWithASelection) {
4440 startSelectionActionMode();
4441 mCreatedWithASelection = false;
4442 }
4443
4444 // Phone specific code (there is no ExtractEditText on tablets).
4445 // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
4446 // not be set. Do the test here instead.
4447 if (this instanceof ExtractEditText && hasSelection()) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004448 startSelectionActionMode();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004449 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07004450
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004451 mPreDrawState = PREDRAW_DONE;
4452 return !changed;
4453 }
4454
4455 @Override
4456 protected void onAttachedToWindow() {
4457 super.onAttachedToWindow();
4458
4459 mTemporaryDetach = false;
4460
4461 if (mShowErrorAfterAttach) {
4462 showError();
4463 mShowErrorAfterAttach = false;
4464 }
Adam Powell624380a2010-10-02 18:12:02 -07004465
4466 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004467 // No need to create the controller.
4468 // The get method will add the listener on controller creation.
4469 if (mInsertionPointCursorController != null) {
4470 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4471 }
4472 if (mSelectionModifierCursorController != null) {
4473 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell624380a2010-10-02 18:12:02 -07004474 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004475
4476 // Resolve drawables as the layout direction has been resolved
4477 resolveDrawables();
Gilles Debunnec115fa02011-12-07 13:38:31 -08004478
4479 updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004480 }
4481
4482 @Override
4483 protected void onDetachedFromWindow() {
4484 super.onDetachedFromWindow();
4485
Adam Powell624380a2010-10-02 18:12:02 -07004486 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004487 if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4488 observer.removeOnPreDrawListener(this);
4489 mPreDrawState = PREDRAW_NOT_REGISTERED;
4490 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004491
4492 if (mError != null) {
4493 hideError();
4494 }
Adam Powellba0a2c32010-09-28 17:41:23 -07004495
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004496 if (mBlink != null) {
Gilles Debunne3d010062011-02-18 14:16:41 -08004497 mBlink.removeCallbacks(mBlink);
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004498 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08004499
4500 if (mInsertionPointCursorController != null) {
4501 mInsertionPointCursorController.onDetached();
4502 }
4503
4504 if (mSelectionModifierCursorController != null) {
4505 mSelectionModifierCursorController.onDetached();
4506 }
4507
Adam Powellba0a2c32010-09-28 17:41:23 -07004508 hideControllers();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004509
4510 resetResolvedDrawables();
Gilles Debunne186aaf92011-09-16 14:26:12 -07004511
4512 if (mSpellChecker != null) {
4513 mSpellChecker.closeSession();
Gilles Debunne186aaf92011-09-16 14:26:12 -07004514 // Forces the creation of a new SpellChecker next time this window is created.
4515 // Will handle the cases where the settings has been changed in the meantime.
4516 mSpellChecker = null;
4517 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004518 }
4519
4520 @Override
4521 protected boolean isPaddingOffsetRequired() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004522 return mShadowRadius != 0 || mDrawables != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004523 }
4524
4525 @Override
4526 protected int getLeftPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004527 return getCompoundPaddingLeft() - mPaddingLeft +
4528 (int) Math.min(0, mShadowDx - mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004529 }
4530
4531 @Override
4532 protected int getTopPaddingOffset() {
4533 return (int) Math.min(0, mShadowDy - mShadowRadius);
4534 }
4535
4536 @Override
4537 protected int getBottomPaddingOffset() {
4538 return (int) Math.max(0, mShadowDy + mShadowRadius);
4539 }
4540
4541 @Override
4542 protected int getRightPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004543 return -(getCompoundPaddingRight() - mPaddingRight) +
4544 (int) Math.max(0, mShadowDx + mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004545 }
4546
4547 @Override
4548 protected boolean verifyDrawable(Drawable who) {
4549 final boolean verified = super.verifyDrawable(who);
4550 if (!verified && mDrawables != null) {
4551 return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004552 who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom ||
4553 who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004554 }
4555 return verified;
4556 }
4557
4558 @Override
Dianne Hackborne2136772010-11-04 15:08:59 -07004559 public void jumpDrawablesToCurrentState() {
4560 super.jumpDrawablesToCurrentState();
4561 if (mDrawables != null) {
4562 if (mDrawables.mDrawableLeft != null) {
4563 mDrawables.mDrawableLeft.jumpToCurrentState();
4564 }
4565 if (mDrawables.mDrawableTop != null) {
4566 mDrawables.mDrawableTop.jumpToCurrentState();
4567 }
4568 if (mDrawables.mDrawableRight != null) {
4569 mDrawables.mDrawableRight.jumpToCurrentState();
4570 }
4571 if (mDrawables.mDrawableBottom != null) {
4572 mDrawables.mDrawableBottom.jumpToCurrentState();
4573 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004574 if (mDrawables.mDrawableStart != null) {
4575 mDrawables.mDrawableStart.jumpToCurrentState();
4576 }
4577 if (mDrawables.mDrawableEnd != null) {
4578 mDrawables.mDrawableEnd.jumpToCurrentState();
4579 }
Dianne Hackborne2136772010-11-04 15:08:59 -07004580 }
4581 }
4582
4583 @Override
Romain Guy3c77d392009-05-20 11:26:50 -07004584 public void invalidateDrawable(Drawable drawable) {
4585 if (verifyDrawable(drawable)) {
4586 final Rect dirty = drawable.getBounds();
4587 int scrollX = mScrollX;
4588 int scrollY = mScrollY;
4589
4590 // IMPORTANT: The coordinates below are based on the coordinates computed
4591 // for each compound drawable in onDraw(). Make sure to update each section
4592 // accordingly.
4593 final TextView.Drawables drawables = mDrawables;
Romain Guya6cd4e02009-05-20 15:09:21 -07004594 if (drawables != null) {
4595 if (drawable == drawables.mDrawableLeft) {
4596 final int compoundPaddingTop = getCompoundPaddingTop();
4597 final int compoundPaddingBottom = getCompoundPaddingBottom();
4598 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004599
Romain Guya6cd4e02009-05-20 15:09:21 -07004600 scrollX += mPaddingLeft;
4601 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4602 } else if (drawable == drawables.mDrawableRight) {
4603 final int compoundPaddingTop = getCompoundPaddingTop();
4604 final int compoundPaddingBottom = getCompoundPaddingBottom();
4605 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004606
Romain Guya6cd4e02009-05-20 15:09:21 -07004607 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4608 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4609 } else if (drawable == drawables.mDrawableTop) {
4610 final int compoundPaddingLeft = getCompoundPaddingLeft();
4611 final int compoundPaddingRight = getCompoundPaddingRight();
4612 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004613
Romain Guya6cd4e02009-05-20 15:09:21 -07004614 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4615 scrollY += mPaddingTop;
4616 } else if (drawable == drawables.mDrawableBottom) {
4617 final int compoundPaddingLeft = getCompoundPaddingLeft();
4618 final int compoundPaddingRight = getCompoundPaddingRight();
4619 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004620
Romain Guya6cd4e02009-05-20 15:09:21 -07004621 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4622 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4623 }
Romain Guy3c77d392009-05-20 11:26:50 -07004624 }
4625
4626 invalidate(dirty.left + scrollX, dirty.top + scrollY,
4627 dirty.right + scrollX, dirty.bottom + scrollY);
4628 }
4629 }
4630
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004631 /**
4632 * @hide
4633 */
Romain Guy3c77d392009-05-20 11:26:50 -07004634 @Override
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004635 public int getResolvedLayoutDirection(Drawable who) {
4636 if (who == null) return View.LAYOUT_DIRECTION_LTR;
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004637 if (mDrawables != null) {
4638 final Drawables drawables = mDrawables;
4639 if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004640 who == drawables.mDrawableTop || who == drawables.mDrawableBottom ||
4641 who == drawables.mDrawableStart || who == drawables.mDrawableEnd) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004642 return getResolvedLayoutDirection();
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004643 }
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004644 }
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004645 return super.getResolvedLayoutDirection(who);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004646 }
4647
4648 @Override
Romain Guyc4d8eb62010-08-18 20:48:33 -07004649 protected boolean onSetAlpha(int alpha) {
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004650 // Alpha is supported if and only if the drawing can be done in one pass.
4651 // TODO text with spans with a background color currently do not respect this alpha.
4652 if (getBackground() == null) {
Romain Guyc4d8eb62010-08-18 20:48:33 -07004653 mCurrentAlpha = alpha;
4654 final Drawables dr = mDrawables;
4655 if (dr != null) {
Michael Jurka406f0522010-09-15 18:48:48 -07004656 if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4657 if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4658 if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4659 if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004660 if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha);
4661 if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha);
Romain Guyc4d8eb62010-08-18 20:48:33 -07004662 }
4663 return true;
4664 }
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004665
4666 mCurrentAlpha = 255;
Romain Guyc4d8eb62010-08-18 20:48:33 -07004667 return false;
4668 }
4669
Gilles Debunne86b9c782010-11-11 10:43:48 -08004670 /**
4671 * When a TextView is used to display a useful piece of information to the user (such as a
4672 * contact's address), it should be made selectable, so that the user can select and copy this
4673 * content.
4674 *
4675 * Use {@link #setTextIsSelectable(boolean)} or the
4676 * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
Gilles Debunnee12f9992010-12-17 11:04:55 -08004677 * selectable (text is not selectable by default).
Gilles Debunne6f100f32010-12-13 18:04:20 -08004678 *
Gilles Debunnebb588da2011-07-11 18:26:19 -07004679 * Note that this method simply returns the state of this flag. Although this flag has to be set
4680 * in order to select text in non-editable TextView, the content of an {@link EditText} can
4681 * always be selected, independently of the value of this flag.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004682 *
4683 * @return True if the text displayed in this TextView can be selected by the user.
4684 *
4685 * @attr ref android.R.styleable#TextView_textIsSelectable
4686 */
4687 public boolean isTextSelectable() {
4688 return mTextIsSelectable;
4689 }
4690
4691 /**
4692 * Sets whether or not (default) the content of this view is selectable by the user.
Gilles Debunne6f100f32010-12-13 18:04:20 -08004693 *
Gilles Debunnee12f9992010-12-17 11:04:55 -08004694 * Note that this methods affect the {@link #setFocusable(boolean)},
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004695 * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4696 * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4697 * customized.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004698 *
4699 * See {@link #isTextSelectable} for details.
4700 *
4701 * @param selectable Whether or not the content of this TextView should be selectable.
4702 */
4703 public void setTextIsSelectable(boolean selectable) {
4704 if (mTextIsSelectable == selectable) return;
4705
4706 mTextIsSelectable = selectable;
4707
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004708 setFocusableInTouchMode(selectable);
Gilles Debunne86b9c782010-11-11 10:43:48 -08004709 setFocusable(selectable);
4710 setClickable(selectable);
4711 setLongClickable(selectable);
4712
4713 // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4714
4715 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4716 setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4717
4718 // Called by setText above, but safer in case of future code changes
4719 prepareCursorControllers();
4720 }
4721
4722 @Override
4723 protected int[] onCreateDrawableState(int extraSpace) {
Gilles Debunnefb817032011-01-13 13:52:49 -08004724 final int[] drawableState;
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004725
Gilles Debunnefb817032011-01-13 13:52:49 -08004726 if (mSingleLine) {
4727 drawableState = super.onCreateDrawableState(extraSpace);
4728 } else {
4729 drawableState = super.onCreateDrawableState(extraSpace + 1);
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004730 mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4731 }
4732
Gilles Debunne86b9c782010-11-11 10:43:48 -08004733 if (mTextIsSelectable) {
4734 // Disable pressed state, which was introduced when TextView was made clickable.
4735 // Prevents text color change.
4736 // setClickable(false) would have a similar effect, but it also disables focus changes
4737 // and long press actions, which are both needed by text selection.
4738 final int length = drawableState.length;
4739 for (int i = 0; i < length; i++) {
4740 if (drawableState[i] == R.attr.state_pressed) {
4741 final int[] nonPressedState = new int[length - 1];
4742 System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4743 System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4744 return nonPressedState;
4745 }
4746 }
4747 }
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004748
Gilles Debunne86b9c782010-11-11 10:43:48 -08004749 return drawableState;
4750 }
4751
Romain Guyc4d8eb62010-08-18 20:48:33 -07004752 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004753 protected void onDraw(Canvas canvas) {
Michael Jurka2b942d52011-03-01 13:26:11 -08004754 if (mPreDrawState == PREDRAW_DONE) {
4755 final ViewTreeObserver observer = getViewTreeObserver();
4756 observer.removeOnPreDrawListener(this);
4757 mPreDrawState = PREDRAW_NOT_REGISTERED;
4758 }
4759
Romain Guy909cbaf2010-10-13 18:19:48 -07004760 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4761
Romain Guy986003d2009-03-25 17:42:35 -07004762 restartMarqueeIfNeeded();
4763
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004764 // Draw the background for this view
4765 super.onDraw(canvas);
4766
4767 final int compoundPaddingLeft = getCompoundPaddingLeft();
4768 final int compoundPaddingTop = getCompoundPaddingTop();
4769 final int compoundPaddingRight = getCompoundPaddingRight();
4770 final int compoundPaddingBottom = getCompoundPaddingBottom();
4771 final int scrollX = mScrollX;
4772 final int scrollY = mScrollY;
4773 final int right = mRight;
4774 final int left = mLeft;
4775 final int bottom = mBottom;
4776 final int top = mTop;
4777
4778 final Drawables dr = mDrawables;
4779 if (dr != null) {
4780 /*
4781 * Compound, not extended, because the icon is not clipped
4782 * if the text height is smaller.
4783 */
4784
4785 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4786 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4787
Romain Guy3c77d392009-05-20 11:26:50 -07004788 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4789 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004790 if (dr.mDrawableLeft != null) {
4791 canvas.save();
4792 canvas.translate(scrollX + mPaddingLeft,
4793 scrollY + compoundPaddingTop +
4794 (vspace - dr.mDrawableHeightLeft) / 2);
4795 dr.mDrawableLeft.draw(canvas);
4796 canvas.restore();
4797 }
4798
Romain Guy3c77d392009-05-20 11:26:50 -07004799 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4800 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004801 if (dr.mDrawableRight != null) {
4802 canvas.save();
4803 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4804 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4805 dr.mDrawableRight.draw(canvas);
4806 canvas.restore();
4807 }
4808
Romain Guy3c77d392009-05-20 11:26:50 -07004809 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4810 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004811 if (dr.mDrawableTop != null) {
4812 canvas.save();
4813 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4814 scrollY + mPaddingTop);
4815 dr.mDrawableTop.draw(canvas);
4816 canvas.restore();
4817 }
4818
Romain Guy3c77d392009-05-20 11:26:50 -07004819 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4820 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004821 if (dr.mDrawableBottom != null) {
4822 canvas.save();
4823 canvas.translate(scrollX + compoundPaddingLeft +
4824 (hspace - dr.mDrawableWidthBottom) / 2,
4825 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4826 dr.mDrawableBottom.draw(canvas);
4827 canvas.restore();
4828 }
4829 }
4830
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004831 int color = mCurTextColor;
4832
4833 if (mLayout == null) {
4834 assumeLayout();
4835 }
4836
4837 Layout layout = mLayout;
4838 int cursorcolor = color;
4839
4840 if (mHint != null && mText.length() == 0) {
4841 if (mHintTextColor != null) {
4842 color = mCurHintTextColor;
4843 }
4844
4845 layout = mHintLayout;
4846 }
4847
4848 mTextPaint.setColor(color);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004849 if (mCurrentAlpha != 255) {
4850 // If set, the alpha will override the color's alpha. Multiply the alphas.
4851 mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4852 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004853 mTextPaint.drawableState = getDrawableState();
4854
4855 canvas.save();
4856 /* Would be faster if we didn't have to do this. Can we chop the
4857 (displayable) text so that we don't need to do this ever?
4858 */
4859
4860 int extendedPaddingTop = getExtendedPaddingTop();
4861 int extendedPaddingBottom = getExtendedPaddingBottom();
4862
4863 float clipLeft = compoundPaddingLeft + scrollX;
4864 float clipTop = extendedPaddingTop + scrollY;
4865 float clipRight = right - left - compoundPaddingRight + scrollX;
4866 float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4867
4868 if (mShadowRadius != 0) {
4869 clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4870 clipRight += Math.max(0, mShadowDx + mShadowRadius);
4871
4872 clipTop += Math.min(0, mShadowDy - mShadowRadius);
4873 clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4874 }
4875
4876 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4877
4878 int voffsetText = 0;
4879 int voffsetCursor = 0;
4880
4881 // translate in by our padding
4882 {
4883 /* shortcircuit calling getVerticaOffset() */
4884 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4885 voffsetText = getVerticalOffset(false);
4886 voffsetCursor = getVerticalOffset(true);
4887 }
4888 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4889 }
4890
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004891 final int layoutDirection = getResolvedLayoutDirection();
4892 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Adam Powell282e3772011-08-30 16:51:11 -07004893 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
4894 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004895 if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004896 (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004897 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4898 getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4899 }
4900
4901 if (mMarquee != null && mMarquee.isRunning()) {
4902 canvas.translate(-mMarquee.mScroll, 0.0f);
4903 }
4904 }
4905
4906 Path highlight = null;
4907 int selStart = -1, selEnd = -1;
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004908 boolean drawCursor = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004909
4910 // If there is no movement method, then there can be no selection.
4911 // Check that first and attempt to skip everything having to do with
4912 // the cursor.
4913 // XXX This is not strictly true -- a program could set the
4914 // selection manually if it really wanted to.
4915 if (mMovement != null && (isFocused() || isPressed())) {
Gilles Debunne05336272010-07-09 20:13:45 -07004916 selStart = getSelectionStart();
4917 selEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004918
Gilles Debunnebb588da2011-07-11 18:26:19 -07004919 if (selStart >= 0) {
Gilles Debunnefd419b02011-08-25 11:53:26 -07004920 if (mHighlightPath == null) mHighlightPath = new Path();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004921
4922 if (selStart == selEnd) {
Gilles Debunnebb588da2011-07-11 18:26:19 -07004923 if (isCursorVisible() &&
Gilles Debunne86b9c782010-11-11 10:43:48 -08004924 (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004925 if (mHighlightPathBogus) {
4926 mHighlightPath.reset();
4927 mLayout.getCursorPath(selStart, mHighlightPath, mText);
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004928 updateCursorsPositions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004929 mHighlightPathBogus = false;
4930 }
4931
4932 // XXX should pass to skin instead of drawing directly
4933 mHighlightPaint.setColor(cursorcolor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004934 if (mCurrentAlpha != 255) {
4935 mHighlightPaint.setAlpha(
4936 (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4937 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004938 mHighlightPaint.setStyle(Paint.Style.STROKE);
Gilles Debunne46b7d442011-02-17 16:03:10 -08004939 highlight = mHighlightPath;
Gilles Debunneeca97a32011-02-23 17:48:28 -08004940 drawCursor = mCursorCount > 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004941 }
Gilles Debunnebb588da2011-07-11 18:26:19 -07004942 } else if (textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004943 if (mHighlightPathBogus) {
4944 mHighlightPath.reset();
4945 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4946 mHighlightPathBogus = false;
4947 }
4948
4949 // XXX should pass to skin instead of drawing directly
4950 mHighlightPaint.setColor(mHighlightColor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004951 if (mCurrentAlpha != 255) {
4952 mHighlightPaint.setAlpha(
4953 (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4954 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004955 mHighlightPaint.setStyle(Paint.Style.FILL);
4956
4957 highlight = mHighlightPath;
4958 }
4959 }
4960 }
4961
4962 /* Comment out until we decide what to do about animations
4963 boolean isLinearTextOn = false;
4964 if (currentTransformation != null) {
4965 isLinearTextOn = mTextPaint.isLinearTextOn();
4966 Matrix m = currentTransformation.getMatrix();
4967 if (!m.isIdentity()) {
4968 // mTextPaint.setLinearTextOn(true);
4969 }
4970 }
4971 */
4972
4973 final InputMethodState ims = mInputMethodState;
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004974 final int cursorOffsetVertical = voffsetCursor - voffsetText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004975 if (ims != null && ims.mBatchEditNesting == 0) {
4976 InputMethodManager imm = InputMethodManager.peekInstance();
4977 if (imm != null) {
4978 if (imm.isActive(this)) {
4979 boolean reported = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07004980 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004981 // We are in extract mode and the content has changed
4982 // in some way... just report complete new text to the
4983 // input method.
4984 reported = reportExtractedText();
4985 }
4986 if (!reported && highlight != null) {
4987 int candStart = -1;
4988 int candEnd = -1;
4989 if (mText instanceof Spannable) {
4990 Spannable sp = (Spannable)mText;
4991 candStart = EditableInputConnection.getComposingSpanStart(sp);
4992 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4993 }
4994 imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4995 }
4996 }
4997
4998 if (imm.isWatchingCursor(this) && highlight != null) {
4999 highlight.computeBounds(ims.mTmpRectF, true);
5000 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
5001
5002 canvas.getMatrix().mapPoints(ims.mTmpOffset);
5003 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
5004
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005005 ims.mTmpRectF.offset(0, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005006
5007 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
5008 (int)(ims.mTmpRectF.top + 0.5),
5009 (int)(ims.mTmpRectF.right + 0.5),
5010 (int)(ims.mTmpRectF.bottom + 0.5));
5011
5012 imm.updateCursor(this,
5013 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
5014 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
5015 }
5016 }
5017 }
5018
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005019 if (mCorrectionHighlighter != null) {
5020 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
5021 }
5022
Gilles Debunne46b7d442011-02-17 16:03:10 -08005023 if (drawCursor) {
5024 drawCursor(canvas, cursorOffsetVertical);
5025 // Rely on the drawable entirely, do not draw the cursor line.
5026 // Has to be done after the IMM related code above which relies on the highlight.
5027 highlight = null;
5028 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005029
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005030 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005031
Romain Guyc2303192009-04-03 17:37:18 -07005032 if (mMarquee != null && mMarquee.shouldDrawGhost()) {
5033 canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005034 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
Romain Guyc2303192009-04-03 17:37:18 -07005035 }
5036
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005037 /* Comment out until we decide what to do about animations
5038 if (currentTransformation != null) {
5039 mTextPaint.setLinearTextOn(isLinearTextOn);
5040 }
5041 */
5042
5043 canvas.restore();
Leon Scroggins56426252010-11-01 15:45:37 -04005044 }
5045
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005046 private void updateCursorsPositions() {
Gilles Debunneeca97a32011-02-23 17:48:28 -08005047 if (mCursorDrawableRes == 0) {
5048 mCursorCount = 0;
5049 return;
5050 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005051
5052 final int offset = getSelectionStart();
5053 final int line = mLayout.getLineForOffset(offset);
5054 final int top = mLayout.getLineTop(line);
5055 final int bottom = mLayout.getLineTop(line + 1);
5056
5057 mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
5058
5059 int middle = bottom;
5060 if (mCursorCount == 2) {
5061 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
5062 middle = (top + bottom) >> 1;
5063 }
5064
5065 updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
5066
5067 if (mCursorCount == 2) {
5068 updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
5069 }
5070 }
5071
5072 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
5073 if (mCursorDrawable[cursorIndex] == null)
5074 mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
5075
5076 if (mTempRect == null) mTempRect = new Rect();
5077
5078 mCursorDrawable[cursorIndex].getPadding(mTempRect);
5079 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
5080 horizontal = Math.max(0.5f, horizontal - 0.5f);
5081 final int left = (int) (horizontal) - mTempRect.left;
5082 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
5083 bottom + mTempRect.bottom);
5084 }
5085
5086 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
5087 final boolean translate = cursorOffsetVertical != 0;
5088 if (translate) canvas.translate(0, cursorOffsetVertical);
5089 for (int i = 0; i < mCursorCount; i++) {
5090 mCursorDrawable[i].draw(canvas);
5091 }
5092 if (translate) canvas.translate(0, -cursorOffsetVertical);
5093 }
5094
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005095 @Override
5096 public void getFocusedRect(Rect r) {
5097 if (mLayout == null) {
5098 super.getFocusedRect(r);
5099 return;
5100 }
5101
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005102 int selEnd = getSelectionEnd();
5103 if (selEnd < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005104 super.getFocusedRect(r);
5105 return;
5106 }
5107
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005108 int selStart = getSelectionStart();
5109 if (selStart < 0 || selStart >= selEnd) {
5110 int line = mLayout.getLineForOffset(selEnd);
5111 r.top = mLayout.getLineTop(line);
5112 r.bottom = mLayout.getLineBottom(line);
5113 r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
5114 r.right = r.left + 4;
5115 } else {
5116 int lineStart = mLayout.getLineForOffset(selStart);
5117 int lineEnd = mLayout.getLineForOffset(selEnd);
5118 r.top = mLayout.getLineTop(lineStart);
5119 r.bottom = mLayout.getLineBottom(lineEnd);
5120 if (lineStart == lineEnd) {
5121 r.left = (int) mLayout.getPrimaryHorizontal(selStart);
5122 r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
5123 } else {
5124 // Selection extends across multiple lines -- the focused
5125 // rect covers the entire width.
Gilles Debunnefd419b02011-08-25 11:53:26 -07005126 if (mHighlightPath == null) mHighlightPath = new Path();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005127 if (mHighlightPathBogus) {
5128 mHighlightPath.reset();
5129 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
5130 mHighlightPathBogus = false;
5131 }
5132 synchronized (sTempRect) {
5133 mHighlightPath.computeBounds(sTempRect, true);
5134 r.left = (int)sTempRect.left-1;
5135 r.right = (int)sTempRect.right+1;
5136 }
5137 }
5138 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005139
5140 // Adjust for padding and gravity.
5141 int paddingLeft = getCompoundPaddingLeft();
5142 int paddingTop = getExtendedPaddingTop();
5143 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5144 paddingTop += getVerticalOffset(false);
5145 }
5146 r.offset(paddingLeft, paddingTop);
5147 }
5148
5149 /**
5150 * Return the number of lines of text, or 0 if the internal Layout has not
5151 * been built.
5152 */
5153 public int getLineCount() {
5154 return mLayout != null ? mLayout.getLineCount() : 0;
5155 }
5156
5157 /**
5158 * Return the baseline for the specified line (0...getLineCount() - 1)
5159 * If bounds is not null, return the top, left, right, bottom extents
5160 * of the specified line in it. If the internal Layout has not been built,
5161 * return 0 and set bounds to (0, 0, 0, 0)
5162 * @param line which line to examine (0..getLineCount() - 1)
5163 * @param bounds Optional. If not null, it returns the extent of the line
5164 * @return the Y-coordinate of the baseline
5165 */
5166 public int getLineBounds(int line, Rect bounds) {
5167 if (mLayout == null) {
5168 if (bounds != null) {
5169 bounds.set(0, 0, 0, 0);
5170 }
5171 return 0;
5172 }
5173 else {
5174 int baseline = mLayout.getLineBounds(line, bounds);
5175
5176 int voffset = getExtendedPaddingTop();
5177 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5178 voffset += getVerticalOffset(true);
5179 }
5180 if (bounds != null) {
5181 bounds.offset(getCompoundPaddingLeft(), voffset);
5182 }
5183 return baseline + voffset;
5184 }
5185 }
5186
5187 @Override
5188 public int getBaseline() {
5189 if (mLayout == null) {
5190 return super.getBaseline();
5191 }
5192
5193 int voffset = 0;
5194 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5195 voffset = getVerticalOffset(true);
5196 }
5197
5198 return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
5199 }
5200
Romain Guyf2fc4602011-07-19 15:20:03 -07005201 /**
5202 * @hide
5203 * @param offsetRequired
5204 */
5205 @Override
5206 protected int getFadeTop(boolean offsetRequired) {
Romain Guy59f13c7d2011-07-19 18:35:33 -07005207 if (mLayout == null) return 0;
5208
Romain Guyf2fc4602011-07-19 15:20:03 -07005209 int voffset = 0;
5210 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5211 voffset = getVerticalOffset(true);
5212 }
5213
5214 if (offsetRequired) voffset += getTopPaddingOffset();
5215
5216 return getExtendedPaddingTop() + voffset;
5217 }
5218
5219 /**
5220 * @hide
5221 * @param offsetRequired
5222 */
Gilles Debunne3784a7f2011-07-15 13:49:38 -07005223 @Override
Romain Guyf2fc4602011-07-19 15:20:03 -07005224 protected int getFadeHeight(boolean offsetRequired) {
5225 return mLayout != null ? mLayout.getHeight() : 0;
5226 }
5227
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005228 @Override
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005229 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
5230 if (keyCode == KeyEvent.KEYCODE_BACK) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005231 boolean isInSelectionMode = mSelectionActionMode != null;
5232
Gilles Debunne28294cc2011-08-24 12:02:05 -07005233 if (isInSelectionMode) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005234 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
5235 KeyEvent.DispatcherState state = getKeyDispatcherState();
5236 if (state != null) {
5237 state.startTracking(event, this);
5238 }
5239 return true;
5240 } else if (event.getAction() == KeyEvent.ACTION_UP) {
5241 KeyEvent.DispatcherState state = getKeyDispatcherState();
5242 if (state != null) {
5243 state.handleUpEvent(event);
5244 }
5245 if (event.isTracking() && !event.isCanceled()) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005246 if (isInSelectionMode) {
5247 stopSelectionActionMode();
5248 return true;
5249 }
5250 }
5251 }
5252 }
5253 }
5254 return super.onKeyPreIme(keyCode, event);
5255 }
5256
5257 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005258 public boolean onKeyDown(int keyCode, KeyEvent event) {
5259 int which = doKeyDown(keyCode, event, null);
5260 if (which == 0) {
5261 // Go through default dispatching.
5262 return super.onKeyDown(keyCode, event);
5263 }
5264
5265 return true;
5266 }
5267
5268 @Override
5269 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005270 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005271
5272 int which = doKeyDown(keyCode, down, event);
5273 if (which == 0) {
5274 // Go through default dispatching.
5275 return super.onKeyMultiple(keyCode, repeatCount, event);
5276 }
5277 if (which == -1) {
5278 // Consumed the whole thing.
5279 return true;
5280 }
5281
5282 repeatCount--;
5283
5284 // We are going to dispatch the remaining events to either the input
5285 // or movement method. To do this, we will just send a repeated stream
5286 // of down and up events until we have done the complete repeatCount.
5287 // It would be nice if those interfaces had an onKeyMultiple() method,
5288 // but adding that is a more complicated change.
The Android Open Source Project10592532009-03-18 17:39:46 -07005289 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005290 if (which == 1) {
5291 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5292 while (--repeatCount > 0) {
5293 mInput.onKeyDown(this, (Editable)mText, keyCode, down);
5294 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5295 }
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005296 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005297
5298 } else if (which == 2) {
5299 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5300 while (--repeatCount > 0) {
5301 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
5302 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5303 }
5304 }
5305
5306 return true;
5307 }
5308
5309 /**
5310 * Returns true if pressing ENTER in this field advances focus instead
5311 * of inserting the character. This is true mostly in single-line fields,
5312 * but also in mail addresses and subjects which will display on multiple
5313 * lines but where it doesn't make sense to insert newlines.
5314 */
The Android Open Source Project4df24232009-03-05 14:34:35 -08005315 private boolean shouldAdvanceFocusOnEnter() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005316 if (mInput == null) {
5317 return false;
5318 }
5319
5320 if (mSingleLine) {
5321 return true;
5322 }
5323
5324 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5325 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005326 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
5327 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005328 return true;
5329 }
5330 }
5331
5332 return false;
5333 }
5334
Jeff Brown4e6319b2010-12-13 10:36:51 -08005335 /**
5336 * Returns true if pressing TAB in this field advances focus instead
5337 * of inserting the character. Insert tabs only in multi-line editors.
5338 */
5339 private boolean shouldAdvanceFocusOnTab() {
5340 if (mInput != null && !mSingleLine) {
5341 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5342 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
5343 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
5344 || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
5345 return false;
5346 }
5347 }
5348 }
5349 return true;
5350 }
5351
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005352 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
5353 if (!isEnabled()) {
5354 return 0;
5355 }
5356
5357 switch (keyCode) {
5358 case KeyEvent.KEYCODE_ENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005359 if (event.hasNoModifiers()) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005360 // When mInputContentType is set, we know that we are
5361 // running in a "modern" cupcake environment, so don't need
5362 // to worry about the application trying to capture
5363 // enter key events.
5364 if (mInputContentType != null) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005365 // If there is an action listener, given them a
5366 // chance to consume the event.
5367 if (mInputContentType.onEditorActionListener != null &&
5368 mInputContentType.onEditorActionListener.onEditorAction(
5369 this, EditorInfo.IME_NULL, event)) {
5370 mInputContentType.enterDown = true;
5371 // We are consuming the enter key for them.
5372 return -1;
5373 }
5374 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005375
The Android Open Source Project10592532009-03-18 17:39:46 -07005376 // If our editor should move focus when enter is pressed, or
5377 // this is a generated event from an IME action button, then
5378 // don't let it be inserted into the text.
Jeff Brown4e6319b2010-12-13 10:36:51 -08005379 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
The Android Open Source Project10592532009-03-18 17:39:46 -07005380 || shouldAdvanceFocusOnEnter()) {
Dianne Hackborn0500b3c2011-11-01 15:28:43 -07005381 if (hasOnClickListeners()) {
Leon Scroggins7014b122011-01-11 15:17:34 -05005382 return 0;
5383 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005384 return -1;
5385 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005386 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005387 break;
5388
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005389 case KeyEvent.KEYCODE_DPAD_CENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005390 if (event.hasNoModifiers()) {
5391 if (shouldAdvanceFocusOnEnter()) {
5392 return 0;
5393 }
5394 }
5395 break;
5396
5397 case KeyEvent.KEYCODE_TAB:
5398 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
5399 if (shouldAdvanceFocusOnTab()) {
5400 return 0;
5401 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005402 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005403 break;
5404
5405 // Has to be done on key down (and not on key up) to correctly be intercepted.
5406 case KeyEvent.KEYCODE_BACK:
5407 if (mSelectionActionMode != null) {
5408 stopSelectionActionMode();
5409 return -1;
5410 }
5411 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005412 }
5413
5414 if (mInput != null) {
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005415 resetErrorChangedFlag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005416
5417 boolean doDown = true;
5418 if (otherEvent != null) {
5419 try {
5420 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005421 final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005422 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005423 doDown = false;
5424 if (handled) {
5425 return -1;
5426 }
5427 } catch (AbstractMethodError e) {
5428 // onKeyOther was added after 1.0, so if it isn't
5429 // implemented we need to try to dispatch as a regular down.
5430 } finally {
5431 endBatchEdit();
5432 }
5433 }
5434
5435 if (doDown) {
5436 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005437 final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005438 endBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005439 hideErrorIfUnchanged();
5440 if (handled) return 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005441 }
5442 }
5443
5444 // bug 650865: sometimes we get a key event before a layout.
5445 // don't try to move around if we don't know the layout.
5446
5447 if (mMovement != null && mLayout != null) {
5448 boolean doDown = true;
5449 if (otherEvent != null) {
5450 try {
5451 boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
5452 otherEvent);
5453 doDown = false;
5454 if (handled) {
5455 return -1;
5456 }
5457 } catch (AbstractMethodError e) {
5458 // onKeyOther was added after 1.0, so if it isn't
5459 // implemented we need to try to dispatch as a regular down.
5460 }
5461 }
5462 if (doDown) {
5463 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
5464 return 2;
5465 }
5466 }
5467
5468 return 0;
5469 }
5470
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005471 /**
5472 * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
5473 * can be recorded.
5474 * @hide
5475 */
5476 public void resetErrorChangedFlag() {
5477 /*
5478 * Keep track of what the error was before doing the input
5479 * so that if an input filter changed the error, we leave
5480 * that error showing. Otherwise, we take down whatever
5481 * error was showing when the user types something.
5482 */
5483 mErrorWasChanged = false;
5484 }
5485
5486 /**
5487 * @hide
5488 */
5489 public void hideErrorIfUnchanged() {
5490 if (mError != null && !mErrorWasChanged) {
5491 setError(null, null);
5492 }
5493 }
5494
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005495 @Override
5496 public boolean onKeyUp(int keyCode, KeyEvent event) {
5497 if (!isEnabled()) {
5498 return super.onKeyUp(keyCode, event);
5499 }
5500
5501 switch (keyCode) {
5502 case KeyEvent.KEYCODE_DPAD_CENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005503 if (event.hasNoModifiers()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005504 /*
5505 * If there is a click listener, just call through to
5506 * super, which will invoke it.
5507 *
Jeff Brown4e6319b2010-12-13 10:36:51 -08005508 * If there isn't a click listener, try to show the soft
5509 * input method. (It will also
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005510 * call performClick(), but that won't do anything in
5511 * this case.)
5512 */
Gilles Debunne06a8e9b2011-12-08 10:39:39 -08005513 if (!hasOnClickListeners()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005514 if (mMovement != null && mText instanceof Editable
5515 && mLayout != null && onCheckIsTextEditor()) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08005516 InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09005517 viewClicked(imm);
Gilles Debunne0f4109e2011-10-19 11:26:21 -07005518 if (imm != null && mSoftInputShownOnFocus) {
satok863fcd62011-06-21 17:38:02 +09005519 imm.showSoftInput(this, 0);
5520 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005521 }
5522 }
5523 }
5524 return super.onKeyUp(keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005525
Jeff Brown4e6319b2010-12-13 10:36:51 -08005526 case KeyEvent.KEYCODE_ENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005527 if (event.hasNoModifiers()) {
5528 if (mInputContentType != null
5529 && mInputContentType.onEditorActionListener != null
5530 && mInputContentType.enterDown) {
5531 mInputContentType.enterDown = false;
5532 if (mInputContentType.onEditorActionListener.onEditorAction(
5533 this, EditorInfo.IME_NULL, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005534 return true;
5535 }
5536 }
5537
Jeff Brown4e6319b2010-12-13 10:36:51 -08005538 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
5539 || shouldAdvanceFocusOnEnter()) {
5540 /*
5541 * If there is a click listener, just call through to
5542 * super, which will invoke it.
5543 *
5544 * If there isn't a click listener, try to advance focus,
5545 * but still call through to super, which will reset the
5546 * pressed state and longpress state. (It will also
5547 * call performClick(), but that won't do anything in
5548 * this case.)
5549 */
Gilles Debunne06a8e9b2011-12-08 10:39:39 -08005550 if (!hasOnClickListeners()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005551 View v = focusSearch(FOCUS_DOWN);
5552
5553 if (v != null) {
5554 if (!v.requestFocus(FOCUS_DOWN)) {
5555 throw new IllegalStateException(
5556 "focus search returned a view " +
5557 "that wasn't able to take focus!");
5558 }
5559
5560 /*
5561 * Return true because we handled the key; super
5562 * will return false because there was no click
5563 * listener.
5564 */
5565 super.onKeyUp(keyCode, event);
5566 return true;
5567 } else if ((event.getFlags()
5568 & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
5569 // No target for next focus, but make sure the IME
5570 // if this came from it.
5571 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08005572 if (imm != null && imm.isActive(this)) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005573 imm.hideSoftInputFromWindow(getWindowToken(), 0);
5574 }
5575 }
5576 }
5577 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005578 return super.onKeyUp(keyCode, event);
5579 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005580 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005581 }
5582
5583 if (mInput != null)
5584 if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
5585 return true;
5586
5587 if (mMovement != null && mLayout != null)
5588 if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
5589 return true;
5590
5591 return super.onKeyUp(keyCode, event);
5592 }
5593
5594 @Override public boolean onCheckIsTextEditor() {
5595 return mInputType != EditorInfo.TYPE_NULL;
5596 }
Gilles Debunneb062e812011-09-27 14:58:37 -07005597
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005598 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
Janos Levai042856c2010-10-15 02:53:58 +03005599 if (onCheckIsTextEditor() && isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005600 if (mInputMethodState == null) {
5601 mInputMethodState = new InputMethodState();
5602 }
5603 outAttrs.inputType = mInputType;
5604 if (mInputContentType != null) {
5605 outAttrs.imeOptions = mInputContentType.imeOptions;
5606 outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5607 outAttrs.actionLabel = mInputContentType.imeActionLabel;
5608 outAttrs.actionId = mInputContentType.imeActionId;
5609 outAttrs.extras = mInputContentType.extras;
5610 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005611 outAttrs.imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005612 }
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005613 if (focusSearch(FOCUS_DOWN) != null) {
5614 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5615 }
5616 if (focusSearch(FOCUS_UP) != null) {
5617 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5618 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005619 if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5620 == EditorInfo.IME_ACTION_UNSPECIFIED) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005621 if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005622 // An action has not been set, but the enter key will move to
5623 // the next focus, so set the action to that.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005624 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005625 } else {
5626 // An action has not been set, and there is no focus to move
5627 // to, so let's just supply a "done" action.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005628 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005629 }
5630 if (!shouldAdvanceFocusOnEnter()) {
5631 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005632 }
5633 }
Gilles Debunne91a08cf2010-11-08 17:34:49 -08005634 if (isMultilineInputType(outAttrs.inputType)) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005635 // Multi-line text editors should always show an enter key.
5636 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5637 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005638 outAttrs.hintText = mHint;
5639 if (mText instanceof Editable) {
5640 InputConnection ic = new EditableInputConnection(this);
Gilles Debunne05336272010-07-09 20:13:45 -07005641 outAttrs.initialSelStart = getSelectionStart();
5642 outAttrs.initialSelEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005643 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5644 return ic;
5645 }
5646 }
5647 return null;
5648 }
5649
5650 /**
5651 * If this TextView contains editable content, extract a portion of it
5652 * based on the information in <var>request</var> in to <var>outText</var>.
5653 * @return Returns true if the text was successfully extracted, else false.
5654 */
5655 public boolean extractText(ExtractedTextRequest request,
5656 ExtractedText outText) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005657 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5658 EXTRACT_UNKNOWN, outText);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005659 }
5660
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005661 static final int EXTRACT_NOTHING = -2;
5662 static final int EXTRACT_UNKNOWN = -1;
5663
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005664 boolean extractTextInternal(ExtractedTextRequest request,
5665 int partialStartOffset, int partialEndOffset, int delta,
5666 ExtractedText outText) {
5667 final CharSequence content = mText;
5668 if (content != null) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005669 if (partialStartOffset != EXTRACT_NOTHING) {
5670 final int N = content.length();
5671 if (partialStartOffset < 0) {
5672 outText.partialStartOffset = outText.partialEndOffset = -1;
5673 partialStartOffset = 0;
5674 partialEndOffset = N;
5675 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01005676 // Now use the delta to determine the actual amount of text
5677 // we need.
5678 partialEndOffset += delta;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005679 // Adjust offsets to ensure we contain full spans.
5680 if (content instanceof Spanned) {
5681 Spanned spanned = (Spanned)content;
5682 Object[] spans = spanned.getSpans(partialStartOffset,
5683 partialEndOffset, ParcelableSpan.class);
5684 int i = spans.length;
5685 while (i > 0) {
5686 i--;
5687 int j = spanned.getSpanStart(spans[i]);
5688 if (j < partialStartOffset) partialStartOffset = j;
5689 j = spanned.getSpanEnd(spans[i]);
5690 if (j > partialEndOffset) partialEndOffset = j;
5691 }
5692 }
5693 outText.partialStartOffset = partialStartOffset;
Viktor Yakovel964be412010-02-17 08:35:57 +01005694 outText.partialEndOffset = partialEndOffset - delta;
5695
Eric Fischer32929412009-12-14 17:33:11 -08005696 if (partialStartOffset > N) {
5697 partialStartOffset = N;
5698 } else if (partialStartOffset < 0) {
5699 partialStartOffset = 0;
5700 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005701 if (partialEndOffset > N) {
5702 partialEndOffset = N;
5703 } else if (partialEndOffset < 0) {
5704 partialEndOffset = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005705 }
5706 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005707 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5708 outText.text = content.subSequence(partialStartOffset,
5709 partialEndOffset);
5710 } else {
5711 outText.text = TextUtils.substring(content, partialStartOffset,
5712 partialEndOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005713 }
Viktor Yakovel970a138c92010-02-12 16:00:41 +01005714 } else {
5715 outText.partialStartOffset = 0;
5716 outText.partialEndOffset = 0;
5717 outText.text = "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005718 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005719 outText.flags = 0;
5720 if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5721 outText.flags |= ExtractedText.FLAG_SELECTING;
5722 }
5723 if (mSingleLine) {
5724 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005725 }
5726 outText.startOffset = 0;
Gilles Debunne05336272010-07-09 20:13:45 -07005727 outText.selectionStart = getSelectionStart();
5728 outText.selectionEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005729 return true;
5730 }
5731 return false;
5732 }
5733
5734 boolean reportExtractedText() {
5735 final InputMethodState ims = mInputMethodState;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005736 if (ims != null) {
5737 final boolean contentChanged = ims.mContentChanged;
5738 if (contentChanged || ims.mSelectionModeChanged) {
5739 ims.mContentChanged = false;
5740 ims.mSelectionModeChanged = false;
5741 final ExtractedTextRequest req = mInputMethodState.mExtracting;
5742 if (req != null) {
5743 InputMethodManager imm = InputMethodManager.peekInstance();
5744 if (imm != null) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005745 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005746 + ims.mChangedStart + " end=" + ims.mChangedEnd
5747 + " delta=" + ims.mChangedDelta);
5748 if (ims.mChangedStart < 0 && !contentChanged) {
5749 ims.mChangedStart = EXTRACT_NOTHING;
5750 }
5751 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5752 ims.mChangedDelta, ims.mTmpExtracted)) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005753 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005754 + ims.mTmpExtracted.partialStartOffset
5755 + " end=" + ims.mTmpExtracted.partialEndOffset
5756 + ": " + ims.mTmpExtracted.text);
5757 imm.updateExtractedText(this, req.token,
5758 mInputMethodState.mTmpExtracted);
Viktor Yakovel964be412010-02-17 08:35:57 +01005759 ims.mChangedStart = EXTRACT_UNKNOWN;
5760 ims.mChangedEnd = EXTRACT_UNKNOWN;
5761 ims.mChangedDelta = 0;
5762 ims.mContentChanged = false;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005763 return true;
5764 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005765 }
5766 }
5767 }
5768 }
5769 return false;
5770 }
5771
5772 /**
5773 * This is used to remove all style-impacting spans from text before new
5774 * extracted text is being replaced into it, so that we don't have any
5775 * lingering spans applied during the replace.
5776 */
5777 static void removeParcelableSpans(Spannable spannable, int start, int end) {
5778 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5779 int i = spans.length;
5780 while (i > 0) {
5781 i--;
5782 spannable.removeSpan(spans[i]);
5783 }
5784 }
5785
5786 /**
5787 * Apply to this text view the given extracted text, as previously
5788 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5789 */
5790 public void setExtractedText(ExtractedText text) {
5791 Editable content = getEditableText();
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005792 if (text.text != null) {
5793 if (content == null) {
5794 setText(text.text, TextView.BufferType.EDITABLE);
5795 } else if (text.partialStartOffset < 0) {
5796 removeParcelableSpans(content, 0, content.length());
5797 content.replace(0, content.length(), text.text);
5798 } else {
5799 final int N = content.length();
5800 int start = text.partialStartOffset;
5801 if (start > N) start = N;
5802 int end = text.partialEndOffset;
5803 if (end > N) end = N;
5804 removeParcelableSpans(content, start, end);
5805 content.replace(start, end, text.text);
5806 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005807 }
5808
5809 // Now set the selection position... make sure it is in range, to
5810 // avoid crashes. If this is a partial update, it is possible that
5811 // the underlying text may have changed, causing us problems here.
5812 // Also we just don't want to trust clients to do the right thing.
5813 Spannable sp = (Spannable)getText();
5814 final int N = sp.length();
5815 int start = text.selectionStart;
5816 if (start < 0) start = 0;
5817 else if (start > N) start = N;
5818 int end = text.selectionEnd;
5819 if (end < 0) end = 0;
5820 else if (end > N) end = N;
5821 Selection.setSelection(sp, start, end);
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005822
5823 // Finally, update the selection mode.
5824 if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5825 MetaKeyKeyListener.startSelecting(this, sp);
5826 } else {
5827 MetaKeyKeyListener.stopSelecting(this, sp);
5828 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005829 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005830
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005831 /**
5832 * @hide
5833 */
5834 public void setExtracting(ExtractedTextRequest req) {
5835 if (mInputMethodState != null) {
5836 mInputMethodState.mExtracting = req;
5837 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005838 // This would stop a possible selection mode, but no such mode is started in case
5839 // extracted mode will start. Some text is selected though, and will trigger an action mode
5840 // in the extracted view.
Adam Powellba0a2c32010-09-28 17:41:23 -07005841 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005842 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005843
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005844 /**
5845 * Called by the framework in response to a text completion from
5846 * the current input method, provided by it calling
5847 * {@link InputConnection#commitCompletion
5848 * InputConnection.commitCompletion()}. The default implementation does
5849 * nothing; text views that are supporting auto-completion should override
5850 * this to do their desired behavior.
5851 *
5852 * @param text The auto complete text the user has selected.
5853 */
5854 public void onCommitCompletion(CompletionInfo text) {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08005855 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005856 }
5857
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005858 /**
5859 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5860 * a dictionnary) from the current input method, provided by it calling
5861 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5862 * implementation flashes the background of the corrected word to provide feedback to the user.
5863 *
5864 * @param info The auto correct info about the text that was corrected.
5865 */
5866 public void onCommitCorrection(CorrectionInfo info) {
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005867 if (mCorrectionHighlighter == null) {
5868 mCorrectionHighlighter = new CorrectionHighlighter();
5869 } else {
5870 mCorrectionHighlighter.invalidate(false);
5871 }
5872
5873 mCorrectionHighlighter.highlight(info);
5874 }
5875
5876 private class CorrectionHighlighter {
5877 private final Path mPath = new Path();
5878 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5879 private int mStart, mEnd;
5880 private long mFadingStartTime;
5881 private final static int FADE_OUT_DURATION = 400;
5882
5883 public CorrectionHighlighter() {
5884 mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5885 mPaint.setStyle(Paint.Style.FILL);
5886 }
5887
5888 public void highlight(CorrectionInfo info) {
5889 mStart = info.getOffset();
5890 mEnd = mStart + info.getNewText().length();
5891 mFadingStartTime = SystemClock.uptimeMillis();
5892
5893 if (mStart < 0 || mEnd < 0) {
5894 stopAnimation();
5895 }
5896 }
5897
5898 public void draw(Canvas canvas, int cursorOffsetVertical) {
5899 if (updatePath() && updatePaint()) {
5900 if (cursorOffsetVertical != 0) {
5901 canvas.translate(0, cursorOffsetVertical);
5902 }
5903
5904 canvas.drawPath(mPath, mPaint);
5905
5906 if (cursorOffsetVertical != 0) {
5907 canvas.translate(0, -cursorOffsetVertical);
5908 }
Gilles Debunne8615ac92011-11-29 15:25:03 -08005909 invalidate(true); // TODO invalidate cursor region only
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005910 } else {
5911 stopAnimation();
Gilles Debunne8615ac92011-11-29 15:25:03 -08005912 invalidate(false); // TODO invalidate cursor region only
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005913 }
5914 }
5915
5916 private boolean updatePaint() {
5917 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5918 if (duration > FADE_OUT_DURATION) return false;
5919
5920 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5921 final int highlightColorAlpha = Color.alpha(mHighlightColor);
5922 final int color = (mHighlightColor & 0x00FFFFFF) +
5923 ((int) (highlightColorAlpha * coef) << 24);
5924 mPaint.setColor(color);
5925 return true;
5926 }
5927
5928 private boolean updatePath() {
5929 final Layout layout = TextView.this.mLayout;
5930 if (layout == null) return false;
5931
5932 // Update in case text is edited while the animation is run
5933 final int length = mText.length();
5934 int start = Math.min(length, mStart);
5935 int end = Math.min(length, mEnd);
5936
5937 mPath.reset();
5938 TextView.this.mLayout.getSelectionPath(start, end, mPath);
5939 return true;
5940 }
5941
5942 private void invalidate(boolean delayed) {
5943 if (TextView.this.mLayout == null) return;
5944
5945 synchronized (sTempRect) {
5946 mPath.computeBounds(sTempRect, false);
5947
5948 int left = getCompoundPaddingLeft();
5949 int top = getExtendedPaddingTop() + getVerticalOffset(true);
5950
5951 if (delayed) {
5952 TextView.this.postInvalidateDelayed(16, // 60 Hz update
5953 left + (int) sTempRect.left, top + (int) sTempRect.top,
5954 left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5955 } else {
5956 TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5957 (int) sTempRect.right, (int) sTempRect.bottom);
5958 }
5959 }
5960 }
5961
5962 private void stopAnimation() {
5963 TextView.this.mCorrectionHighlighter = null;
5964 }
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005965 }
5966
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005967 public void beginBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005968 mInBatchEditControllers = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005969 final InputMethodState ims = mInputMethodState;
5970 if (ims != null) {
5971 int nesting = ++ims.mBatchEditNesting;
5972 if (nesting == 1) {
5973 ims.mCursorChanged = false;
5974 ims.mChangedDelta = 0;
5975 if (ims.mContentChanged) {
5976 // We already have a pending change from somewhere else,
5977 // so turn this into a full update.
5978 ims.mChangedStart = 0;
5979 ims.mChangedEnd = mText.length();
5980 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005981 ims.mChangedStart = EXTRACT_UNKNOWN;
5982 ims.mChangedEnd = EXTRACT_UNKNOWN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005983 ims.mContentChanged = false;
5984 }
5985 onBeginBatchEdit();
5986 }
5987 }
5988 }
5989
5990 public void endBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005991 mInBatchEditControllers = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005992 final InputMethodState ims = mInputMethodState;
5993 if (ims != null) {
5994 int nesting = --ims.mBatchEditNesting;
5995 if (nesting == 0) {
5996 finishBatchEdit(ims);
5997 }
5998 }
5999 }
6000
6001 void ensureEndedBatchEdit() {
6002 final InputMethodState ims = mInputMethodState;
6003 if (ims != null && ims.mBatchEditNesting != 0) {
6004 ims.mBatchEditNesting = 0;
6005 finishBatchEdit(ims);
6006 }
6007 }
6008
6009 void finishBatchEdit(final InputMethodState ims) {
6010 onEndBatchEdit();
6011
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07006012 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006013 updateAfterEdit();
6014 reportExtractedText();
6015 } else if (ims.mCursorChanged) {
6016 // Cheezy way to get us to report the current cursor location.
6017 invalidateCursor();
6018 }
6019 }
6020
6021 void updateAfterEdit() {
6022 invalidate();
Gilles Debunne05336272010-07-09 20:13:45 -07006023 int curs = getSelectionStart();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006024
Gilles Debunne3d010062011-02-18 14:16:41 -08006025 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006026 registerForPreDraw();
6027 }
6028
6029 if (curs >= 0) {
6030 mHighlightPathBogus = true;
Gilles Debunne3d010062011-02-18 14:16:41 -08006031 makeBlink();
Gilles Debunne8202cd32011-07-15 09:56:30 -07006032 bringPointIntoView(curs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006033 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006034
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006035 checkForResize();
6036 }
6037
6038 /**
6039 * Called by the framework in response to a request to begin a batch
6040 * of edit operations through a call to link {@link #beginBatchEdit()}.
6041 */
6042 public void onBeginBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08006043 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006044 }
6045
6046 /**
6047 * Called by the framework in response to a request to end a batch
6048 * of edit operations through a call to link {@link #endBatchEdit}.
6049 */
6050 public void onEndBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08006051 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006052 }
6053
6054 /**
6055 * Called by the framework in response to a private command from the
6056 * current method, provided by it calling
6057 * {@link InputConnection#performPrivateCommand
6058 * InputConnection.performPrivateCommand()}.
6059 *
6060 * @param action The action name of the command.
6061 * @param data Any additional data for the command. This may be null.
6062 * @return Return true if you handled the command, else false.
6063 */
6064 public boolean onPrivateIMECommand(String action, Bundle data) {
6065 return false;
6066 }
6067
6068 private void nullLayouts() {
6069 if (mLayout instanceof BoringLayout && mSavedLayout == null) {
6070 mSavedLayout = (BoringLayout) mLayout;
6071 }
6072 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
6073 mSavedHintLayout = (BoringLayout) mHintLayout;
6074 }
6075
Adam Powell282e3772011-08-30 16:51:11 -07006076 mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
Gilles Debunne77f18b02010-10-22 14:28:25 -07006077
Fabrice Di Megliod4c3b8e2011-11-09 18:04:07 -08006078 mBoring = mHintBoring = null;
6079
Gilles Debunne77f18b02010-10-22 14:28:25 -07006080 // Since it depends on the value of mLayout
6081 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006082 }
6083
6084 /**
6085 * Make a new Layout based on the already-measured size of the view,
6086 * on the assumption that it was measured correctly at some point.
6087 */
6088 private void assumeLayout() {
6089 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6090
6091 if (width < 1) {
6092 width = 0;
6093 }
6094
6095 int physicalWidth = width;
6096
6097 if (mHorizontallyScrolling) {
Jeff Brown033a0012011-11-11 15:30:16 -08006098 width = VERY_WIDE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006099 }
6100
6101 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
6102 physicalWidth, false);
6103 }
6104
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006105 @Override
Fabrice Di Meglio7f86c802011-07-01 15:09:24 -07006106 protected void resetResolvedLayoutDirection() {
6107 super.resetResolvedLayoutDirection();
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006108
6109 if (mLayoutAlignment != null &&
6110 (mTextAlign == TextAlign.VIEW_START ||
6111 mTextAlign == TextAlign.VIEW_END)) {
6112 mLayoutAlignment = null;
6113 }
6114 }
6115
6116 private Layout.Alignment getLayoutAlignment() {
6117 if (mLayoutAlignment == null) {
6118 Layout.Alignment alignment;
6119 TextAlign textAlign = mTextAlign;
6120 switch (textAlign) {
6121 case INHERIT:
6122 // fall through to gravity temporarily
6123 // intention is to inherit value through view hierarchy.
6124 case GRAVITY:
6125 switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
6126 case Gravity.START:
6127 alignment = Layout.Alignment.ALIGN_NORMAL;
6128 break;
6129 case Gravity.END:
6130 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6131 break;
6132 case Gravity.LEFT:
6133 alignment = Layout.Alignment.ALIGN_LEFT;
6134 break;
6135 case Gravity.RIGHT:
6136 alignment = Layout.Alignment.ALIGN_RIGHT;
6137 break;
6138 case Gravity.CENTER_HORIZONTAL:
6139 alignment = Layout.Alignment.ALIGN_CENTER;
6140 break;
6141 default:
6142 alignment = Layout.Alignment.ALIGN_NORMAL;
6143 break;
6144 }
6145 break;
6146 case TEXT_START:
6147 alignment = Layout.Alignment.ALIGN_NORMAL;
6148 break;
6149 case TEXT_END:
6150 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6151 break;
6152 case CENTER:
6153 alignment = Layout.Alignment.ALIGN_CENTER;
6154 break;
6155 case VIEW_START:
6156 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6157 Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
6158 break;
6159 case VIEW_END:
6160 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6161 Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
6162 break;
6163 default:
6164 alignment = Layout.Alignment.ALIGN_NORMAL;
6165 break;
6166 }
6167 mLayoutAlignment = alignment;
6168 }
6169 return mLayoutAlignment;
6170 }
6171
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006172 /**
6173 * The width passed in is now the desired layout width,
6174 * not the full view width with padding.
6175 * {@hide}
6176 */
Gilles Debunne287d6c62011-10-05 18:22:11 -07006177 protected void makeNewLayout(int wantWidth, int hintWidth,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006178 BoringLayout.Metrics boring,
6179 BoringLayout.Metrics hintBoring,
6180 int ellipsisWidth, boolean bringIntoView) {
6181 stopMarquee();
6182
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006183 // Update "old" cached values
6184 mOldMaximum = mMaximum;
6185 mOldMaxMode = mMaxMode;
6186
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006187 mHighlightPathBogus = true;
6188
Gilles Debunne287d6c62011-10-05 18:22:11 -07006189 if (wantWidth < 0) {
6190 wantWidth = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006191 }
6192 if (hintWidth < 0) {
6193 hintWidth = 0;
6194 }
6195
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006196 Layout.Alignment alignment = getLayoutAlignment();
Romain Guy4dc4f732009-06-19 15:16:40 -07006197 boolean shouldEllipsize = mEllipsize != null && mInput == null;
Adam Powell282e3772011-08-30 16:51:11 -07006198 final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
6199 mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
6200 TruncateAt effectiveEllipsize = mEllipsize;
6201 if (mEllipsize == TruncateAt.MARQUEE &&
6202 mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07006203 effectiveEllipsize = TruncateAt.END_SMALL;
Adam Powell282e3772011-08-30 16:51:11 -07006204 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006205
Doug Feltcb3791202011-07-07 11:57:48 -07006206 if (mTextDir == null) {
6207 resolveTextDirection();
6208 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006209
Gilles Debunne287d6c62011-10-05 18:22:11 -07006210 mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
Adam Powell282e3772011-08-30 16:51:11 -07006211 effectiveEllipsize, effectiveEllipsize == mEllipsize);
6212 if (switchEllipsize) {
6213 TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
6214 TruncateAt.END : TruncateAt.MARQUEE;
Gilles Debunne287d6c62011-10-05 18:22:11 -07006215 mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
Adam Powell282e3772011-08-30 16:51:11 -07006216 shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006217 }
6218
Romain Guy4dc4f732009-06-19 15:16:40 -07006219 shouldEllipsize = mEllipsize != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006220 mHintLayout = null;
6221
6222 if (mHint != null) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006223 if (shouldEllipsize) hintWidth = wantWidth;
Romain Guy4dc4f732009-06-19 15:16:40 -07006224
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006225 if (hintBoring == UNKNOWN_BORING) {
Doug Feltcb3791202011-07-07 11:57:48 -07006226 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006227 mHintBoring);
6228 if (hintBoring != null) {
6229 mHintBoring = hintBoring;
6230 }
6231 }
6232
6233 if (hintBoring != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006234 if (hintBoring.width <= hintWidth &&
6235 (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006236 if (mSavedHintLayout != null) {
6237 mHintLayout = mSavedHintLayout.
6238 replaceOrMake(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006239 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6240 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006241 } else {
6242 mHintLayout = BoringLayout.make(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006243 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6244 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006245 }
6246
6247 mSavedHintLayout = (BoringLayout) mHintLayout;
Romain Guy4dc4f732009-06-19 15:16:40 -07006248 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
6249 if (mSavedHintLayout != null) {
6250 mHintLayout = mSavedHintLayout.
6251 replaceOrMake(mHint, mTextPaint,
6252 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6253 hintBoring, mIncludePad, mEllipsize,
6254 ellipsisWidth);
6255 } else {
6256 mHintLayout = BoringLayout.make(mHint, mTextPaint,
6257 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6258 hintBoring, mIncludePad, mEllipsize,
6259 ellipsisWidth);
6260 }
6261 } else if (shouldEllipsize) {
6262 mHintLayout = new StaticLayout(mHint,
6263 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006264 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006265 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006266 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006267 } else {
6268 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006269 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006270 mIncludePad);
6271 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006272 } else if (shouldEllipsize) {
6273 mHintLayout = new StaticLayout(mHint,
6274 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006275 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006276 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006277 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006278 } else {
6279 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006280 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006281 mIncludePad);
6282 }
6283 }
6284
6285 if (bringIntoView) {
6286 registerForPreDraw();
6287 }
6288
6289 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
Romain Guy939151f2009-04-08 14:22:40 -07006290 if (!compressText(ellipsisWidth)) {
6291 final int height = mLayoutParams.height;
6292 // If the size of the view does not depend on the size of the text, try to
6293 // start the marquee immediately
Romain Guy980a9382010-01-08 15:06:28 -08006294 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
Romain Guy939151f2009-04-08 14:22:40 -07006295 startMarquee();
6296 } else {
6297 // Defer the start of the marquee until we know our width (see setFrame())
6298 mRestartMarquee = true;
6299 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006300 }
6301 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07006302
6303 // CursorControllers need a non-null mLayout
6304 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006305 }
6306
Gilles Debunne287d6c62011-10-05 18:22:11 -07006307 private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Adam Powell282e3772011-08-30 16:51:11 -07006308 Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
6309 boolean useSaved) {
6310 Layout result = null;
6311 if (mText instanceof Spannable) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006312 result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
Adam Powell282e3772011-08-30 16:51:11 -07006313 alignment, mTextDir, mSpacingMult,
6314 mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
Fabrice Di Meglioad0b0512011-10-04 17:21:26 -07006315 ellipsisWidth);
Adam Powell282e3772011-08-30 16:51:11 -07006316 } else {
6317 if (boring == UNKNOWN_BORING) {
6318 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
6319 if (boring != null) {
6320 mBoring = boring;
6321 }
6322 }
6323
6324 if (boring != null) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006325 if (boring.width <= wantWidth &&
Adam Powell282e3772011-08-30 16:51:11 -07006326 (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
6327 if (useSaved && mSavedLayout != null) {
6328 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006329 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006330 boring, mIncludePad);
6331 } else {
6332 result = BoringLayout.make(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006333 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006334 boring, mIncludePad);
6335 }
6336
6337 if (useSaved) {
6338 mSavedLayout = (BoringLayout) result;
6339 }
Gilles Debunne287d6c62011-10-05 18:22:11 -07006340 } else if (shouldEllipsize && boring.width <= wantWidth) {
Adam Powell282e3772011-08-30 16:51:11 -07006341 if (useSaved && mSavedLayout != null) {
6342 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006343 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006344 boring, mIncludePad, effectiveEllipsize,
6345 ellipsisWidth);
6346 } else {
6347 result = BoringLayout.make(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006348 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006349 boring, mIncludePad, effectiveEllipsize,
6350 ellipsisWidth);
6351 }
6352 } else if (shouldEllipsize) {
6353 result = new StaticLayout(mTransformed,
6354 0, mTransformed.length(),
Gilles Debunne287d6c62011-10-05 18:22:11 -07006355 mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
Adam Powell282e3772011-08-30 16:51:11 -07006356 mSpacingAdd, mIncludePad, effectiveEllipsize,
6357 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6358 } else {
6359 result = new StaticLayout(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006360 wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006361 mIncludePad);
6362 }
6363 } else if (shouldEllipsize) {
6364 result = new StaticLayout(mTransformed,
6365 0, mTransformed.length(),
Gilles Debunne287d6c62011-10-05 18:22:11 -07006366 mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
Adam Powell282e3772011-08-30 16:51:11 -07006367 mSpacingAdd, mIncludePad, effectiveEllipsize,
6368 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6369 } else {
6370 result = new StaticLayout(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006371 wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006372 mIncludePad);
6373 }
6374 }
6375 return result;
6376 }
6377
Romain Guy939151f2009-04-08 14:22:40 -07006378 private boolean compressText(float width) {
Romain Guy2bffd262010-09-12 17:40:02 -07006379 if (isHardwareAccelerated()) return false;
6380
Romain Guy3373ed62009-05-04 14:13:32 -07006381 // Only compress the text if it hasn't been compressed by the previous pass
6382 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
6383 mTextPaint.getTextScaleX() == 1.0f) {
Romain Guy939151f2009-04-08 14:22:40 -07006384 final float textWidth = mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07006385 final float overflow = (textWidth + 1.0f - width) / width;
Romain Guy939151f2009-04-08 14:22:40 -07006386 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
6387 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
6388 post(new Runnable() {
6389 public void run() {
6390 requestLayout();
6391 }
6392 });
6393 return true;
6394 }
6395 }
6396
6397 return false;
6398 }
6399
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006400 private static int desired(Layout layout) {
6401 int n = layout.getLineCount();
6402 CharSequence text = layout.getText();
6403 float max = 0;
6404
6405 // if any line was wrapped, we can't use it.
6406 // but it's ok for the last line not to have a newline
6407
6408 for (int i = 0; i < n - 1; i++) {
6409 if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
6410 return -1;
6411 }
6412
6413 for (int i = 0; i < n; i++) {
6414 max = Math.max(max, layout.getLineWidth(i));
6415 }
6416
6417 return (int) FloatMath.ceil(max);
6418 }
6419
6420 /**
6421 * Set whether the TextView includes extra top and bottom padding to make
6422 * room for accents that go above the normal ascent and descent.
6423 * The default is true.
6424 *
6425 * @attr ref android.R.styleable#TextView_includeFontPadding
6426 */
6427 public void setIncludeFontPadding(boolean includepad) {
Gilles Debunne22378292011-08-12 10:38:52 -07006428 if (mIncludePad != includepad) {
6429 mIncludePad = includepad;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006430
Gilles Debunne22378292011-08-12 10:38:52 -07006431 if (mLayout != null) {
6432 nullLayouts();
6433 requestLayout();
6434 invalidate();
6435 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006436 }
6437 }
6438
Romain Guy4dc4f732009-06-19 15:16:40 -07006439 private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006440
6441 @Override
6442 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
6443 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
6444 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
6445 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
6446 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
6447
6448 int width;
6449 int height;
6450
6451 BoringLayout.Metrics boring = UNKNOWN_BORING;
6452 BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
6453
Doug Feltcb3791202011-07-07 11:57:48 -07006454 if (mTextDir == null) {
6455 resolveTextDirection();
6456 }
6457
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006458 int des = -1;
6459 boolean fromexisting = false;
6460
6461 if (widthMode == MeasureSpec.EXACTLY) {
6462 // Parent has told us how big to be. So be it.
6463 width = widthSize;
6464 } else {
6465 if (mLayout != null && mEllipsize == null) {
6466 des = desired(mLayout);
6467 }
6468
6469 if (des < 0) {
Doug Feltcb3791202011-07-07 11:57:48 -07006470 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006471 if (boring != null) {
6472 mBoring = boring;
6473 }
6474 } else {
6475 fromexisting = true;
6476 }
6477
6478 if (boring == null || boring == UNKNOWN_BORING) {
6479 if (des < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006480 des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006481 }
6482
6483 width = des;
6484 } else {
6485 width = boring.width;
6486 }
6487
6488 final Drawables dr = mDrawables;
6489 if (dr != null) {
6490 width = Math.max(width, dr.mDrawableWidthTop);
6491 width = Math.max(width, dr.mDrawableWidthBottom);
6492 }
6493
6494 if (mHint != null) {
6495 int hintDes = -1;
6496 int hintWidth;
6497
Romain Guy4dc4f732009-06-19 15:16:40 -07006498 if (mHintLayout != null && mEllipsize == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006499 hintDes = desired(mHintLayout);
6500 }
6501
6502 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006503 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006504 if (hintBoring != null) {
6505 mHintBoring = hintBoring;
6506 }
6507 }
6508
6509 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
6510 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006511 hintDes = (int) FloatMath.ceil(
6512 Layout.getDesiredWidth(mHint, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006513 }
6514
6515 hintWidth = hintDes;
6516 } else {
6517 hintWidth = hintBoring.width;
6518 }
6519
6520 if (hintWidth > width) {
6521 width = hintWidth;
6522 }
6523 }
6524
6525 width += getCompoundPaddingLeft() + getCompoundPaddingRight();
6526
6527 if (mMaxWidthMode == EMS) {
6528 width = Math.min(width, mMaxWidth * getLineHeight());
6529 } else {
6530 width = Math.min(width, mMaxWidth);
6531 }
6532
6533 if (mMinWidthMode == EMS) {
6534 width = Math.max(width, mMinWidth * getLineHeight());
6535 } else {
6536 width = Math.max(width, mMinWidth);
6537 }
6538
6539 // Check against our minimum width
6540 width = Math.max(width, getSuggestedMinimumWidth());
6541
6542 if (widthMode == MeasureSpec.AT_MOST) {
6543 width = Math.min(widthSize, width);
6544 }
6545 }
6546
6547 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
6548 int unpaddedWidth = want;
Gilles Debunne9a80a652011-01-31 12:56:07 -08006549
Jeff Brown033a0012011-11-11 15:30:16 -08006550 if (mHorizontallyScrolling) want = VERY_WIDE;
Gilles Debunne9a80a652011-01-31 12:56:07 -08006551
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006552 int hintWant = want;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006553 int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006554
6555 if (mLayout == null) {
6556 makeNewLayout(want, hintWant, boring, hintBoring,
Romain Guy4dc4f732009-06-19 15:16:40 -07006557 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006558 } else {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006559 final boolean layoutChanged = (mLayout.getWidth() != want) ||
6560 (hintWidth != hintWant) ||
6561 (mLayout.getEllipsizedWidth() !=
6562 width - getCompoundPaddingLeft() - getCompoundPaddingRight());
6563
6564 final boolean widthChanged = (mHint == null) &&
6565 (mEllipsize == null) &&
6566 (want > mLayout.getWidth()) &&
6567 (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
6568
6569 final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
6570
6571 if (layoutChanged || maximumChanged) {
6572 if (!maximumChanged && widthChanged) {
6573 mLayout.increaseWidthTo(want);
6574 } else {
6575 makeNewLayout(want, hintWant, boring, hintBoring,
6576 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
6577 }
6578 } else {
6579 // Nothing has changed
6580 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006581 }
6582
6583 if (heightMode == MeasureSpec.EXACTLY) {
6584 // Parent has told us how big to be. So be it.
6585 height = heightSize;
6586 mDesiredHeightAtMeasure = -1;
6587 } else {
6588 int desired = getDesiredHeight();
6589
6590 height = desired;
6591 mDesiredHeightAtMeasure = desired;
6592
6593 if (heightMode == MeasureSpec.AT_MOST) {
Christoffer Gurell1d05c7c2009-10-12 15:53:39 +02006594 height = Math.min(desired, heightSize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006595 }
6596 }
6597
Romain Guy4dc4f732009-06-19 15:16:40 -07006598 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006599 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006600 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006601 }
6602
6603 /*
6604 * We didn't let makeNewLayout() register to bring the cursor into view,
6605 * so do it here if there is any possibility that it is needed.
6606 */
6607 if (mMovement != null ||
6608 mLayout.getWidth() > unpaddedWidth ||
6609 mLayout.getHeight() > unpaddedHeight) {
6610 registerForPreDraw();
6611 } else {
6612 scrollTo(0, 0);
6613 }
6614
6615 setMeasuredDimension(width, height);
6616 }
6617
6618 private int getDesiredHeight() {
Romain Guy4dc4f732009-06-19 15:16:40 -07006619 return Math.max(
6620 getDesiredHeight(mLayout, true),
6621 getDesiredHeight(mHintLayout, mEllipsize != null));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006622 }
6623
6624 private int getDesiredHeight(Layout layout, boolean cap) {
6625 if (layout == null) {
6626 return 0;
6627 }
6628
6629 int linecount = layout.getLineCount();
6630 int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
6631 int desired = layout.getLineTop(linecount);
6632
6633 final Drawables dr = mDrawables;
6634 if (dr != null) {
6635 desired = Math.max(desired, dr.mDrawableHeightLeft);
6636 desired = Math.max(desired, dr.mDrawableHeightRight);
6637 }
6638
6639 desired += pad;
6640
6641 if (mMaxMode == LINES) {
6642 /*
6643 * Don't cap the hint to a certain number of lines.
6644 * (Do cap it, though, if we have a maximum pixel height.)
6645 */
6646 if (cap) {
6647 if (linecount > mMaximum) {
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08006648 desired = layout.getLineTop(mMaximum);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006649
6650 if (dr != null) {
6651 desired = Math.max(desired, dr.mDrawableHeightLeft);
6652 desired = Math.max(desired, dr.mDrawableHeightRight);
6653 }
6654
6655 desired += pad;
6656 linecount = mMaximum;
6657 }
6658 }
6659 } else {
6660 desired = Math.min(desired, mMaximum);
6661 }
6662
6663 if (mMinMode == LINES) {
6664 if (linecount < mMinimum) {
6665 desired += getLineHeight() * (mMinimum - linecount);
6666 }
6667 } else {
6668 desired = Math.max(desired, mMinimum);
6669 }
6670
6671 // Check against our minimum height
6672 desired = Math.max(desired, getSuggestedMinimumHeight());
6673
6674 return desired;
6675 }
6676
6677 /**
6678 * Check whether a change to the existing text layout requires a
6679 * new view layout.
6680 */
6681 private void checkForResize() {
6682 boolean sizeChanged = false;
6683
6684 if (mLayout != null) {
6685 // Check if our width changed
6686 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
6687 sizeChanged = true;
6688 invalidate();
6689 }
6690
6691 // Check if our height changed
6692 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6693 int desiredHeight = getDesiredHeight();
6694
6695 if (desiredHeight != this.getHeight()) {
6696 sizeChanged = true;
6697 }
Romain Guy980a9382010-01-08 15:06:28 -08006698 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006699 if (mDesiredHeightAtMeasure >= 0) {
6700 int desiredHeight = getDesiredHeight();
6701
6702 if (desiredHeight != mDesiredHeightAtMeasure) {
6703 sizeChanged = true;
6704 }
6705 }
6706 }
6707 }
6708
6709 if (sizeChanged) {
6710 requestLayout();
6711 // caller will have already invalidated
6712 }
6713 }
6714
6715 /**
6716 * Check whether entirely new text requires a new view layout
6717 * or merely a new text layout.
6718 */
6719 private void checkForRelayout() {
6720 // If we have a fixed width, we can just swap in a new text layout
6721 // if the text height stays the same or if the view height is fixed.
6722
6723 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6724 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6725 (mHint == null || mHintLayout != null) &&
6726 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6727 // Static width, so try making a new text layout.
6728
6729 int oldht = mLayout.getHeight();
6730 int want = mLayout.getWidth();
6731 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6732
6733 /*
6734 * No need to bring the text into view, since the size is not
6735 * changing (unless we do the requestLayout(), in which case it
6736 * will happen at measure).
6737 */
6738 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
Romain Guye1e0dc82009-11-03 17:21:04 -08006739 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6740 false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006741
Romain Guye1e0dc82009-11-03 17:21:04 -08006742 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6743 // In a fixed-height view, so use our new text layout.
6744 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
Romain Guy980a9382010-01-08 15:06:28 -08006745 mLayoutParams.height != LayoutParams.MATCH_PARENT) {
Romain Guye1e0dc82009-11-03 17:21:04 -08006746 invalidate();
6747 return;
6748 }
6749
6750 // Dynamic height, but height has stayed the same,
6751 // so use our new text layout.
6752 if (mLayout.getHeight() == oldht &&
6753 (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6754 invalidate();
6755 return;
6756 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006757 }
6758
6759 // We lose: the height has changed and we have a dynamic height.
6760 // Request a new view layout using our new text layout.
6761 requestLayout();
6762 invalidate();
6763 } else {
6764 // Dynamic width, so we have no choice but to request a new
6765 // view layout with a new text layout.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006766 nullLayouts();
6767 requestLayout();
6768 invalidate();
6769 }
6770 }
6771
6772 /**
6773 * Returns true if anything changed.
6774 */
6775 private boolean bringTextIntoView() {
6776 int line = 0;
6777 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6778 line = mLayout.getLineCount() - 1;
6779 }
6780
6781 Layout.Alignment a = mLayout.getParagraphAlignment(line);
6782 int dir = mLayout.getParagraphDirection(line);
6783 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6784 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6785 int ht = mLayout.getHeight();
6786
6787 int scrollx, scrolly;
6788
Doug Felt25b9f422011-07-11 13:48:37 -07006789 // Convert to left, center, or right alignment.
6790 if (a == Layout.Alignment.ALIGN_NORMAL) {
6791 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT :
6792 Layout.Alignment.ALIGN_RIGHT;
6793 } else if (a == Layout.Alignment.ALIGN_OPPOSITE){
6794 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT :
6795 Layout.Alignment.ALIGN_LEFT;
6796 }
6797
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006798 if (a == Layout.Alignment.ALIGN_CENTER) {
6799 /*
6800 * Keep centered if possible, or, if it is too wide to fit,
6801 * keep leading edge in view.
6802 */
6803
6804 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6805 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6806
6807 if (right - left < hspace) {
6808 scrollx = (right + left) / 2 - hspace / 2;
6809 } else {
6810 if (dir < 0) {
6811 scrollx = right - hspace;
6812 } else {
6813 scrollx = left;
6814 }
6815 }
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006816 } else if (a == Layout.Alignment.ALIGN_RIGHT) {
Doug Felt25b9f422011-07-11 13:48:37 -07006817 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6818 scrollx = right - hspace;
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006819 } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
6820 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006821 }
6822
6823 if (ht < vspace) {
6824 scrolly = 0;
6825 } else {
6826 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6827 scrolly = ht - vspace;
6828 } else {
6829 scrolly = 0;
6830 }
6831 }
6832
6833 if (scrollx != mScrollX || scrolly != mScrollY) {
6834 scrollTo(scrollx, scrolly);
6835 return true;
6836 } else {
6837 return false;
6838 }
6839 }
6840
6841 /**
6842 * Move the point, specified by the offset, into the view if it is needed.
6843 * This has to be called after layout. Returns true if anything changed.
6844 */
6845 public boolean bringPointIntoView(int offset) {
6846 boolean changed = false;
6847
Gilles Debunne176ee3d2011-07-16 13:28:41 -07006848 if (mLayout == null) return changed;
6849
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006850 int line = mLayout.getLineForOffset(offset);
6851
6852 // FIXME: Is it okay to truncate this, or should we round?
6853 final int x = (int)mLayout.getPrimaryHorizontal(offset);
6854 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006855 final int bottom = mLayout.getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006856
6857 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6858 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6859 int ht = mLayout.getHeight();
6860
6861 int grav;
6862
6863 switch (mLayout.getParagraphAlignment(line)) {
Doug Felt25b9f422011-07-11 13:48:37 -07006864 case ALIGN_LEFT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006865 grav = 1;
6866 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006867 case ALIGN_RIGHT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006868 grav = -1;
6869 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006870 case ALIGN_NORMAL:
6871 grav = mLayout.getParagraphDirection(line);
6872 break;
6873 case ALIGN_OPPOSITE:
6874 grav = -mLayout.getParagraphDirection(line);
6875 break;
6876 case ALIGN_CENTER:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006877 default:
6878 grav = 0;
Doug Felt25b9f422011-07-11 13:48:37 -07006879 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006880 }
6881
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006882 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6883 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6884
6885 int hslack = (bottom - top) / 2;
6886 int vslack = hslack;
6887
6888 if (vslack > vspace / 4)
6889 vslack = vspace / 4;
6890 if (hslack > hspace / 4)
6891 hslack = hspace / 4;
6892
6893 int hs = mScrollX;
6894 int vs = mScrollY;
6895
6896 if (top - vs < vslack)
6897 vs = top - vslack;
6898 if (bottom - vs > vspace - vslack)
6899 vs = bottom - (vspace - vslack);
6900 if (ht - vs < vspace)
6901 vs = ht - vspace;
6902 if (0 - vs > 0)
6903 vs = 0;
6904
6905 if (grav != 0) {
6906 if (x - hs < hslack) {
6907 hs = x - hslack;
6908 }
6909 if (x - hs > hspace - hslack) {
6910 hs = x - (hspace - hslack);
6911 }
6912 }
6913
6914 if (grav < 0) {
6915 if (left - hs > 0)
6916 hs = left;
6917 if (right - hs < hspace)
6918 hs = right - hspace;
6919 } else if (grav > 0) {
6920 if (right - hs < hspace)
6921 hs = right - hspace;
6922 if (left - hs > 0)
6923 hs = left;
6924 } else /* grav == 0 */ {
6925 if (right - left <= hspace) {
6926 /*
6927 * If the entire text fits, center it exactly.
6928 */
6929 hs = left - (hspace - (right - left)) / 2;
6930 } else if (x > right - hslack) {
6931 /*
6932 * If we are near the right edge, keep the right edge
6933 * at the edge of the view.
6934 */
6935 hs = right - hspace;
6936 } else if (x < left + hslack) {
6937 /*
6938 * If we are near the left edge, keep the left edge
6939 * at the edge of the view.
6940 */
6941 hs = left;
6942 } else if (left > hs) {
6943 /*
6944 * Is there whitespace visible at the left? Fix it if so.
6945 */
6946 hs = left;
6947 } else if (right < hs + hspace) {
6948 /*
6949 * Is there whitespace visible at the right? Fix it if so.
6950 */
6951 hs = right - hspace;
6952 } else {
6953 /*
6954 * Otherwise, float as needed.
6955 */
6956 if (x - hs < hslack) {
6957 hs = x - hslack;
6958 }
6959 if (x - hs > hspace - hslack) {
6960 hs = x - (hspace - hslack);
6961 }
6962 }
6963 }
6964
6965 if (hs != mScrollX || vs != mScrollY) {
6966 if (mScroller == null) {
6967 scrollTo(hs, vs);
6968 } else {
6969 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6970 int dx = hs - mScrollX;
6971 int dy = vs - mScrollY;
6972
6973 if (duration > ANIMATED_SCROLL_GAP) {
6974 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
Mike Cleronf116bf82009-09-27 19:14:12 -07006975 awakenScrollBars(mScroller.getDuration());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006976 invalidate();
6977 } else {
6978 if (!mScroller.isFinished()) {
6979 mScroller.abortAnimation();
6980 }
6981
6982 scrollBy(dx, dy);
6983 }
6984
6985 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6986 }
6987
6988 changed = true;
6989 }
6990
6991 if (isFocused()) {
Gilles Debunne716dbf62011-03-07 18:12:10 -08006992 // This offsets because getInterestingRect() is in terms of viewport coordinates, but
6993 // requestRectangleOnScreen() is in terms of content coordinates.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006994
Gilles Debunne716dbf62011-03-07 18:12:10 -08006995 if (mTempRect == null) mTempRect = new Rect();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07006996 // The offsets here are to ensure the rectangle we are using is
6997 // within our view bounds, in case the cursor is on the far left
6998 // or right. If it isn't withing the bounds, then this request
6999 // will be ignored.
7000 mTempRect.set(x - 2, top, x + 2, bottom);
Gilles Debunne716dbf62011-03-07 18:12:10 -08007001 getInterestingRect(mTempRect, line);
7002 mTempRect.offset(mScrollX, mScrollY);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007003
Gilles Debunne716dbf62011-03-07 18:12:10 -08007004 if (requestRectangleOnScreen(mTempRect)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007005 changed = true;
7006 }
7007 }
7008
7009 return changed;
7010 }
7011
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007012 /**
7013 * Move the cursor, if needed, so that it is at an offset that is visible
7014 * to the user. This will not move the cursor if it represents more than
7015 * one character (a selection range). This will only work if the
7016 * TextView contains spannable text; otherwise it will do nothing.
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07007017 *
Gilles Debunne57f4e5b2010-06-21 16:21:51 -07007018 * @return True if the cursor was actually moved, false otherwise.
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007019 */
7020 public boolean moveCursorToVisibleOffset() {
7021 if (!(mText instanceof Spannable)) {
7022 return false;
7023 }
Gilles Debunne05336272010-07-09 20:13:45 -07007024 int start = getSelectionStart();
7025 int end = getSelectionEnd();
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007026 if (start != end) {
7027 return false;
7028 }
7029
7030 // First: make sure the line is visible on screen:
7031
7032 int line = mLayout.getLineForOffset(start);
7033
7034 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007035 final int bottom = mLayout.getLineTop(line + 1);
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007036 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
7037 int vslack = (bottom - top) / 2;
7038 if (vslack > vspace / 4)
7039 vslack = vspace / 4;
7040 final int vs = mScrollY;
7041
7042 if (top < (vs+vslack)) {
7043 line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
7044 } else if (bottom > (vspace+vs-vslack)) {
7045 line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
7046 }
7047
7048 // Next: make sure the character is visible on screen:
7049
7050 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
7051 final int hs = mScrollX;
7052 final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
7053 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
7054
Doug Feltc982f602010-05-25 11:51:40 -07007055 // line might contain bidirectional text
7056 final int lowChar = leftChar < rightChar ? leftChar : rightChar;
7057 final int highChar = leftChar > rightChar ? leftChar : rightChar;
7058
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007059 int newStart = start;
Doug Feltc982f602010-05-25 11:51:40 -07007060 if (newStart < lowChar) {
7061 newStart = lowChar;
7062 } else if (newStart > highChar) {
7063 newStart = highChar;
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007064 }
7065
7066 if (newStart != start) {
7067 Selection.setSelection((Spannable)mText, newStart);
7068 return true;
7069 }
7070
7071 return false;
7072 }
7073
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007074 @Override
7075 public void computeScroll() {
7076 if (mScroller != null) {
7077 if (mScroller.computeScrollOffset()) {
7078 mScrollX = mScroller.getCurrX();
7079 mScrollY = mScroller.getCurrY();
Romain Guy0fd89bf2011-01-26 15:41:30 -08007080 invalidateParentCaches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007081 postInvalidate(); // So we draw again
7082 }
7083 }
7084 }
7085
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007086 private void getInterestingRect(Rect r, int line) {
7087 convertFromViewportToContentCoordinates(r);
7088
7089 // Rectangle can can be expanded on first and last line to take
7090 // padding into account.
7091 // TODO Take left/right padding into account too?
7092 if (line == 0) r.top -= getExtendedPaddingTop();
7093 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
7094 }
7095
7096 private void convertFromViewportToContentCoordinates(Rect r) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007097 final int horizontalOffset = viewportToContentHorizontalOffset();
7098 r.left += horizontalOffset;
7099 r.right += horizontalOffset;
7100
7101 final int verticalOffset = viewportToContentVerticalOffset();
7102 r.top += verticalOffset;
7103 r.bottom += verticalOffset;
7104 }
7105
7106 private int viewportToContentHorizontalOffset() {
7107 return getCompoundPaddingLeft() - mScrollX;
7108 }
7109
7110 private int viewportToContentVerticalOffset() {
7111 int offset = getExtendedPaddingTop() - mScrollY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007112 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007113 offset += getVerticalOffset(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007114 }
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007115 return offset;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007116 }
7117
7118 @Override
7119 public void debug(int depth) {
7120 super.debug(depth);
7121
7122 String output = debugIndent(depth);
7123 output += "frame={" + mLeft + ", " + mTop + ", " + mRight
7124 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
7125 + "} ";
7126
7127 if (mText != null) {
7128
7129 output += "mText=\"" + mText + "\" ";
7130 if (mLayout != null) {
7131 output += "mLayout width=" + mLayout.getWidth()
7132 + " height=" + mLayout.getHeight();
7133 }
7134 } else {
7135 output += "mText=NULL";
7136 }
7137 Log.d(VIEW_LOG_TAG, output);
7138 }
7139
7140 /**
7141 * Convenience for {@link Selection#getSelectionStart}.
7142 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007143 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007144 public int getSelectionStart() {
7145 return Selection.getSelectionStart(getText());
7146 }
7147
7148 /**
7149 * Convenience for {@link Selection#getSelectionEnd}.
7150 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007151 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007152 public int getSelectionEnd() {
7153 return Selection.getSelectionEnd(getText());
7154 }
7155
7156 /**
7157 * Return true iff there is a selection inside this text view.
7158 */
7159 public boolean hasSelection() {
Gilles Debunne03789e82010-09-07 19:07:17 -07007160 final int selectionStart = getSelectionStart();
7161 final int selectionEnd = getSelectionEnd();
7162
7163 return selectionStart >= 0 && selectionStart != selectionEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007164 }
7165
7166 /**
7167 * Sets the properties of this field (lines, horizontally scrolling,
7168 * transformation method) to be for a single-line input.
7169 *
7170 * @attr ref android.R.styleable#TextView_singleLine
7171 */
7172 public void setSingleLine() {
7173 setSingleLine(true);
7174 }
7175
7176 /**
Adam Powell7f8f79a2011-07-07 18:35:54 -07007177 * Sets the properties of this field to transform input to ALL CAPS
7178 * display. This may use a "small caps" formatting if available.
7179 * This setting will be ignored if this field is editable or selectable.
7180 *
7181 * This call replaces the current transformation method. Disabling this
7182 * will not necessarily restore the previous behavior from before this
7183 * was enabled.
7184 *
7185 * @see #setTransformationMethod(TransformationMethod)
7186 * @attr ref android.R.styleable#TextView_textAllCaps
7187 */
7188 public void setAllCaps(boolean allCaps) {
7189 if (allCaps) {
7190 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
7191 } else {
7192 setTransformationMethod(null);
7193 }
7194 }
7195
7196 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007197 * If true, sets the properties of this field (number of lines, horizontally scrolling,
7198 * transformation method) to be for a single-line input; if false, restores these to the default
7199 * conditions.
7200 *
7201 * Note that the default conditions are not necessarily those that were in effect prior this
7202 * method, and you may want to reset these properties to your custom values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007203 *
7204 * @attr ref android.R.styleable#TextView_singleLine
7205 */
7206 @android.view.RemotableViewMethod
7207 public void setSingleLine(boolean singleLine) {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007208 // Could be used, but may break backward compatibility.
7209 // if (mSingleLine == singleLine) return;
Gilles Debunned7483bf2010-11-10 10:47:45 -08007210 setInputTypeSingleLine(singleLine);
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007211 applySingleLine(singleLine, true, true);
Gilles Debunned7483bf2010-11-10 10:47:45 -08007212 }
7213
7214 /**
7215 * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
7216 * @param singleLine
7217 */
7218 private void setInputTypeSingleLine(boolean singleLine) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007219 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007220 if (singleLine) {
7221 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7222 } else {
7223 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7224 }
7225 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007226 }
7227
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007228 private void applySingleLine(boolean singleLine, boolean applyTransformation,
7229 boolean changeMaxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007230 mSingleLine = singleLine;
7231 if (singleLine) {
7232 setLines(1);
7233 setHorizontallyScrolling(true);
7234 if (applyTransformation) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007235 setTransformationMethod(SingleLineTransformationMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007236 }
7237 } else {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007238 if (changeMaxLines) {
7239 setMaxLines(Integer.MAX_VALUE);
7240 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007241 setHorizontallyScrolling(false);
7242 if (applyTransformation) {
7243 setTransformationMethod(null);
7244 }
7245 }
7246 }
Gilles Debunneb2316962010-12-21 17:32:43 -08007247
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007248 /**
7249 * Causes words in the text that are longer than the view is wide
7250 * to be ellipsized instead of broken in the middle. You may also
7251 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
Kenny Roote855d132009-06-11 11:00:42 -05007252 * to constrain the text to a single line. Use <code>null</code>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007253 * to turn off ellipsizing.
7254 *
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007255 * If {@link #setMaxLines} has been used to set two or more lines,
Gilles Debunne6435a562011-08-04 21:22:30 -07007256 * {@link android.text.TextUtils.TruncateAt#END} and
7257 * {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported
7258 * (other ellipsizing types will not do anything).
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007259 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007260 * @attr ref android.R.styleable#TextView_ellipsize
7261 */
7262 public void setEllipsize(TextUtils.TruncateAt where) {
Gilles Debunne22378292011-08-12 10:38:52 -07007263 // TruncateAt is an enum. != comparison is ok between these singleton objects.
7264 if (mEllipsize != where) {
7265 mEllipsize = where;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007266
Gilles Debunne22378292011-08-12 10:38:52 -07007267 if (mLayout != null) {
7268 nullLayouts();
7269 requestLayout();
7270 invalidate();
7271 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007272 }
7273 }
7274
7275 /**
7276 * Sets how many times to repeat the marquee animation. Only applied if the
7277 * TextView has marquee enabled. Set to -1 to repeat indefinitely.
7278 *
7279 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
7280 */
7281 public void setMarqueeRepeatLimit(int marqueeLimit) {
7282 mMarqueeRepeatLimit = marqueeLimit;
7283 }
7284
7285 /**
7286 * Returns where, if anywhere, words that are longer than the view
7287 * is wide should be ellipsized.
7288 */
7289 @ViewDebug.ExportedProperty
7290 public TextUtils.TruncateAt getEllipsize() {
7291 return mEllipsize;
7292 }
7293
7294 /**
7295 * Set the TextView so that when it takes focus, all the text is
7296 * selected.
7297 *
7298 * @attr ref android.R.styleable#TextView_selectAllOnFocus
7299 */
7300 @android.view.RemotableViewMethod
7301 public void setSelectAllOnFocus(boolean selectAllOnFocus) {
7302 mSelectAllOnFocus = selectAllOnFocus;
7303
7304 if (selectAllOnFocus && !(mText instanceof Spannable)) {
7305 setText(mText, BufferType.SPANNABLE);
7306 }
7307 }
7308
7309 /**
7310 * Set whether the cursor is visible. The default is true.
7311 *
7312 * @attr ref android.R.styleable#TextView_cursorVisible
7313 */
7314 @android.view.RemotableViewMethod
7315 public void setCursorVisible(boolean visible) {
Gilles Debunne3d010062011-02-18 14:16:41 -08007316 if (mCursorVisible != visible) {
7317 mCursorVisible = visible;
7318 invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007319
Gilles Debunne3d010062011-02-18 14:16:41 -08007320 makeBlink();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007321
Gilles Debunne3d010062011-02-18 14:16:41 -08007322 // InsertionPointCursorController depends on mCursorVisible
7323 prepareCursorControllers();
7324 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007325 }
7326
Gilles Debunne98dbfd42011-01-24 12:54:10 -08007327 private boolean isCursorVisible() {
7328 return mCursorVisible && isTextEditable();
7329 }
7330
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007331 private boolean canMarquee() {
7332 int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
Adam Powell282e3772011-08-30 16:51:11 -07007333 return width > 0 && (mLayout.getLineWidth(0) > width ||
7334 (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null &&
7335 mSavedMarqueeModeLayout.getLineWidth(0) > width));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007336 }
7337
7338 private void startMarquee() {
Romain Guy4dc4f732009-06-19 15:16:40 -07007339 // Do not ellipsize EditText
7340 if (mInput != null) return;
7341
Romain Guy939151f2009-04-08 14:22:40 -07007342 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
7343 return;
7344 }
7345
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007346 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
7347 getLineCount() == 1 && canMarquee()) {
Romain Guy939151f2009-04-08 14:22:40 -07007348
Adam Powell282e3772011-08-30 16:51:11 -07007349 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
7350 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
7351 final Layout tmp = mLayout;
7352 mLayout = mSavedMarqueeModeLayout;
7353 mSavedMarqueeModeLayout = tmp;
7354 setHorizontalFadingEdgeEnabled(true);
7355 requestLayout();
7356 invalidate();
7357 }
7358
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007359 if (mMarquee == null) mMarquee = new Marquee(this);
7360 mMarquee.start(mMarqueeRepeatLimit);
7361 }
7362 }
7363
7364 private void stopMarquee() {
7365 if (mMarquee != null && !mMarquee.isStopped()) {
7366 mMarquee.stop();
7367 }
Adam Powell282e3772011-08-30 16:51:11 -07007368
7369 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
7370 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
7371 final Layout tmp = mSavedMarqueeModeLayout;
7372 mSavedMarqueeModeLayout = mLayout;
7373 mLayout = tmp;
7374 setHorizontalFadingEdgeEnabled(false);
7375 requestLayout();
7376 invalidate();
7377 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007378 }
7379
7380 private void startStopMarquee(boolean start) {
7381 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7382 if (start) {
7383 startMarquee();
7384 } else {
7385 stopMarquee();
7386 }
7387 }
7388 }
7389
7390 private static final class Marquee extends Handler {
7391 // TODO: Add an option to configure this
Romain Guy939151f2009-04-08 14:22:40 -07007392 private static final float MARQUEE_DELTA_MAX = 0.07f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007393 private static final int MARQUEE_DELAY = 1200;
7394 private static final int MARQUEE_RESTART_DELAY = 1200;
7395 private static final int MARQUEE_RESOLUTION = 1000 / 30;
7396 private static final int MARQUEE_PIXELS_PER_SECOND = 30;
7397
7398 private static final byte MARQUEE_STOPPED = 0x0;
7399 private static final byte MARQUEE_STARTING = 0x1;
7400 private static final byte MARQUEE_RUNNING = 0x2;
7401
7402 private static final int MESSAGE_START = 0x1;
7403 private static final int MESSAGE_TICK = 0x2;
7404 private static final int MESSAGE_RESTART = 0x3;
7405
7406 private final WeakReference<TextView> mView;
7407
7408 private byte mStatus = MARQUEE_STOPPED;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007409 private final float mScrollUnit;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007410 private float mMaxScroll;
Romain Guyc2303192009-04-03 17:37:18 -07007411 float mMaxFadeScroll;
7412 private float mGhostStart;
7413 private float mGhostOffset;
7414 private float mFadeStop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007415 private int mRepeatLimit;
7416
7417 float mScroll;
7418
7419 Marquee(TextView v) {
7420 final float density = v.getContext().getResources().getDisplayMetrics().density;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007421 mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007422 mView = new WeakReference<TextView>(v);
7423 }
7424
7425 @Override
7426 public void handleMessage(Message msg) {
7427 switch (msg.what) {
7428 case MESSAGE_START:
7429 mStatus = MARQUEE_RUNNING;
7430 tick();
7431 break;
7432 case MESSAGE_TICK:
7433 tick();
7434 break;
7435 case MESSAGE_RESTART:
7436 if (mStatus == MARQUEE_RUNNING) {
7437 if (mRepeatLimit >= 0) {
7438 mRepeatLimit--;
7439 }
7440 start(mRepeatLimit);
7441 }
7442 break;
7443 }
7444 }
7445
7446 void tick() {
7447 if (mStatus != MARQUEE_RUNNING) {
7448 return;
7449 }
7450
7451 removeMessages(MESSAGE_TICK);
7452
7453 final TextView textView = mView.get();
7454 if (textView != null && (textView.isFocused() || textView.isSelected())) {
7455 mScroll += mScrollUnit;
7456 if (mScroll > mMaxScroll) {
7457 mScroll = mMaxScroll;
7458 sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
7459 } else {
7460 sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
7461 }
7462 textView.invalidate();
7463 }
7464 }
7465
7466 void stop() {
7467 mStatus = MARQUEE_STOPPED;
7468 removeMessages(MESSAGE_START);
7469 removeMessages(MESSAGE_RESTART);
7470 removeMessages(MESSAGE_TICK);
7471 resetScroll();
7472 }
7473
7474 private void resetScroll() {
7475 mScroll = 0.0f;
7476 final TextView textView = mView.get();
7477 if (textView != null) textView.invalidate();
7478 }
7479
7480 void start(int repeatLimit) {
7481 if (repeatLimit == 0) {
7482 stop();
7483 return;
7484 }
7485 mRepeatLimit = repeatLimit;
7486 final TextView textView = mView.get();
7487 if (textView != null && textView.mLayout != null) {
7488 mStatus = MARQUEE_STARTING;
7489 mScroll = 0.0f;
Romain Guyc2303192009-04-03 17:37:18 -07007490 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
7491 textView.getCompoundPaddingRight();
7492 final float lineWidth = textView.mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07007493 final float gap = textWidth / 3.0f;
7494 mGhostStart = lineWidth - textWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007495 mMaxScroll = mGhostStart + textWidth;
Romain Guy3373ed62009-05-04 14:13:32 -07007496 mGhostOffset = lineWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007497 mFadeStop = lineWidth + textWidth / 6.0f;
7498 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
7499
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007500 textView.invalidate();
7501 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
7502 }
7503 }
7504
Romain Guyc2303192009-04-03 17:37:18 -07007505 float getGhostOffset() {
7506 return mGhostOffset;
7507 }
7508
7509 boolean shouldDrawLeftFade() {
7510 return mScroll <= mFadeStop;
7511 }
7512
7513 boolean shouldDrawGhost() {
7514 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
7515 }
7516
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007517 boolean isRunning() {
7518 return mStatus == MARQUEE_RUNNING;
7519 }
7520
7521 boolean isStopped() {
7522 return mStatus == MARQUEE_STOPPED;
7523 }
7524 }
7525
7526 /**
Gilles Debunne4469e602011-03-09 14:38:04 -08007527 * This method is called when the text is changed, in case any subclasses
7528 * would like to know.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007529 *
Gilles Debunne4469e602011-03-09 14:38:04 -08007530 * Within <code>text</code>, the <code>lengthAfter</code> characters
7531 * beginning at <code>start</code> have just replaced old text that had
7532 * length <code>lengthBefore</code>. It is an error to attempt to make
7533 * changes to <code>text</code> from this callback.
7534 *
7535 * @param text The text the TextView is displaying
7536 * @param start The offset of the start of the range of the text that was
7537 * modified
7538 * @param lengthBefore The length of the former text that has been replaced
7539 * @param lengthAfter The length of the replacement modified text
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007540 */
Gilles Debunne4469e602011-03-09 14:38:04 -08007541 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
Gilles Debunne6435a562011-08-04 21:22:30 -07007542 // intentionally empty, template pattern method can be overridden by subclasses
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007543 }
7544
7545 /**
7546 * This method is called when the selection has changed, in case any
7547 * subclasses would like to know.
7548 *
7549 * @param selStart The new selection start location.
7550 * @param selEnd The new selection end location.
7551 */
7552 protected void onSelectionChanged(int selStart, int selEnd) {
Svetoslav Ganova0156172011-06-26 17:55:44 -07007553 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007554 }
Svetoslav Ganova0156172011-06-26 17:55:44 -07007555
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007556 /**
7557 * Adds a TextWatcher to the list of those whose methods are called
7558 * whenever this TextView's text changes.
7559 * <p>
7560 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
7561 * not called after {@link #setText} calls. Now, doing {@link #setText}
7562 * if there are any text changed listeners forces the buffer type to
7563 * Editable if it would not otherwise be and does call this method.
7564 */
7565 public void addTextChangedListener(TextWatcher watcher) {
7566 if (mListeners == null) {
7567 mListeners = new ArrayList<TextWatcher>();
7568 }
7569
7570 mListeners.add(watcher);
7571 }
7572
7573 /**
7574 * Removes the specified TextWatcher from the list of those whose
7575 * methods are called
7576 * whenever this TextView's text changes.
7577 */
7578 public void removeTextChangedListener(TextWatcher watcher) {
7579 if (mListeners != null) {
7580 int i = mListeners.indexOf(watcher);
7581
7582 if (i >= 0) {
7583 mListeners.remove(i);
7584 }
7585 }
7586 }
7587
Gilles Debunne6435a562011-08-04 21:22:30 -07007588 private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007589 if (mListeners != null) {
7590 final ArrayList<TextWatcher> list = mListeners;
7591 final int count = list.size();
7592 for (int i = 0; i < count; i++) {
7593 list.get(i).beforeTextChanged(text, start, before, after);
7594 }
7595 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007596
7597 // The spans that are inside or intersect the modified region no longer make sense
7598 removeIntersectingSpans(start, start + before, SpellCheckSpan.class);
7599 removeIntersectingSpans(start, start + before, SuggestionSpan.class);
7600 }
7601
7602 // Removes all spans that are inside or actually overlap the start..end range
7603 private <T> void removeIntersectingSpans(int start, int end, Class<T> type) {
7604 if (!(mText instanceof Editable)) return;
7605 Editable text = (Editable) mText;
7606
7607 T[] spans = text.getSpans(start, end, type);
7608 final int length = spans.length;
7609 for (int i = 0; i < length; i++) {
7610 final int s = text.getSpanStart(spans[i]);
7611 final int e = text.getSpanEnd(spans[i]);
Gilles Debunneb062e812011-09-27 14:58:37 -07007612 // Spans that are adjacent to the edited region will be handled in
7613 // updateSpellCheckSpans. Result depends on what will be added (space or text)
Gilles Debunne6435a562011-08-04 21:22:30 -07007614 if (e == start || s == end) break;
7615 text.removeSpan(spans[i]);
7616 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007617 }
7618
7619 /**
7620 * Not private so it can be called from an inner class without going
7621 * through a thunk.
7622 */
Gilles Debunne6435a562011-08-04 21:22:30 -07007623 void sendOnTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007624 if (mListeners != null) {
7625 final ArrayList<TextWatcher> list = mListeners;
7626 final int count = list.size();
7627 for (int i = 0; i < count; i++) {
7628 list.get(i).onTextChanged(text, start, before, after);
7629 }
7630 }
Gilles Debunne1a22db22011-11-20 22:13:21 +01007631
Gilles Debunnec115fa02011-12-07 13:38:31 -08007632 updateSpellCheckSpans(start, start + after, false);
Gilles Debunne1a22db22011-11-20 22:13:21 +01007633
7634 // Hide the controllers as soon as text is modified (typing, procedural...)
7635 // We do not hide the span controllers, since they can be added when a new text is
7636 // inserted into the text view (voice IME).
7637 hideCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007638 }
7639
7640 /**
7641 * Not private so it can be called from an inner class without going
7642 * through a thunk.
7643 */
7644 void sendAfterTextChanged(Editable text) {
7645 if (mListeners != null) {
7646 final ArrayList<TextWatcher> list = mListeners;
7647 final int count = list.size();
7648 for (int i = 0; i < count; i++) {
7649 list.get(i).afterTextChanged(text);
7650 }
7651 }
7652 }
7653
7654 /**
7655 * Not private so it can be called from an inner class without going
7656 * through a thunk.
7657 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007658 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007659 final InputMethodState ims = mInputMethodState;
7660 if (ims == null || ims.mBatchEditNesting == 0) {
7661 updateAfterEdit();
7662 }
7663 if (ims != null) {
7664 ims.mContentChanged = true;
7665 if (ims.mChangedStart < 0) {
7666 ims.mChangedStart = start;
7667 ims.mChangedEnd = start+before;
7668 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01007669 ims.mChangedStart = Math.min(ims.mChangedStart, start);
7670 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007671 }
7672 ims.mChangedDelta += after-before;
7673 }
Gilles Debunne186aaf92011-09-16 14:26:12 -07007674
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007675 sendOnTextChanged(buffer, start, before, after);
7676 onTextChanged(buffer, start, before, after);
7677 }
7678
7679 /**
7680 * Not private so it can be called from an inner class without going
7681 * through a thunk.
7682 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007683 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007684 // XXX Make the start and end move together if this ends up
7685 // spending too much time invalidating.
7686
7687 boolean selChanged = false;
7688 int newSelStart=-1, newSelEnd=-1;
7689
7690 final InputMethodState ims = mInputMethodState;
7691
7692 if (what == Selection.SELECTION_END) {
7693 mHighlightPathBogus = true;
7694 selChanged = true;
7695 newSelEnd = newStart;
7696
7697 if (!isFocused()) {
7698 mSelectionMoved = true;
7699 }
7700
7701 if (oldStart >= 0 || newStart >= 0) {
7702 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
7703 registerForPreDraw();
Gilles Debunne3d010062011-02-18 14:16:41 -08007704 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007705 }
7706 }
7707
7708 if (what == Selection.SELECTION_START) {
7709 mHighlightPathBogus = true;
7710 selChanged = true;
7711 newSelStart = newStart;
7712
7713 if (!isFocused()) {
7714 mSelectionMoved = true;
7715 }
7716
7717 if (oldStart >= 0 || newStart >= 0) {
7718 int end = Selection.getSelectionEnd(buf);
7719 invalidateCursor(end, oldStart, newStart);
7720 }
7721 }
7722
7723 if (selChanged) {
7724 if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
7725 if (newSelStart < 0) {
7726 newSelStart = Selection.getSelectionStart(buf);
7727 }
7728 if (newSelEnd < 0) {
7729 newSelEnd = Selection.getSelectionEnd(buf);
7730 }
7731 onSelectionChanged(newSelStart, newSelEnd);
7732 }
7733 }
Gilles Debunne8615ac92011-11-29 15:25:03 -08007734
7735 if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007736 if (ims == null || ims.mBatchEditNesting == 0) {
7737 invalidate();
7738 mHighlightPathBogus = true;
7739 checkForResize();
7740 } else {
7741 ims.mContentChanged = true;
7742 }
7743 }
7744
7745 if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7746 mHighlightPathBogus = true;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007747 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7748 ims.mSelectionModeChanged = true;
7749 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007750
7751 if (Selection.getSelectionStart(buf) >= 0) {
7752 if (ims == null || ims.mBatchEditNesting == 0) {
7753 invalidateCursor();
7754 } else {
7755 ims.mCursorChanged = true;
7756 }
7757 }
7758 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007759
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007760 if (what instanceof ParcelableSpan) {
7761 // If this is a span that can be sent to a remote process,
7762 // the current extract editor would be interested in it.
7763 if (ims != null && ims.mExtracting != null) {
7764 if (ims.mBatchEditNesting != 0) {
7765 if (oldStart >= 0) {
7766 if (ims.mChangedStart > oldStart) {
7767 ims.mChangedStart = oldStart;
7768 }
7769 if (ims.mChangedStart > oldEnd) {
7770 ims.mChangedStart = oldEnd;
7771 }
7772 }
7773 if (newStart >= 0) {
7774 if (ims.mChangedStart > newStart) {
7775 ims.mChangedStart = newStart;
7776 }
7777 if (ims.mChangedStart > newEnd) {
7778 ims.mChangedStart = newEnd;
7779 }
7780 }
7781 } else {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007782 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007783 + oldStart + "-" + oldEnd + ","
7784 + newStart + "-" + newEnd + what);
7785 ims.mContentChanged = true;
7786 }
7787 }
7788 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007789
Gilles Debunnec115fa02011-12-07 13:38:31 -08007790 if (mSpellChecker != null && newStart < 0 && what instanceof SpellCheckSpan) {
7791 mSpellChecker.removeSpellCheckSpan((SpellCheckSpan) what);
Gilles Debunne6435a562011-08-04 21:22:30 -07007792 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007793 }
7794
Gilles Debunne6435a562011-08-04 21:22:30 -07007795 /**
7796 * Create new SpellCheckSpans on the modified region.
7797 */
Gilles Debunnec115fa02011-12-07 13:38:31 -08007798 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07007799 if (isTextEditable() && isSuggestionsEnabled()) {
Gilles Debunnec115fa02011-12-07 13:38:31 -08007800 if (mSpellChecker == null && createSpellChecker) {
7801 mSpellChecker = new SpellChecker(this);
7802 }
7803 if (mSpellChecker != null) {
7804 mSpellChecker.spellCheck(start, end);
7805 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007806 }
7807 }
7808
Luca Zanoline6d36822011-08-30 18:04:34 +01007809 /**
7810 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
7811 * pop-up should be displayed.
7812 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007813 private class EasyEditSpanController {
Luca Zanoline6d36822011-08-30 18:04:34 +01007814
Luca Zanolin1564fc72011-09-07 00:01:28 +01007815 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
Luca Zanoline6d36822011-08-30 18:04:34 +01007816
Luca Zanolin1564fc72011-09-07 00:01:28 +01007817 private EasyEditPopupWindow mPopupWindow;
7818
7819 private EasyEditSpan mEasyEditSpan;
7820
7821 private Runnable mHidePopup;
Luca Zanoline6d36822011-08-30 18:04:34 +01007822
7823 private void hide() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007824 if (mPopupWindow != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007825 mPopupWindow.hide();
Luca Zanolin1564fc72011-09-07 00:01:28 +01007826 TextView.this.removeCallbacks(mHidePopup);
Luca Zanoline6d36822011-08-30 18:04:34 +01007827 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007828 removeSpans(mText);
7829 mEasyEditSpan = null;
Luca Zanoline6d36822011-08-30 18:04:34 +01007830 }
7831
7832 /**
7833 * Monitors the changes in the text.
7834 *
7835 * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
7836 * as the notifications are not sent when a spannable (with spans) is inserted.
7837 */
7838 public void onTextChange(CharSequence buffer) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007839 adjustSpans(mText);
7840
7841 if (getWindowVisibility() != View.VISIBLE) {
7842 // The window is not visible yet, ignore the text change.
7843 return;
Luca Zanoline6d36822011-08-30 18:04:34 +01007844 }
7845
Luca Zanolin1564fc72011-09-07 00:01:28 +01007846 if (mLayout == null) {
7847 // The view has not been layout yet, ignore the text change
7848 return;
7849 }
7850
7851 InputMethodManager imm = InputMethodManager.peekInstance();
7852 if (!(TextView.this instanceof ExtractEditText)
7853 && imm != null && imm.isFullscreenMode()) {
7854 // The input is in extract mode. We do not have to handle the easy edit in the
7855 // original TextView, as the ExtractEditText will do
7856 return;
7857 }
7858
7859 // Remove the current easy edit span, as the text changed, and remove the pop-up
7860 // (if any)
7861 if (mEasyEditSpan != null) {
7862 if (mText instanceof Spannable) {
7863 ((Spannable) mText).removeSpan(mEasyEditSpan);
7864 }
7865 mEasyEditSpan = null;
7866 }
7867 if (mPopupWindow != null && mPopupWindow.isShowing()) {
7868 mPopupWindow.hide();
7869 }
7870
7871 // Display the new easy edit span (if any).
Luca Zanoline6d36822011-08-30 18:04:34 +01007872 if (buffer instanceof Spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007873 mEasyEditSpan = getSpan((Spanned) buffer);
7874 if (mEasyEditSpan != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007875 if (mPopupWindow == null) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007876 mPopupWindow = new EasyEditPopupWindow();
7877 mHidePopup = new Runnable() {
7878 @Override
7879 public void run() {
7880 hide();
7881 }
7882 };
Luca Zanoline6d36822011-08-30 18:04:34 +01007883 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007884 mPopupWindow.show(mEasyEditSpan);
7885 TextView.this.removeCallbacks(mHidePopup);
7886 TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
7887 }
7888 }
7889 }
7890
7891 /**
7892 * Adjusts the spans by removing all of them except the last one.
7893 */
7894 private void adjustSpans(CharSequence buffer) {
7895 // This method enforces that only one easy edit span is attached to the text.
7896 // A better way to enforce this would be to listen for onSpanAdded, but this method
7897 // cannot be used in this scenario as no notification is triggered when a text with
7898 // spans is inserted into a text.
7899 if (buffer instanceof Spannable) {
7900 Spannable spannable = (Spannable) buffer;
7901 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7902 EasyEditSpan.class);
7903 for (int i = 0; i < spans.length - 1; i++) {
7904 spannable.removeSpan(spans[i]);
7905 }
7906 }
7907 }
7908
7909 /**
7910 * Removes all the {@link EasyEditSpan} currently attached.
7911 */
7912 private void removeSpans(CharSequence buffer) {
7913 if (buffer instanceof Spannable) {
7914 Spannable spannable = (Spannable) buffer;
7915 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7916 EasyEditSpan.class);
7917 for (int i = 0; i < spans.length; i++) {
7918 spannable.removeSpan(spans[i]);
Luca Zanoline6d36822011-08-30 18:04:34 +01007919 }
7920 }
7921 }
7922
7923 private EasyEditSpan getSpan(Spanned spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007924 EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
Luca Zanoline6d36822011-08-30 18:04:34 +01007925 EasyEditSpan.class);
Luca Zanolin1564fc72011-09-07 00:01:28 +01007926 if (easyEditSpans.length == 0) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007927 return null;
7928 } else {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007929 return easyEditSpans[0];
Luca Zanoline6d36822011-08-30 18:04:34 +01007930 }
7931 }
7932 }
7933
7934 /**
7935 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Luca Zanolin1564fc72011-09-07 00:01:28 +01007936 * by {@link EasyEditSpanController}.
Luca Zanoline6d36822011-08-30 18:04:34 +01007937 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007938 private class EasyEditPopupWindow extends PinnedPopupWindow
Luca Zanoline6d36822011-08-30 18:04:34 +01007939 implements OnClickListener {
7940 private static final int POPUP_TEXT_LAYOUT =
7941 com.android.internal.R.layout.text_edit_action_popup_text;
7942 private TextView mDeleteTextView;
Luca Zanolin1564fc72011-09-07 00:01:28 +01007943 private EasyEditSpan mEasyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007944
7945 @Override
7946 protected void createPopupWindow() {
7947 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
7948 com.android.internal.R.attr.textSelectHandleWindowStyle);
7949 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
7950 mPopupWindow.setClippingEnabled(true);
7951 }
7952
7953 @Override
7954 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07007955 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
7956 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
7957 mContentView = linearLayout;
Luca Zanoline6d36822011-08-30 18:04:34 +01007958 mContentView.setBackgroundResource(
7959 com.android.internal.R.drawable.text_edit_side_paste_window);
7960
7961 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
7962 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
7963
7964 LayoutParams wrapContent = new LayoutParams(
7965 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
7966
7967 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
7968 mDeleteTextView.setLayoutParams(wrapContent);
7969 mDeleteTextView.setText(com.android.internal.R.string.delete);
7970 mDeleteTextView.setOnClickListener(this);
7971 mContentView.addView(mDeleteTextView);
7972 }
7973
Luca Zanolin1564fc72011-09-07 00:01:28 +01007974 public void show(EasyEditSpan easyEditSpan) {
7975 mEasyEditSpan = easyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007976 super.show();
7977 }
7978
7979 @Override
7980 public void onClick(View view) {
7981 if (view == mDeleteTextView) {
Gilles Debunne39ba6d92011-11-09 05:26:26 +01007982 Editable editable = (Editable) mText;
7983 int start = editable.getSpanStart(mEasyEditSpan);
7984 int end = editable.getSpanEnd(mEasyEditSpan);
7985 if (start >= 0 && end >= 0) {
7986 deleteText_internal(start, end);
7987 }
Luca Zanoline6d36822011-08-30 18:04:34 +01007988 }
7989 }
7990
7991 @Override
7992 protected int getTextOffset() {
7993 // Place the pop-up at the end of the span
7994 Editable editable = (Editable) mText;
Luca Zanolin1564fc72011-09-07 00:01:28 +01007995 return editable.getSpanEnd(mEasyEditSpan);
Luca Zanoline6d36822011-08-30 18:04:34 +01007996 }
7997
7998 @Override
7999 protected int getVerticalLocalPosition(int line) {
8000 return mLayout.getLineBottom(line);
8001 }
8002
8003 @Override
8004 protected int clipVertically(int positionY) {
8005 // As we display the pop-up below the span, no vertical clipping is required.
8006 return positionY;
8007 }
8008 }
8009
Gilles Debunne6435a562011-08-04 21:22:30 -07008010 private class ChangeWatcher implements TextWatcher, SpanWatcher {
svetoslavganov75986cf2009-05-14 22:28:01 -07008011
8012 private CharSequence mBeforeText;
8013
Luca Zanolin1564fc72011-09-07 00:01:28 +01008014 private EasyEditSpanController mEasyEditSpanController;
Luca Zanoline6d36822011-08-30 18:04:34 +01008015
8016 private ChangeWatcher() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008017 mEasyEditSpanController = new EasyEditSpanController();
Luca Zanoline6d36822011-08-30 18:04:34 +01008018 }
8019
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008020 public void beforeTextChanged(CharSequence buffer, int start,
8021 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008022 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008023 + " before=" + before + " after=" + after + ": " + buffer);
svetoslavganov75986cf2009-05-14 22:28:01 -07008024
Amith Yamasani91ccdb52010-01-14 18:56:02 -08008025 if (AccessibilityManager.getInstance(mContext).isEnabled()
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008026 && !isPasswordInputType(mInputType)
8027 && !hasPasswordTransformationMethod()) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008028 mBeforeText = buffer.toString();
8029 }
8030
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008031 TextView.this.sendBeforeTextChanged(buffer, start, before, after);
8032 }
8033
8034 public void onTextChanged(CharSequence buffer, int start,
8035 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008036 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008037 + " before=" + before + " after=" + after + ": " + buffer);
8038 TextView.this.handleTextChanged(buffer, start, before, after);
svetoslavganov75986cf2009-05-14 22:28:01 -07008039
Luca Zanolin1564fc72011-09-07 00:01:28 +01008040 mEasyEditSpanController.onTextChange(buffer);
Luca Zanoline6d36822011-08-30 18:04:34 +01008041
svetoslavganov75986cf2009-05-14 22:28:01 -07008042 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
Gilles Debunne6435a562011-08-04 21:22:30 -07008043 (isFocused() || isSelected() && isShown())) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008044 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
8045 mBeforeText = null;
8046 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008047 }
8048
8049 public void afterTextChanged(Editable buffer) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008050 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008051 TextView.this.sendAfterTextChanged(buffer);
8052
Gilles Debunne6435a562011-08-04 21:22:30 -07008053 if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008054 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
8055 }
8056 }
8057
8058 public void onSpanChanged(Spannable buf,
8059 Object what, int s, int e, int st, int en) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008060 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008061 + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
8062 TextView.this.spanChange(buf, what, s, st, e, en);
8063 }
8064
8065 public void onSpanAdded(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008066 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008067 + " what=" + what + ": " + buf);
8068 TextView.this.spanChange(buf, what, -1, s, -1, e);
8069 }
8070
8071 public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008072 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008073 + " what=" + what + ": " + buf);
8074 TextView.this.spanChange(buf, what, s, -1, e, -1);
8075 }
Luca Zanoline6d36822011-08-30 18:04:34 +01008076
8077 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008078 mEasyEditSpanController.hide();
Luca Zanoline6d36822011-08-30 18:04:34 +01008079 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008080 }
8081
Romain Guydcc490f2010-02-24 17:59:35 -08008082 /**
8083 * @hide
8084 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008085 @Override
Romain Guya440b002010-02-24 15:57:54 -08008086 public void dispatchFinishTemporaryDetach() {
8087 mDispatchTemporaryDetach = true;
8088 super.dispatchFinishTemporaryDetach();
8089 mDispatchTemporaryDetach = false;
8090 }
8091
8092 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008093 public void onStartTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008094 super.onStartTemporaryDetach();
8095 // Only track when onStartTemporaryDetach() is called directly,
8096 // usually because this instance is an editable field in a list
8097 if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
Gilles Debunne4b2274f2011-02-25 15:18:03 -08008098
8099 // Because of View recycling in ListView, there is no easy way to know when a TextView with
8100 // selection becomes visible again. Until a better solution is found, stop text selection
8101 // mode (if any) as soon as this TextView is recycled.
Gilles Debunne4a7199a2011-07-11 14:58:27 -07008102 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008103 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008104
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008105 @Override
8106 public void onFinishTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008107 super.onFinishTemporaryDetach();
8108 // Only track when onStartTemporaryDetach() is called directly,
8109 // usually because this instance is an editable field in a list
8110 if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008111 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008112
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008113 @Override
8114 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
8115 if (mTemporaryDetach) {
8116 // If we are temporarily in the detach state, then do nothing.
8117 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8118 return;
8119 }
8120
8121 mShowCursor = SystemClock.uptimeMillis();
8122
8123 ensureEndedBatchEdit();
Gilles Debunne03789e82010-09-07 19:07:17 -07008124
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008125 if (focused) {
8126 int selStart = getSelectionStart();
8127 int selEnd = getSelectionEnd();
8128
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08008129 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
8130 // mode for these, unless there was a specific selection already started.
8131 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
8132 selEnd == mText.length();
8133 mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
8134
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008135 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008136 // If a tap was used to give focus to that view, move cursor at tap position.
Gilles Debunne64e54a62010-09-07 19:07:17 -07008137 // Has to be done before onTakeFocus, which can be overloaded.
Gilles Debunne380b6042010-10-08 16:12:11 -07008138 final int lastTapPosition = getLastTapPosition();
8139 if (lastTapPosition >= 0) {
8140 Selection.setSelection((Spannable) mText, lastTapPosition);
8141 }
Gilles Debunne2703a422010-08-23 15:14:03 -07008142
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008143 if (mMovement != null) {
8144 mMovement.onTakeFocus(this, (Spannable) mText, direction);
8145 }
8146
Gilles Debunne64e54a62010-09-07 19:07:17 -07008147 // The DecorView does not have focus when the 'Done' ExtractEditText button is
Joe Onoratoc6cc0f82011-04-12 11:53:13 -07008148 // pressed. Since it is the ViewAncestor's mView, it requests focus before
Gilles Debunne64e54a62010-09-07 19:07:17 -07008149 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
8150 // This special case ensure that we keep current selection in that case.
8151 // It would be better to know why the DecorView does not have focus at that time.
Gilles Debunne47fa8e82010-09-07 18:50:07 -07008152 if (((this instanceof ExtractEditText) || mSelectionMoved) &&
8153 selStart >= 0 && selEnd >= 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008154 /*
8155 * Someone intentionally set the selection, so let them
8156 * do whatever it is that they wanted to do instead of
8157 * the default on-focus behavior. We reset the selection
8158 * here instead of just skipping the onTakeFocus() call
8159 * because some movement methods do something other than
8160 * just setting the selection in theirs and we still
8161 * need to go through that path.
8162 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008163 Selection.setSelection((Spannable) mText, selStart, selEnd);
8164 }
Gilles Debunnef170a342010-11-11 11:08:59 -08008165
8166 if (mSelectAllOnFocus) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008167 selectAll();
Gilles Debunnef170a342010-11-11 11:08:59 -08008168 }
8169
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008170 mTouchFocusSelected = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008171 }
8172
8173 mFrozenWithFocus = false;
8174 mSelectionMoved = false;
8175
8176 if (mText instanceof Spannable) {
8177 Spannable sp = (Spannable) mText;
8178 MetaKeyKeyListener.resetMetaState(sp);
8179 }
8180
8181 makeBlink();
8182
8183 if (mError != null) {
8184 showError();
8185 }
8186 } else {
8187 if (mError != null) {
8188 hideError();
8189 }
8190 // Don't leave us in the middle of a batch edit.
8191 onEndBatchEdit();
Gilles Debunne05336272010-07-09 20:13:45 -07008192
Gilles Debunne64e54a62010-09-07 19:07:17 -07008193 if (this instanceof ExtractEditText) {
8194 // terminateTextSelectionMode removes selection, which we want to keep when
8195 // ExtractEditText goes out of focus.
8196 final int selStart = getSelectionStart();
8197 final int selEnd = getSelectionEnd();
Gilles Debunneb7012e842011-02-24 15:40:38 -08008198 hideControllers();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008199 Selection.setSelection((Spannable) mText, selStart, selEnd);
8200 } else {
Gilles Debunneb7012e842011-02-24 15:40:38 -08008201 hideControllers();
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008202 downgradeEasyCorrectionSpans();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008203 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008204
Gilles Debunnee587d832010-11-23 20:20:11 -08008205 // No need to create the controller
Gilles Debunne380b6042010-10-08 16:12:11 -07008206 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008207 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunne380b6042010-10-08 16:12:11 -07008208 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008209 }
8210
8211 startStopMarquee(focused);
8212
8213 if (mTransformation != null) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008214 mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008215 }
8216
8217 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8218 }
8219
Gilles Debunne380b6042010-10-08 16:12:11 -07008220 private int getLastTapPosition() {
Gilles Debunnee587d832010-11-23 20:20:11 -08008221 // No need to create the controller at that point, no last tap position saved
Gilles Debunne528c64882010-10-08 11:56:13 -07008222 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008223 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
Gilles Debunne380b6042010-10-08 16:12:11 -07008224 if (lastTapPosition >= 0) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008225 // Safety check, should not be possible.
Gilles Debunne380b6042010-10-08 16:12:11 -07008226 if (lastTapPosition > mText.length()) {
8227 Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
Gilles Debunne528c64882010-10-08 11:56:13 -07008228 + mText.length() + ")");
Gilles Debunne380b6042010-10-08 16:12:11 -07008229 lastTapPosition = mText.length();
Gilles Debunne528c64882010-10-08 11:56:13 -07008230 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008231 return lastTapPosition;
Gilles Debunne528c64882010-10-08 11:56:13 -07008232 }
8233 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008234
8235 return -1;
Gilles Debunne528c64882010-10-08 11:56:13 -07008236 }
8237
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008238 @Override
8239 public void onWindowFocusChanged(boolean hasWindowFocus) {
8240 super.onWindowFocusChanged(hasWindowFocus);
8241
8242 if (hasWindowFocus) {
8243 if (mBlink != null) {
8244 mBlink.uncancel();
Gilles Debunne3d010062011-02-18 14:16:41 -08008245 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008246 }
8247 } else {
8248 if (mBlink != null) {
8249 mBlink.cancel();
8250 }
8251 // Don't leave us in the middle of a batch edit.
8252 onEndBatchEdit();
8253 if (mInputContentType != null) {
8254 mInputContentType.enterDown = false;
8255 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008256
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008257 hideControllers();
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07008258 if (mSuggestionsPopupWindow != null) {
8259 mSuggestionsPopupWindow.onParentLostFocus();
8260 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008261 }
8262
8263 startStopMarquee(hasWindowFocus);
8264 }
8265
Adam Powellba0a2c32010-09-28 17:41:23 -07008266 @Override
8267 protected void onVisibilityChanged(View changedView, int visibility) {
8268 super.onVisibilityChanged(changedView, visibility);
8269 if (visibility != VISIBLE) {
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008270 hideControllers();
Adam Powellba0a2c32010-09-28 17:41:23 -07008271 }
8272 }
8273
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008274 /**
8275 * Use {@link BaseInputConnection#removeComposingSpans
8276 * BaseInputConnection.removeComposingSpans()} to remove any IME composing
8277 * state from this text view.
8278 */
8279 public void clearComposingText() {
8280 if (mText instanceof Spannable) {
8281 BaseInputConnection.removeComposingSpans((Spannable)mText);
8282 }
8283 }
8284
8285 @Override
8286 public void setSelected(boolean selected) {
8287 boolean wasSelected = isSelected();
8288
8289 super.setSelected(selected);
8290
8291 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8292 if (selected) {
8293 startMarquee();
8294 } else {
8295 stopMarquee();
8296 }
8297 }
8298 }
8299
8300 @Override
8301 public boolean onTouchEvent(MotionEvent event) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008302 final int action = event.getActionMasked();
Adam Powell965b9692010-10-21 18:44:32 -07008303
Adam Powell965b9692010-10-21 18:44:32 -07008304 if (hasSelectionController()) {
8305 getSelectionController().onTouchEvent(event);
Gilles Debunne5347c582010-10-27 14:22:35 -07008306 }
Adam Powellb08013c2010-09-16 16:28:11 -07008307
Gilles Debunne5347c582010-10-27 14:22:35 -07008308 if (action == MotionEvent.ACTION_DOWN) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07008309 mLastDownPositionX = event.getX();
8310 mLastDownPositionY = event.getY();
Gilles Debunne9948ad72010-11-24 14:00:46 -08008311
The Android Open Source Project4df24232009-03-05 14:34:35 -08008312 // Reset this state; it will be re-set if super.onTouchEvent
8313 // causes focus to move to the view.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008314 mTouchFocusSelected = false;
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008315 mIgnoreActionUpEvent = false;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008316 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008317
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008318 final boolean superResult = super.onTouchEvent(event);
8319
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008320 /*
8321 * Don't handle the release after a long press, because it will
8322 * move the selection away from whatever the menu action was
8323 * trying to affect.
8324 */
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008325 if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
8326 mDiscardNextActionUp = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008327 return superResult;
8328 }
8329
Gilles Debunne70a63122011-09-01 13:27:33 -07008330 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
8331 !shouldIgnoreActionUpEvent() && isFocused();
Gilles Debunnec3e85a72011-01-21 08:46:06 -08008332
Gilles Debunne70a63122011-09-01 13:27:33 -07008333 if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
Janos Levai042856c2010-10-15 02:53:58 +03008334 && mText instanceof Spannable && mLayout != null) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008335 boolean handled = false;
8336
The Android Open Source Projectba87e3e2009-03-13 13:04:22 -07008337 if (mMovement != null) {
8338 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
8339 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008340
Gilles Debunne70a63122011-09-01 13:27:33 -07008341 if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable) {
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008342 // The LinkMovementMethod which should handle taps on links has not been installed
Gilles Debunne70a63122011-09-01 13:27:33 -07008343 // on non editable text that support text selection.
8344 // We reproduce its behavior here to open links for these.
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008345 ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
8346 getSelectionEnd(), ClickableSpan.class);
8347
8348 if (links.length != 0) {
8349 links[0].onClick(this);
8350 handled = true;
8351 }
8352 }
8353
Gilles Debunne70a63122011-09-01 13:27:33 -07008354 if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008355 // Show the IME, except when selecting in read-only text.
satok863fcd62011-06-21 17:38:02 +09008356 final InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09008357 viewClicked(imm);
Gilles Debunne0f4109e2011-10-19 11:26:21 -07008358 if (!mTextIsSelectable && mSoftInputShownOnFocus) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008359 handled |= imm != null && imm.showSoftInput(this, 0);
Adam Powell879fb6b2010-09-20 11:23:56 -07008360 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008361
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008362 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
Gilles Debunne70a63122011-09-01 13:27:33 -07008363 hideControllers();
8364 if (!selectAllGotFocus && mText.length() > 0) {
Gilles Debunneb062e812011-09-27 14:58:37 -07008365 if (mSpellChecker != null) {
8366 // When the cursor moves, the word that was typed may need spell check
8367 mSpellChecker.onSelectionChanged();
8368 }
Gilles Debunne61ddbba2011-11-09 09:48:40 +01008369 if (!extractedTextModeWillBeStarted()) {
8370 if (isCursorInsideEasyCorrectionSpan()) {
8371 showSuggestions();
8372 } else if (hasInsertionController()) {
8373 getInsertionController().show();
8374 }
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08008375 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008376 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008377
8378 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008379 }
8380
The Android Open Source Project4df24232009-03-05 14:34:35 -08008381 if (handled) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008382 return true;
8383 }
8384 }
8385
8386 return superResult;
8387 }
8388
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008389 /**
Gilles Debunne6435a562011-08-04 21:22:30 -07008390 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
8391 */
8392 private boolean isCursorInsideSuggestionSpan() {
8393 if (!(mText instanceof Spannable)) return false;
8394
8395 SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
8396 getSelectionEnd(), SuggestionSpan.class);
8397 return (suggestionSpans.length > 0);
8398 }
8399
8400 /**
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008401 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
8402 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
8403 */
8404 private boolean isCursorInsideEasyCorrectionSpan() {
Gilles Debunne6435a562011-08-04 21:22:30 -07008405 Spannable spannable = (Spannable) mText;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008406 SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
8407 getSelectionEnd(), SuggestionSpan.class);
8408 for (int i = 0; i < suggestionSpans.length; i++) {
8409 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
8410 return true;
8411 }
8412 }
8413 return false;
8414 }
8415
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008416 /**
8417 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
8418 * span.
8419 */
8420 private void downgradeEasyCorrectionSpans() {
8421 if (mText instanceof Spannable) {
8422 Spannable spannable = (Spannable) mText;
8423 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8424 spannable.length(), SuggestionSpan.class);
8425 for (int i = 0; i < suggestionSpans.length; i++) {
8426 int flags = suggestionSpans[i].getFlags();
8427 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8428 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
Gilles Debunne186aaf92011-09-16 14:26:12 -07008429 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008430 suggestionSpans[i].setFlags(flags);
8431 }
8432 }
8433 }
8434 }
8435
Jeff Brown8f345672011-02-26 13:29:53 -08008436 @Override
8437 public boolean onGenericMotionEvent(MotionEvent event) {
8438 if (mMovement != null && mText instanceof Spannable && mLayout != null) {
8439 try {
8440 if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
8441 return true;
8442 }
8443 } catch (AbstractMethodError ex) {
8444 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
8445 // Ignore its absence in case third party applications implemented the
8446 // interface directly.
8447 }
8448 }
8449 return super.onGenericMotionEvent(event);
8450 }
8451
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008452 private void prepareCursorControllers() {
Adam Powell8c8293b2010-10-12 14:45:12 -07008453 boolean windowSupportsHandles = false;
8454
8455 ViewGroup.LayoutParams params = getRootView().getLayoutParams();
8456 if (params instanceof WindowManager.LayoutParams) {
8457 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
8458 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
8459 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
8460 }
8461
Gilles Debunne98dbfd42011-01-24 12:54:10 -08008462 mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
Adam Powell965b9692010-10-21 18:44:32 -07008463 mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
8464 mLayout != null;
8465
8466 if (!mInsertionControllerEnabled) {
Gilles Debunnef48e83b2010-12-06 18:36:08 -08008467 hideInsertionPointCursorController();
Adam Powell65a1de92011-01-30 15:47:29 -08008468 if (mInsertionPointCursorController != null) {
8469 mInsertionPointCursorController.onDetached();
8470 mInsertionPointCursorController = null;
8471 }
Gilles Debunne05336272010-07-09 20:13:45 -07008472 }
8473
Adam Powell965b9692010-10-21 18:44:32 -07008474 if (!mSelectionControllerEnabled) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008475 stopSelectionActionMode();
Adam Powell65a1de92011-01-30 15:47:29 -08008476 if (mSelectionModifierCursorController != null) {
8477 mSelectionModifierCursorController.onDetached();
8478 mSelectionModifierCursorController = null;
8479 }
Gilles Debunne05336272010-07-09 20:13:45 -07008480 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008481 }
8482
8483 /**
Gilles Debunne86b9c782010-11-11 10:43:48 -08008484 * @return True iff this TextView contains a text that can be edited, or if this is
8485 * a selectable TextView.
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008486 */
8487 private boolean isTextEditable() {
Gilles Debunnef076eeb2010-11-29 11:32:53 -08008488 return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008489 }
8490
The Android Open Source Project4df24232009-03-05 14:34:35 -08008491 /**
8492 * Returns true, only while processing a touch gesture, if the initial
8493 * touch down event caused focus to move to the text view and as a result
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008494 * its selection changed. Only valid while processing the touch gesture
8495 * of interest.
The Android Open Source Project4df24232009-03-05 14:34:35 -08008496 */
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008497 public boolean didTouchFocusSelect() {
8498 return mTouchFocusSelected;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008499 }
8500
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008501 @Override
8502 public void cancelLongPress() {
8503 super.cancelLongPress();
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008504 mIgnoreActionUpEvent = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008505 }
Gilles Debunne70a63122011-09-01 13:27:33 -07008506
8507 /**
8508 * This method is only valid during a touch event.
8509 *
8510 * @return true when the ACTION_UP event should be ignored, false otherwise.
8511 *
8512 * @hide
8513 */
8514 public boolean shouldIgnoreActionUpEvent() {
8515 return mIgnoreActionUpEvent;
8516 }
8517
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008518 @Override
8519 public boolean onTrackballEvent(MotionEvent event) {
8520 if (mMovement != null && mText instanceof Spannable &&
8521 mLayout != null) {
8522 if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
8523 return true;
8524 }
8525 }
8526
8527 return super.onTrackballEvent(event);
8528 }
8529
8530 public void setScroller(Scroller s) {
8531 mScroller = s;
8532 }
8533
8534 private static class Blink extends Handler implements Runnable {
Gilles Debunnee15b3582010-06-16 15:17:21 -07008535 private final WeakReference<TextView> mView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008536 private boolean mCancelled;
8537
8538 public Blink(TextView v) {
8539 mView = new WeakReference<TextView>(v);
8540 }
8541
8542 public void run() {
8543 if (mCancelled) {
8544 return;
8545 }
8546
8547 removeCallbacks(Blink.this);
8548
8549 TextView tv = mView.get();
8550
Gilles Debunne3d010062011-02-18 14:16:41 -08008551 if (tv != null && tv.shouldBlink()) {
8552 if (tv.mLayout != null) {
8553 tv.invalidateCursorPath();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008554 }
Gilles Debunne3d010062011-02-18 14:16:41 -08008555
8556 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008557 }
8558 }
8559
8560 void cancel() {
8561 if (!mCancelled) {
8562 removeCallbacks(Blink.this);
8563 mCancelled = true;
8564 }
8565 }
8566
8567 void uncancel() {
8568 mCancelled = false;
8569 }
8570 }
8571
Gilles Debunne3d010062011-02-18 14:16:41 -08008572 /**
8573 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
8574 */
8575 private boolean shouldBlink() {
8576 if (!isFocused()) return false;
8577
8578 final int start = getSelectionStart();
8579 if (start < 0) return false;
8580
8581 final int end = getSelectionEnd();
8582 if (end < 0) return false;
8583
8584 return start == end;
8585 }
8586
8587 private void makeBlink() {
8588 if (isCursorVisible()) {
8589 if (shouldBlink()) {
8590 mShowCursor = SystemClock.uptimeMillis();
8591 if (mBlink == null) mBlink = new Blink(this);
8592 mBlink.removeCallbacks(mBlink);
8593 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
8594 }
8595 } else {
8596 if (mBlink != null) mBlink.removeCallbacks(mBlink);
8597 }
8598 }
8599
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008600 @Override
8601 protected float getLeftFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008602 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008603 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8604 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008605 if (mMarquee != null && !mMarquee.isStopped()) {
8606 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008607 if (marquee.shouldDrawLeftFade()) {
8608 return marquee.mScroll / getHorizontalFadingEdgeLength();
8609 } else {
8610 return 0.0f;
8611 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008612 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008613 final int layoutDirection = getResolvedLayoutDirection();
8614 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008615 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008616 case Gravity.LEFT:
8617 return 0.0f;
8618 case Gravity.RIGHT:
8619 return (mLayout.getLineRight(0) - (mRight - mLeft) -
8620 getCompoundPaddingLeft() - getCompoundPaddingRight() -
8621 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
8622 case Gravity.CENTER_HORIZONTAL:
8623 return 0.0f;
8624 }
8625 }
8626 }
8627 return super.getLeftFadingEdgeStrength();
8628 }
8629
8630 @Override
8631 protected float getRightFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008632 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008633 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8634 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008635 if (mMarquee != null && !mMarquee.isStopped()) {
8636 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008637 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008638 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008639 final int layoutDirection = getResolvedLayoutDirection();
8640 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008641 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008642 case Gravity.LEFT:
Romain Guy076dc9f2009-06-24 17:17:51 -07008643 final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
8644 getCompoundPaddingRight();
8645 final float lineWidth = mLayout.getLineWidth(0);
8646 return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008647 case Gravity.RIGHT:
8648 return 0.0f;
8649 case Gravity.CENTER_HORIZONTAL:
Gilles Debunne44c14732010-10-19 11:56:59 -07008650 case Gravity.FILL_HORIZONTAL:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008651 return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
8652 getCompoundPaddingLeft() - getCompoundPaddingRight())) /
8653 getHorizontalFadingEdgeLength();
8654 }
8655 }
8656 }
8657 return super.getRightFadingEdgeStrength();
8658 }
8659
8660 @Override
8661 protected int computeHorizontalScrollRange() {
Romain Guydac5f9f2010-07-08 11:40:54 -07008662 if (mLayout != null) {
8663 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
8664 (int) mLayout.getLineWidth(0) : mLayout.getWidth();
8665 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008666
8667 return super.computeHorizontalScrollRange();
8668 }
8669
8670 @Override
8671 protected int computeVerticalScrollRange() {
8672 if (mLayout != null)
8673 return mLayout.getHeight();
8674
8675 return super.computeVerticalScrollRange();
8676 }
8677
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008678 @Override
8679 protected int computeVerticalScrollExtent() {
8680 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
8681 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008682
8683 @Override
Svetoslav Ganovea515ae2011-09-14 18:15:32 -07008684 public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
8685 super.findViewsWithText(outViews, searched, flags);
8686 if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
8687 && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
8688 String searchedLowerCase = searched.toString().toLowerCase();
8689 String textLowerCase = mText.toString().toLowerCase();
8690 if (textLowerCase.contains(searchedLowerCase)) {
8691 outViews.add(this);
8692 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008693 }
8694 }
8695
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008696 public enum BufferType {
8697 NORMAL, SPANNABLE, EDITABLE,
8698 }
8699
8700 /**
8701 * Returns the TextView_textColor attribute from the
8702 * Resources.StyledAttributes, if set, or the TextAppearance_textColor
8703 * from the TextView_textAppearance attribute, if TextView_textColor
8704 * was not set directly.
8705 */
8706 public static ColorStateList getTextColors(Context context, TypedArray attrs) {
8707 ColorStateList colors;
8708 colors = attrs.getColorStateList(com.android.internal.R.styleable.
8709 TextView_textColor);
8710
8711 if (colors == null) {
8712 int ap = attrs.getResourceId(com.android.internal.R.styleable.
8713 TextView_textAppearance, -1);
8714 if (ap != -1) {
8715 TypedArray appearance;
8716 appearance = context.obtainStyledAttributes(ap,
8717 com.android.internal.R.styleable.TextAppearance);
8718 colors = appearance.getColorStateList(com.android.internal.R.styleable.
8719 TextAppearance_textColor);
8720 appearance.recycle();
8721 }
8722 }
8723
8724 return colors;
8725 }
8726
8727 /**
8728 * Returns the default color from the TextView_textColor attribute
8729 * from the AttributeSet, if set, or the default color from the
8730 * TextAppearance_textColor from the TextView_textAppearance attribute,
8731 * if TextView_textColor was not set directly.
8732 */
8733 public static int getTextColor(Context context,
8734 TypedArray attrs,
8735 int def) {
8736 ColorStateList colors = getTextColors(context, attrs);
8737
8738 if (colors == null) {
8739 return def;
8740 } else {
8741 return colors.getDefaultColor();
8742 }
8743 }
8744
8745 @Override
8746 public boolean onKeyShortcut(int keyCode, KeyEvent event) {
Jeff Brownc1df9072010-12-21 16:38:50 -08008747 final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
8748 if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
8749 switch (keyCode) {
8750 case KeyEvent.KEYCODE_A:
8751 if (canSelectText()) {
8752 return onTextContextMenuItem(ID_SELECT_ALL);
8753 }
8754 break;
8755 case KeyEvent.KEYCODE_X:
8756 if (canCut()) {
8757 return onTextContextMenuItem(ID_CUT);
8758 }
8759 break;
8760 case KeyEvent.KEYCODE_C:
8761 if (canCopy()) {
8762 return onTextContextMenuItem(ID_COPY);
8763 }
8764 break;
8765 case KeyEvent.KEYCODE_V:
8766 if (canPaste()) {
8767 return onTextContextMenuItem(ID_PASTE);
8768 }
8769 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008770 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008771 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008772 return super.onKeyShortcut(keyCode, event);
8773 }
8774
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008775 /**
8776 * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
8777 * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
8778 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
8779 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008780 private boolean canSelectText() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008781 return hasSelectionController() && mText.length() != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008782 }
8783
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008784 /**
8785 * Test based on the <i>intrinsic</i> charateristics of the TextView.
8786 * The text must be spannable and the movement method must allow for arbitary selection.
8787 *
8788 * See also {@link #canSelectText()}.
8789 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008790 private boolean textCanBeSelected() {
Gilles Debunne05336272010-07-09 20:13:45 -07008791 // prepareCursorController() relies on this method.
8792 // If you change this condition, make sure prepareCursorController is called anywhere
8793 // the value of this condition might be changed.
Gilles Debunnebb588da2011-07-11 18:26:19 -07008794 if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
8795 return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008796 }
8797
8798 private boolean canCut() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008799 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008800 return false;
8801 }
8802
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008803 if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
8804 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008805 }
8806
8807 return false;
8808 }
8809
8810 private boolean canCopy() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008811 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008812 return false;
8813 }
8814
Gilles Debunne03789e82010-09-07 19:07:17 -07008815 if (mText.length() > 0 && hasSelection()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008816 return true;
8817 }
8818
8819 return false;
8820 }
8821
8822 private boolean canPaste() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008823 return (mText instanceof Editable &&
8824 mInput != null &&
8825 getSelectionStart() >= 0 &&
8826 getSelectionEnd() >= 0 &&
8827 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008828 hasPrimaryClip());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008829 }
8830
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008831 private static long packRangeInLong(int start, int end) {
Gilles Debunne05336272010-07-09 20:13:45 -07008832 return (((long) start) << 32) | end;
8833 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008834
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008835 private static int extractRangeStartFromLong(long range) {
8836 return (int) (range >>> 32);
8837 }
8838
8839 private static int extractRangeEndFromLong(long range) {
8840 return (int) (range & 0x00000000FFFFFFFFL);
8841 }
Gilles Debunnecbfbb522010-10-07 16:57:31 -07008842
Gilles Debunnec59269f2011-04-22 11:46:09 -07008843 private boolean selectAll() {
8844 final int length = mText.length();
8845 Selection.setSelection((Spannable) mText, 0, length);
8846 return length > 0;
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008847 }
8848
Gilles Debunnec59269f2011-04-22 11:46:09 -07008849 /**
8850 * Adjusts selection to the word under last touch offset.
8851 * Return true if the operation was successfully performed.
8852 */
8853 private boolean selectCurrentWord() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008854 if (!canSelectText()) {
Gilles Debunnec59269f2011-04-22 11:46:09 -07008855 return false;
Gilles Debunne6da7e932010-12-07 14:28:14 -08008856 }
8857
Gilles Debunne710a9102010-11-23 16:50:28 -08008858 if (hasPasswordTransformationMethod()) {
Gilles Debunne87380bc2011-01-04 13:24:54 -08008859 // Always select all on a password field.
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008860 // Cut/copy menu entries are not available for passwords, but being able to select all
8861 // is however useful to delete or paste to replace the entire content.
Gilles Debunnec59269f2011-04-22 11:46:09 -07008862 return selectAll();
8863 }
8864
8865 int klass = mInputType & InputType.TYPE_MASK_CLASS;
8866 int variation = mInputType & InputType.TYPE_MASK_VARIATION;
8867
8868 // Specific text field types: select the entire text for these
8869 if (klass == InputType.TYPE_CLASS_NUMBER ||
8870 klass == InputType.TYPE_CLASS_PHONE ||
8871 klass == InputType.TYPE_CLASS_DATETIME ||
8872 variation == InputType.TYPE_TEXT_VARIATION_URI ||
8873 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
8874 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
8875 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
8876 return selectAll();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008877 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008878
Gilles Debunne87380bc2011-01-04 13:24:54 -08008879 long lastTouchOffsets = getLastTouchOffsets();
8880 final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
8881 final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008882
Gilles Debunnebb588da2011-07-11 18:26:19 -07008883 // Safety check in case standard touch event handling has been bypassed
8884 if (minOffset < 0 || minOffset >= mText.length()) return false;
8885 if (maxOffset < 0 || maxOffset >= mText.length()) return false;
8886
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008887 int selectionStart, selectionEnd;
8888
8889 // If a URLSpan (web address, email, phone...) is found at that position, select it.
8890 URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008891 if (urlSpans.length >= 1) {
8892 URLSpan urlSpan = urlSpans[0];
8893 selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
8894 selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008895 } else {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008896 final WordIterator wordIterator = getWordIterator();
8897 wordIterator.setCharSequence(mText, minOffset, maxOffset);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008898
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008899 selectionStart = wordIterator.getBeginning(minOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008900 if (selectionStart == BreakIterator.DONE) return false;
8901
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008902 selectionEnd = wordIterator.getEnd(maxOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008903 if (selectionEnd == BreakIterator.DONE) return false;
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008904
8905 if (selectionStart == selectionEnd) {
8906 // Possible when the word iterator does not properly handle the text's language
8907 long range = getCharRange(selectionStart);
8908 selectionStart = extractRangeStartFromLong(range);
8909 selectionEnd = extractRangeEndFromLong(range);
8910 }
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008911 }
8912
8913 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008914 return selectionEnd > selectionStart;
8915 }
8916
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008917 /**
8918 * This is a temporary method. Future versions may support multi-locale text.
8919 *
satok05f24702011-11-02 19:29:35 +09008920 * @return The locale that should be used for a word iterator and a spell checker
8921 * in this TextView, based on the current spell checker settings,
8922 * the current IME's locale, or the system default locale.
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008923 * @hide
8924 */
satok05f24702011-11-02 19:29:35 +09008925 public Locale getTextServicesLocale() {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008926 Locale locale = Locale.getDefault();
satok05f24702011-11-02 19:29:35 +09008927 final TextServicesManager textServicesManager = (TextServicesManager)
8928 mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
8929 final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true);
8930 if (subtype != null) {
8931 locale = new Locale(subtype.getLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008932 }
8933 return locale;
8934 }
8935
8936 void onLocaleChanged() {
Gilles Debunnee9b82802011-10-27 14:38:27 -07008937 removeMisspelledSpans((Editable) mText);
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008938 // Will be re-created on demand in getWordIterator with the proper new locale
8939 mWordIterator = null;
8940 }
8941
8942 /**
8943 * @hide
8944 */
8945 public WordIterator getWordIterator() {
8946 if (mWordIterator == null) {
satok05f24702011-11-02 19:29:35 +09008947 mWordIterator = new WordIterator(getTextServicesLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008948 }
8949 return mWordIterator;
8950 }
8951
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008952 private long getCharRange(int offset) {
8953 final int textLength = mText.length();
8954 if (offset + 1 < textLength) {
8955 final char currentChar = mText.charAt(offset);
8956 final char nextChar = mText.charAt(offset + 1);
8957 if (Character.isSurrogatePair(currentChar, nextChar)) {
8958 return packRangeInLong(offset, offset + 2);
8959 }
8960 }
8961 if (offset < textLength) {
8962 return packRangeInLong(offset, offset + 1);
8963 }
8964 if (offset - 2 >= 0) {
8965 final char previousChar = mText.charAt(offset - 1);
8966 final char previousPreviousChar = mText.charAt(offset - 2);
8967 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
8968 return packRangeInLong(offset - 2, offset);
8969 }
8970 }
8971 if (offset - 1 >= 0) {
8972 return packRangeInLong(offset - 1, offset);
8973 }
8974 return packRangeInLong(offset, offset);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008975 }
8976
8977 private long getLastTouchOffsets() {
Gilles Debunne771d64b2011-11-10 10:12:13 +01008978 SelectionModifierCursorController selectionController = getSelectionController();
8979 final int minOffset = selectionController.getMinTouchOffset();
8980 final int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008981 return packRangeInLong(minOffset, maxOffset);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008982 }
Gilles Debunnedf4ee432010-08-25 19:13:48 -07008983
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008984 @Override
Svetoslav Ganov736c2752011-04-22 18:30:36 -07008985 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganov887e1a12011-04-29 15:09:28 -07008986 super.onPopulateAccessibilityEvent(event);
8987
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008988 final boolean isPassword = hasPasswordTransformationMethod();
svetoslavganov75986cf2009-05-14 22:28:01 -07008989 if (!isPassword) {
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07008990 CharSequence text = getTextForAccessibility();
Svetoslav Ganovd37848a2011-09-20 14:03:55 -07008991 if (!TextUtils.isEmpty(text)) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008992 event.getText().add(text);
8993 }
svetoslavganov75986cf2009-05-14 22:28:01 -07008994 }
svetoslavganov75986cf2009-05-14 22:28:01 -07008995 }
8996
Svetoslav Ganov30401322011-05-12 18:53:45 -07008997 @Override
8998 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
8999 super.onInitializeAccessibilityEvent(event);
9000
9001 final boolean isPassword = hasPasswordTransformationMethod();
9002 event.setPassword(isPassword);
Svetoslav Ganova0156172011-06-26 17:55:44 -07009003
9004 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
9005 event.setFromIndex(Selection.getSelectionStart(mText));
9006 event.setToIndex(Selection.getSelectionEnd(mText));
9007 event.setItemCount(mText.length());
9008 }
Svetoslav Ganov30401322011-05-12 18:53:45 -07009009 }
9010
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07009011 @Override
9012 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
9013 super.onInitializeAccessibilityNodeInfo(info);
9014
9015 final boolean isPassword = hasPasswordTransformationMethod();
9016 if (!isPassword) {
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07009017 info.setText(getTextForAccessibility());
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07009018 }
9019 info.setPassword(isPassword);
9020 }
9021
Svetoslav Ganov4e03f592011-07-29 22:17:14 -07009022 @Override
9023 public void sendAccessibilityEvent(int eventType) {
9024 // Do not send scroll events since first they are not interesting for
9025 // accessibility and second such events a generated too frequently.
9026 // For details see the implementation of bringTextIntoView().
9027 if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
9028 return;
9029 }
9030 super.sendAccessibilityEvent(eventType);
9031 }
9032
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07009033 /**
9034 * Gets the text reported for accessibility purposes. It is the
9035 * text if not empty or the hint.
9036 *
9037 * @return The accessibility text.
9038 */
9039 private CharSequence getTextForAccessibility() {
9040 CharSequence text = getText();
9041 if (TextUtils.isEmpty(text)) {
9042 text = getHint();
9043 }
9044 return text;
9045 }
9046
svetoslavganov75986cf2009-05-14 22:28:01 -07009047 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
9048 int fromIndex, int removedCount, int addedCount) {
9049 AccessibilityEvent event =
9050 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
9051 event.setFromIndex(fromIndex);
9052 event.setRemovedCount(removedCount);
9053 event.setAddedCount(addedCount);
9054 event.setBeforeText(beforeText);
9055 sendAccessibilityEventUnchecked(event);
9056 }
9057
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009058 /**
9059 * Returns whether this text view is a current input method target. The
9060 * default implementation just checks with {@link InputMethodManager}.
9061 */
9062 public boolean isInputMethodTarget() {
9063 InputMethodManager imm = InputMethodManager.peekInstance();
9064 return imm != null && imm.isActive(this);
9065 }
9066
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009067 // Selection context mode
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009068 private static final int ID_SELECT_ALL = android.R.id.selectAll;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009069 private static final int ID_CUT = android.R.id.cut;
9070 private static final int ID_COPY = android.R.id.copy;
9071 private static final int ID_PASTE = android.R.id.paste;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009072
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009073 /**
9074 * Called when a context menu option for the text view is selected. Currently
Gilles Debunne07194e52011-11-02 14:18:44 -07009075 * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
9076 * {@link android.R.id#copy} or {@link android.R.id#paste}.
Gilles Debunnec59269f2011-04-22 11:46:09 -07009077 *
9078 * @return true if the context menu item action was performed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009079 */
9080 public boolean onTextContextMenuItem(int id) {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009081 int min = 0;
9082 int max = mText.length();
Gilles Debunne64e54a62010-09-07 19:07:17 -07009083
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009084 if (isFocused()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07009085 final int selStart = getSelectionStart();
9086 final int selEnd = getSelectionEnd();
9087
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009088 min = Math.max(0, Math.min(selStart, selEnd));
9089 max = Math.max(0, Math.max(selStart, selEnd));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009090 }
9091
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009092 switch (id) {
Jeff Brownc1df9072010-12-21 16:38:50 -08009093 case ID_SELECT_ALL:
Gilles Debunne299733e2011-02-07 17:11:41 -08009094 // This does not enter text selection mode. Text is highlighted, so that it can be
Gilles Debunnec59269f2011-04-22 11:46:09 -07009095 // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
Jeff Brownc1df9072010-12-21 16:38:50 -08009096 selectAll();
Jeff Brownc1df9072010-12-21 16:38:50 -08009097 return true;
9098
9099 case ID_PASTE:
9100 paste(min, max);
9101 return true;
9102
9103 case ID_CUT:
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009104 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009105 deleteText_internal(min, max);
Jeff Brownc1df9072010-12-21 16:38:50 -08009106 stopSelectionActionMode();
9107 return true;
9108
9109 case ID_COPY:
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009110 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08009111 stopSelectionActionMode();
9112 return true;
9113 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009114 return false;
9115 }
9116
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009117 private CharSequence getTransformedText(int start, int end) {
9118 return removeSuggestionSpans(mTransformed.subSequence(start, end));
9119 }
9120
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009121 /**
9122 * Prepare text so that there are not zero or two spaces at beginning and end of region defined
9123 * by [min, max] when replacing this region by paste.
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009124 * Note that if there were two spaces (or more) at that position before, they are kept. We just
Gilles Debunne4ae0f292010-11-29 14:56:39 -08009125 * make sure we do not add an extra one from the paste content.
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009126 */
9127 private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009128 if (paste.length() > 0) {
9129 if (min > 0) {
9130 final char charBefore = mTransformed.charAt(min - 1);
9131 final char charAfter = paste.charAt(0);
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009132
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009133 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9134 // Two spaces at beginning of paste: remove one
9135 final int originalLength = mText.length();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009136 deleteText_internal(min - 1, min);
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009137 // Due to filters, there is no guarantee that exactly one character was
9138 // removed: count instead.
9139 final int delta = mText.length() - originalLength;
9140 min += delta;
9141 max += delta;
9142 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9143 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9144 // No space at beginning of paste: add one
9145 final int originalLength = mText.length();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009146 replaceText_internal(min, min, " ");
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009147 // Taking possible filters into account as above.
9148 final int delta = mText.length() - originalLength;
9149 min += delta;
9150 max += delta;
9151 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009152 }
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009153
9154 if (max < mText.length()) {
9155 final char charBefore = paste.charAt(paste.length() - 1);
9156 final char charAfter = mTransformed.charAt(max);
9157
9158 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9159 // Two spaces at end of paste: remove one
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009160 deleteText_internal(max, max + 1);
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009161 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9162 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9163 // No space at end of paste: add one
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009164 replaceText_internal(max, max, " ");
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009165 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009166 }
9167 }
Gilles Debunne4ae0f292010-11-29 14:56:39 -08009168
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009169 return packRangeInLong(min, max);
9170 }
9171
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009172 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
9173 TextView shadowView = (TextView) inflate(mContext,
Gilles Debunnef170a342010-11-11 11:08:59 -08009174 com.android.internal.R.layout.text_drag_thumbnail, null);
9175
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009176 if (shadowView == null) {
Gilles Debunnef170a342010-11-11 11:08:59 -08009177 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
9178 }
9179
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009180 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
9181 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
Gilles Debunnef170a342010-11-11 11:08:59 -08009182 }
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009183 shadowView.setText(text);
9184 shadowView.setTextColor(getTextColors());
Gilles Debunnef170a342010-11-11 11:08:59 -08009185
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009186 shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
9187 shadowView.setGravity(Gravity.CENTER);
Gilles Debunnef170a342010-11-11 11:08:59 -08009188
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009189 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
Gilles Debunnef170a342010-11-11 11:08:59 -08009190 ViewGroup.LayoutParams.WRAP_CONTENT));
9191
9192 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009193 shadowView.measure(size, size);
Gilles Debunnef170a342010-11-11 11:08:59 -08009194
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009195 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
9196 shadowView.invalidate();
9197 return new DragShadowBuilder(shadowView);
Gilles Debunnef170a342010-11-11 11:08:59 -08009198 }
9199
Gilles Debunneaaa84792010-12-03 11:10:14 -08009200 private static class DragLocalState {
9201 public TextView sourceTextView;
9202 public int start, end;
9203
9204 public DragLocalState(TextView sourceTextView, int start, int end) {
9205 this.sourceTextView = sourceTextView;
9206 this.start = start;
9207 this.end = end;
9208 }
9209 }
9210
Gilles Debunnee15b3582010-06-16 15:17:21 -07009211 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009212 public boolean performLongClick() {
Gilles Debunnee28454a2011-09-07 18:03:44 -07009213 boolean handled = false;
9214 boolean vibrate = true;
9215
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009216 if (super.performLongClick()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009217 mDiscardNextActionUp = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009218 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009219 }
Gilles Debunnef170a342010-11-11 11:08:59 -08009220
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009221 // Long press in empty space moves cursor and shows the Paste affordance if available.
Gilles Debunnee28454a2011-09-07 18:03:44 -07009222 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009223 mInsertionControllerEnabled) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07009224 final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
Gilles Debunned1dc72a2010-11-30 10:16:35 -08009225 stopSelectionActionMode();
Gilles Debunne75beb332011-04-29 11:40:22 -07009226 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne6435a562011-08-04 21:22:30 -07009227 getInsertionController().showWithActionPopup();
Gilles Debunne299733e2011-02-07 17:11:41 -08009228 handled = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009229 vibrate = false;
Gilles Debunne9948ad72010-11-24 14:00:46 -08009230 }
9231
Gilles Debunne299733e2011-02-07 17:11:41 -08009232 if (!handled && mSelectionActionMode != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009233 if (touchPositionIsInSelection()) {
9234 // Start a drag
9235 final int start = getSelectionStart();
9236 final int end = getSelectionEnd();
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009237 CharSequence selectedText = getTransformedText(start, end);
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009238 ClipData data = ClipData.newPlainText(null, selectedText);
Gilles Debunneaaa84792010-12-03 11:10:14 -08009239 DragLocalState localState = new DragLocalState(this, start, end);
Christopher Tate02d2b3b2011-01-10 20:43:53 -08009240 startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009241 stopSelectionActionMode();
9242 } else {
Gilles Debunnef682a772011-08-31 15:49:10 -07009243 getSelectionController().hide();
Gilles Debunne2037b822011-04-22 13:07:33 -07009244 selectCurrentWord();
Gilles Debunne57324c72011-08-29 14:42:15 -07009245 getSelectionController().show();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009246 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009247 handled = true;
Gilles Debunnef170a342010-11-11 11:08:59 -08009248 }
9249
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009250 // Start a new selection
Gilles Debunnee28454a2011-09-07 18:03:44 -07009251 if (!handled) {
Gilles Debunne8f3105f2011-10-03 18:28:59 -07009252 vibrate = handled = startSelectionActionMode();
Gilles Debunnee28454a2011-09-07 18:03:44 -07009253 }
9254
9255 if (vibrate) {
9256 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
9257 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009258
9259 if (handled) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009260 mDiscardNextActionUp = true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009261 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009262
Gilles Debunne299733e2011-02-07 17:11:41 -08009263 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009264 }
9265
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009266 private boolean touchPositionIsInSelection() {
9267 int selectionStart = getSelectionStart();
9268 int selectionEnd = getSelectionEnd();
Gilles Debunne05336272010-07-09 20:13:45 -07009269
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009270 if (selectionStart == selectionEnd) {
9271 return false;
9272 }
Gilles Debunne05336272010-07-09 20:13:45 -07009273
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009274 if (selectionStart > selectionEnd) {
9275 int tmp = selectionStart;
9276 selectionStart = selectionEnd;
9277 selectionEnd = tmp;
Gilles Debunne05336272010-07-09 20:13:45 -07009278 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009279 }
Gilles Debunne05336272010-07-09 20:13:45 -07009280
Gilles Debunnee587d832010-11-23 20:20:11 -08009281 SelectionModifierCursorController selectionController = getSelectionController();
9282 int minOffset = selectionController.getMinTouchOffset();
9283 int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne05336272010-07-09 20:13:45 -07009284
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009285 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
9286 }
9287
Gilles Debunne21078e42011-08-02 10:22:35 -07009288 private PositionListener getPositionListener() {
9289 if (mPositionListener == null) {
9290 mPositionListener = new PositionListener();
9291 }
9292 return mPositionListener;
9293 }
9294
9295 private interface TextViewPositionListener {
Gilles Debunnef682a772011-08-31 15:49:10 -07009296 public void updatePosition(int parentPositionX, int parentPositionY,
9297 boolean parentPositionChanged, boolean parentScrolled);
Gilles Debunne21078e42011-08-02 10:22:35 -07009298 }
9299
9300 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
Luca Zanolin1564fc72011-09-07 00:01:28 +01009301 // 3 handles
9302 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
9303 private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
Gilles Debunne21078e42011-08-02 10:22:35 -07009304 private TextViewPositionListener[] mPositionListeners =
9305 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
9306 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
9307 private boolean mPositionHasChanged = true;
9308 // Absolute position of the TextView with respect to its parent window
9309 private int mPositionX, mPositionY;
9310 private int mNumberOfListeners;
Gilles Debunnef682a772011-08-31 15:49:10 -07009311 private boolean mScrollHasChanged;
Gilles Debunne21078e42011-08-02 10:22:35 -07009312
9313 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
9314 if (mNumberOfListeners == 0) {
9315 updatePosition();
9316 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9317 vto.addOnPreDrawListener(this);
9318 }
9319
9320 int emptySlotIndex = -1;
9321 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9322 TextViewPositionListener listener = mPositionListeners[i];
9323 if (listener == positionListener) {
9324 return;
9325 } else if (emptySlotIndex < 0 && listener == null) {
9326 emptySlotIndex = i;
9327 }
9328 }
9329
9330 mPositionListeners[emptySlotIndex] = positionListener;
9331 mCanMove[emptySlotIndex] = canMove;
9332 mNumberOfListeners++;
9333 }
9334
9335 public void removeSubscriber(TextViewPositionListener positionListener) {
9336 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9337 if (mPositionListeners[i] == positionListener) {
9338 mPositionListeners[i] = null;
9339 mNumberOfListeners--;
9340 break;
9341 }
9342 }
9343
9344 if (mNumberOfListeners == 0) {
9345 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9346 vto.removeOnPreDrawListener(this);
9347 }
9348 }
9349
9350 public int getPositionX() {
9351 return mPositionX;
9352 }
9353
9354 public int getPositionY() {
9355 return mPositionY;
9356 }
9357
9358 @Override
9359 public boolean onPreDraw() {
9360 updatePosition();
9361
9362 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009363 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
Gilles Debunne21078e42011-08-02 10:22:35 -07009364 TextViewPositionListener positionListener = mPositionListeners[i];
9365 if (positionListener != null) {
9366 positionListener.updatePosition(mPositionX, mPositionY,
Gilles Debunnef682a772011-08-31 15:49:10 -07009367 mPositionHasChanged, mScrollHasChanged);
Gilles Debunne21078e42011-08-02 10:22:35 -07009368 }
9369 }
9370 }
9371
Gilles Debunnef682a772011-08-31 15:49:10 -07009372 mScrollHasChanged = false;
Gilles Debunne21078e42011-08-02 10:22:35 -07009373 return true;
9374 }
9375
9376 private void updatePosition() {
9377 TextView.this.getLocationInWindow(mTempCoords);
9378
9379 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
9380
9381 mPositionX = mTempCoords[0];
9382 mPositionY = mTempCoords[1];
9383 }
9384
Gilles Debunnef682a772011-08-31 15:49:10 -07009385 public void onScrollChanged() {
9386 mScrollHasChanged = true;
9387 }
9388 }
9389
Gilles Debunne335c4e62011-12-01 15:36:08 -08009390 private boolean isPositionVisible(int positionX, int positionY) {
Gilles Debunne64901d42011-11-25 10:23:38 +01009391 synchronized (sTmpPosition) {
9392 final float[] position = sTmpPosition;
9393 position[0] = positionX;
9394 position[1] = positionY;
9395 View view = this;
9396
9397 while (view != null) {
9398 if (view != this) {
9399 // Local scroll is already taken into account in positionX/Y
9400 position[0] -= view.getScrollX();
9401 position[1] -= view.getScrollY();
9402 }
9403
9404 if (position[0] < 0 || position[1] < 0 ||
9405 position[0] > view.getWidth() || position[1] > view.getHeight()) {
9406 return false;
9407 }
9408
9409 if (!view.getMatrix().isIdentity()) {
9410 view.getMatrix().mapPoints(position);
9411 }
9412
9413 position[0] += view.getLeft();
9414 position[1] += view.getTop();
9415
9416 final ViewParent parent = view.getParent();
9417 if (parent instanceof View) {
9418 view = (View) parent;
9419 } else {
9420 // We've reached the ViewRoot, stop iterating
9421 view = null;
9422 }
9423 }
9424 }
9425
9426 // We've been able to walk up the view hierarchy and the position was never clipped
9427 return true;
9428 }
9429
Gilles Debunne335c4e62011-12-01 15:36:08 -08009430 private boolean isOffsetVisible(int offset) {
Gilles Debunne64901d42011-11-25 10:23:38 +01009431 final int line = mLayout.getLineForOffset(offset);
9432 final int lineBottom = mLayout.getLineBottom(line);
9433 final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
9434 return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
9435 lineBottom + viewportToContentVerticalOffset());
9436 }
9437
Gilles Debunnef682a772011-08-31 15:49:10 -07009438 @Override
9439 protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
9440 super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
9441 if (mPositionListener != null) {
9442 mPositionListener.onScrollChanged();
9443 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009444 }
9445
9446 private abstract class PinnedPopupWindow implements TextViewPositionListener {
9447 protected PopupWindow mPopupWindow;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009448 protected ViewGroup mContentView;
Gilles Debunne21078e42011-08-02 10:22:35 -07009449 int mPositionX, mPositionY;
9450
9451 protected abstract void createPopupWindow();
9452 protected abstract void initContentView();
9453 protected abstract int getTextOffset();
9454 protected abstract int getVerticalLocalPosition(int line);
9455 protected abstract int clipVertically(int positionY);
9456
9457 public PinnedPopupWindow() {
9458 createPopupWindow();
9459
9460 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9461 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
9462 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
9463
Gilles Debunne0eea6682011-08-29 13:30:31 -07009464 initContentView();
9465
9466 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9467 ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunne21078e42011-08-02 10:22:35 -07009468 mContentView.setLayoutParams(wrapContent);
9469
Gilles Debunne21078e42011-08-02 10:22:35 -07009470 mPopupWindow.setContentView(mContentView);
9471 }
9472
9473 public void show() {
Gilles Debunnef682a772011-08-31 15:49:10 -07009474 TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
Gilles Debunne21078e42011-08-02 10:22:35 -07009475
9476 computeLocalPosition();
9477
9478 final PositionListener positionListener = TextView.this.getPositionListener();
9479 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
9480 }
Gilles Debunne0eea6682011-08-29 13:30:31 -07009481
9482 protected void measureContent() {
9483 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9484 mContentView.measure(
9485 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
9486 View.MeasureSpec.AT_MOST),
9487 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
9488 View.MeasureSpec.AT_MOST));
9489 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009490
Gilles Debunne0eea6682011-08-29 13:30:31 -07009491 /* The popup window will be horizontally centered on the getTextOffset() and vertically
9492 * positioned according to viewportToContentHorizontalOffset.
9493 *
9494 * This method assumes that mContentView has properly been measured from its content. */
Gilles Debunne21078e42011-08-02 10:22:35 -07009495 private void computeLocalPosition() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009496 measureContent();
Gilles Debunne21078e42011-08-02 10:22:35 -07009497 final int width = mContentView.getMeasuredWidth();
Gilles Debunne0eea6682011-08-29 13:30:31 -07009498 final int offset = getTextOffset();
Gilles Debunne21078e42011-08-02 10:22:35 -07009499 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
9500 mPositionX += viewportToContentHorizontalOffset();
9501
9502 final int line = mLayout.getLineForOffset(offset);
9503 mPositionY = getVerticalLocalPosition(line);
9504 mPositionY += viewportToContentVerticalOffset();
9505 }
9506
9507 private void updatePosition(int parentPositionX, int parentPositionY) {
9508 int positionX = parentPositionX + mPositionX;
9509 int positionY = parentPositionY + mPositionY;
9510
9511 positionY = clipVertically(positionY);
9512
9513 // Horizontal clipping
9514 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9515 final int width = mContentView.getMeasuredWidth();
9516 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
9517 positionX = Math.max(0, positionX);
9518
9519 if (isShowing()) {
9520 mPopupWindow.update(positionX, positionY, -1, -1);
9521 } else {
9522 mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
9523 positionX, positionY);
9524 }
9525 }
9526
9527 public void hide() {
9528 mPopupWindow.dismiss();
9529 TextView.this.getPositionListener().removeSubscriber(this);
9530 }
9531
9532 @Override
Gilles Debunnef682a772011-08-31 15:49:10 -07009533 public void updatePosition(int parentPositionX, int parentPositionY,
9534 boolean parentPositionChanged, boolean parentScrolled) {
9535 // Either parentPositionChanged or parentScrolled is true, check if still visible
Gilles Debunne64901d42011-11-25 10:23:38 +01009536 if (isShowing() && isOffsetVisible(getTextOffset())) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009537 if (parentScrolled) computeLocalPosition();
Gilles Debunne21078e42011-08-02 10:22:35 -07009538 updatePosition(parentPositionX, parentPositionY);
9539 } else {
9540 hide();
9541 }
9542 }
9543
9544 public boolean isShowing() {
9545 return mPopupWindow.isShowing();
9546 }
9547 }
9548
Gilles Debunne0eea6682011-08-29 13:30:31 -07009549 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunne6435a562011-08-04 21:22:30 -07009550 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009551 private static final int ADD_TO_DICTIONARY = -1;
9552 private static final int DELETE_TEXT = -2;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009553 private SuggestionInfo[] mSuggestionInfos;
9554 private int mNumberOfSuggestions;
Gilles Debunne28294cc2011-08-24 12:02:05 -07009555 private boolean mCursorWasVisibleBeforeSuggestions;
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009556 private boolean mIsShowingUp = false;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009557 private SuggestionAdapter mSuggestionsAdapter;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009558 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
9559 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
9560
Gilles Debunne28294cc2011-08-24 12:02:05 -07009561 private class CustomPopupWindow extends PopupWindow {
9562 public CustomPopupWindow(Context context, int defStyle) {
9563 super(context, null, defStyle);
9564 }
9565
9566 @Override
9567 public void dismiss() {
9568 super.dismiss();
9569
Fabrice Di Meglio03e4d642011-09-06 19:06:06 -07009570 TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
9571
Gilles Debunne186aaf92011-09-16 14:26:12 -07009572 // Safe cast since show() checks that mText is an Editable
9573 ((Spannable) mText).removeSpan(mSuggestionRangeSpan);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009574
9575 setCursorVisible(mCursorWasVisibleBeforeSuggestions);
9576 if (hasInsertionController()) {
9577 getInsertionController().show();
9578 }
9579 }
9580 }
9581
9582 public SuggestionsPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009583 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009584 mSuggestionSpanComparator = new SuggestionSpanComparator();
9585 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
Gilles Debunne28294cc2011-08-24 12:02:05 -07009586 }
Gilles Debunne69340442011-03-31 13:37:51 -07009587
Gilles Debunne21078e42011-08-02 10:22:35 -07009588 @Override
9589 protected void createPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009590 mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
Gilles Debunne21078e42011-08-02 10:22:35 -07009591 com.android.internal.R.attr.textSuggestionsWindowStyle);
Gilles Debunne28885242011-07-25 18:56:09 -07009592 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009593 mPopupWindow.setFocusable(true);
Gilles Debunne21078e42011-08-02 10:22:35 -07009594 mPopupWindow.setClippingEnabled(false);
9595 }
Gilles Debunne69340442011-03-31 13:37:51 -07009596
Gilles Debunne21078e42011-08-02 10:22:35 -07009597 @Override
9598 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009599 ListView listView = new ListView(TextView.this.getContext());
9600 mSuggestionsAdapter = new SuggestionAdapter();
9601 listView.setAdapter(mSuggestionsAdapter);
9602 listView.setOnItemClickListener(this);
9603 mContentView = listView;
Gilles Debunne28885242011-07-25 18:56:09 -07009604
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009605 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
9606 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
9607 for (int i = 0; i < mSuggestionInfos.length; i++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009608 mSuggestionInfos[i] = new SuggestionInfo();
Gilles Debunne28885242011-07-25 18:56:09 -07009609 }
Gilles Debunne69340442011-03-31 13:37:51 -07009610 }
9611
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009612 public boolean isShowingUp() {
9613 return mIsShowingUp;
9614 }
9615
9616 public void onParentLostFocus() {
9617 mIsShowingUp = false;
9618 }
9619
Gilles Debunne214a8622011-04-26 15:44:37 -07009620 private class SuggestionInfo {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009621 int suggestionStart, suggestionEnd; // range of actual suggestion within text
Gilles Debunneee511cc2011-05-05 14:57:50 -07009622 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009623 int suggestionIndex; // the index of this suggestion inside suggestionSpan
Gilles Debunne0eea6682011-08-29 13:30:31 -07009624 SpannableStringBuilder text = new SpannableStringBuilder();
Luca Zanolin2346e012011-09-06 22:56:47 +01009625 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
9626 android.R.style.TextAppearance_SuggestionHighlight);
Gilles Debunne0eea6682011-08-29 13:30:31 -07009627 }
9628
9629 private class SuggestionAdapter extends BaseAdapter {
9630 private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
9631 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
9632
9633 @Override
9634 public int getCount() {
9635 return mNumberOfSuggestions;
9636 }
9637
9638 @Override
9639 public Object getItem(int position) {
9640 return mSuggestionInfos[position];
9641 }
9642
9643 @Override
9644 public long getItemId(int position) {
9645 return position;
9646 }
9647
9648 @Override
9649 public View getView(int position, View convertView, ViewGroup parent) {
9650 TextView textView = (TextView) convertView;
9651
9652 if (textView == null) {
9653 textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
9654 false);
9655 }
9656
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009657 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
9658 textView.setText(suggestionInfo.text);
9659
9660 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9661 textView.setCompoundDrawablesWithIntrinsicBounds(
9662 com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
9663 } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9664 textView.setCompoundDrawablesWithIntrinsicBounds(
9665 com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
9666 } else {
9667 textView.setCompoundDrawables(null, null, null, null);
9668 }
9669
Gilles Debunne0eea6682011-08-29 13:30:31 -07009670 return textView;
9671 }
Gilles Debunne214a8622011-04-26 15:44:37 -07009672 }
9673
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009674 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
9675 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
9676 final int flag1 = span1.getFlags();
9677 final int flag2 = span2.getFlags();
9678 if (flag1 != flag2) {
9679 // The order here should match what is used in updateDrawState
9680 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9681 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9682 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9683 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9684 if (easy1 && !misspelled1) return -1;
9685 if (easy2 && !misspelled2) return 1;
9686 if (misspelled1) return -1;
9687 if (misspelled2) return 1;
9688 }
9689
9690 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
9691 }
9692 }
9693
Luca Zanoline3f89c02011-08-01 09:55:17 +01009694 /**
9695 * Returns the suggestion spans that cover the current cursor position. The suggestion
9696 * spans are sorted according to the length of text that they are attached to.
9697 */
9698 private SuggestionSpan[] getSuggestionSpans() {
9699 int pos = TextView.this.getSelectionStart();
9700 Spannable spannable = (Spannable) TextView.this.mText;
9701 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
9702
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009703 mSpansLengths.clear();
Luca Zanoline3f89c02011-08-01 09:55:17 +01009704 for (SuggestionSpan suggestionSpan : suggestionSpans) {
9705 int start = spannable.getSpanStart(suggestionSpan);
9706 int end = spannable.getSpanEnd(suggestionSpan);
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009707 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
Luca Zanoline3f89c02011-08-01 09:55:17 +01009708 }
9709
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009710 // The suggestions are sorted according to their types (easy correction first, then
9711 // misspelled) and to the length of the text that they cover (shorter first).
9712 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
Luca Zanoline3f89c02011-08-01 09:55:17 +01009713 return suggestionSpans;
9714 }
9715
Gilles Debunne21078e42011-08-02 10:22:35 -07009716 @Override
Gilles Debunne69340442011-03-31 13:37:51 -07009717 public void show() {
9718 if (!(mText instanceof Editable)) return;
9719
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009720 updateSuggestions();
9721 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9722 setCursorVisible(false);
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009723 mIsShowingUp = true;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009724 super.show();
Gilles Debunne21078e42011-08-02 10:22:35 -07009725 }
9726
9727 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009728 protected void measureContent() {
9729 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9730 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
9731 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
9732 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
9733 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
9734
9735 int width = 0;
9736 View view = null;
9737 for (int i = 0; i < mNumberOfSuggestions; i++) {
9738 view = mSuggestionsAdapter.getView(i, view, mContentView);
Luca Zanolin58707d62011-09-28 09:27:49 +01009739 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009740 view.measure(horizontalMeasure, verticalMeasure);
9741 width = Math.max(width, view.getMeasuredWidth());
9742 }
9743
9744 // Enforce the width based on actual text widths
9745 mContentView.measure(
9746 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
9747 verticalMeasure);
9748
9749 Drawable popupBackground = mPopupWindow.getBackground();
9750 if (popupBackground != null) {
9751 if (mTempRect == null) mTempRect = new Rect();
9752 popupBackground.getPadding(mTempRect);
9753 width += mTempRect.left + mTempRect.right;
9754 }
9755 mPopupWindow.setWidth(width);
9756 }
9757
9758 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -07009759 protected int getTextOffset() {
9760 return getSelectionStart();
9761 }
9762
9763 @Override
9764 protected int getVerticalLocalPosition(int line) {
9765 return mLayout.getLineBottom(line);
9766 }
9767
9768 @Override
9769 protected int clipVertically(int positionY) {
9770 final int height = mContentView.getMeasuredHeight();
9771 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9772 return Math.min(positionY, displayMetrics.heightPixels - height);
9773 }
9774
9775 @Override
9776 public void hide() {
9777 super.hide();
Gilles Debunne21078e42011-08-02 10:22:35 -07009778 }
9779
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009780 private void updateSuggestions() {
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009781 Spannable spannable = (Spannable) TextView.this.mText;
Luca Zanoline3f89c02011-08-01 09:55:17 +01009782 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
9783
satokb3fc1a52011-04-06 18:28:55 +09009784 final int nbSpans = suggestionSpans.length;
Gilles Debunne69340442011-03-31 13:37:51 -07009785
Gilles Debunne0eea6682011-08-29 13:30:31 -07009786 mNumberOfSuggestions = 0;
Gilles Debunne214a8622011-04-26 15:44:37 -07009787 int spanUnionStart = mText.length();
9788 int spanUnionEnd = 0;
9789
Gilles Debunnee90bed12011-08-30 14:28:27 -07009790 SuggestionSpan misspelledSpan = null;
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009791 int underlineColor = 0;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009792
Gilles Debunne69340442011-03-31 13:37:51 -07009793 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
satokb3fc1a52011-04-06 18:28:55 +09009794 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
9795 final int spanStart = spannable.getSpanStart(suggestionSpan);
9796 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009797 spanUnionStart = Math.min(spanStart, spanUnionStart);
9798 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
Gilles Debunne69340442011-03-31 13:37:51 -07009799
Gilles Debunnee90bed12011-08-30 14:28:27 -07009800 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
9801 misspelledSpan = suggestionSpan;
9802 }
9803
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009804 // The first span dictates the background color of the highlighted text
9805 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
9806
satokb3fc1a52011-04-06 18:28:55 +09009807 String[] suggestions = suggestionSpan.getSuggestions();
Gilles Debunne69340442011-03-31 13:37:51 -07009808 int nbSuggestions = suggestions.length;
9809 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009810 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunneee511cc2011-05-05 14:57:50 -07009811 suggestionInfo.suggestionSpan = suggestionSpan;
9812 suggestionInfo.suggestionIndex = suggestionIndex;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009813 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9814 suggestions[suggestionIndex]);
Gilles Debunne69340442011-03-31 13:37:51 -07009815
Gilles Debunne0eea6682011-08-29 13:30:31 -07009816 mNumberOfSuggestions++;
9817 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
Gilles Debunne214a8622011-04-26 15:44:37 -07009818 // Also end outer for loop
Gilles Debunne69340442011-03-31 13:37:51 -07009819 spanIndex = nbSpans;
9820 break;
9821 }
9822 }
9823 }
9824
Gilles Debunnee90bed12011-08-30 14:28:27 -07009825 for (int i = 0; i < mNumberOfSuggestions; i++) {
9826 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
9827 }
9828
Gilles Debunnee300be92011-12-06 10:15:56 -08009829 // Add to dictionary item if there is a span with the misspelled flag
Gilles Debunnee90bed12011-08-30 14:28:27 -07009830 if (misspelledSpan != null) {
9831 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
9832 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
9833 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
9834 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunnee90bed12011-08-30 14:28:27 -07009835 suggestionInfo.suggestionSpan = misspelledSpan;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009836 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009837 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9838 getContext().getString(com.android.internal.R.string.addToDictionary));
Gilles Debunnee6701012011-09-29 10:33:19 -07009839 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9840 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunnee90bed12011-08-30 14:28:27 -07009841
9842 mNumberOfSuggestions++;
9843 }
9844 }
9845
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009846 // Delete item
9847 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9848 suggestionInfo.suggestionSpan = null;
9849 suggestionInfo.suggestionIndex = DELETE_TEXT;
9850 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9851 getContext().getString(com.android.internal.R.string.deleteText));
Gilles Debunnee6701012011-09-29 10:33:19 -07009852 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9853 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009854 mNumberOfSuggestions++;
Gilles Debunne214a8622011-04-26 15:44:37 -07009855
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009856 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
9857 if (underlineColor == 0) {
9858 // Fallback on the default highlight color when the first span does not provide one
9859 mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
9860 } else {
Gilles Debunnec2deadc2011-09-30 10:43:43 -07009861 final float BACKGROUND_TRANSPARENCY = 0.4f;
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009862 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
9863 mSuggestionRangeSpan.setBackgroundColor(
9864 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
9865 }
9866 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
Gilles Debunne6435a562011-08-04 21:22:30 -07009867 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9868
Gilles Debunne0eea6682011-08-29 13:30:31 -07009869 mSuggestionsAdapter.notifyDataSetChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -07009870 }
9871
Luca Zanolin2346e012011-09-06 22:56:47 +01009872 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
9873 int unionEnd) {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009874 final Spannable text = (Spannable) mText;
9875 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
9876 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009877
Luca Zanolin2346e012011-09-06 22:56:47 +01009878 // Adjust the start/end of the suggestion span
9879 suggestionInfo.suggestionStart = spanStart - unionStart;
9880 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
9881 + suggestionInfo.text.length();
Gilles Debunnee6701012011-09-29 10:33:19 -07009882
Luca Zanolin2346e012011-09-06 22:56:47 +01009883 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
9884 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne214a8622011-04-26 15:44:37 -07009885
Luca Zanolin2346e012011-09-06 22:56:47 +01009886 // Add the text before and after the span.
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009887 suggestionInfo.text.insert(0, mText.toString().substring(unionStart, spanStart));
9888 suggestionInfo.text.append(mText.toString().substring(spanEnd, unionEnd));
Gilles Debunne214a8622011-04-26 15:44:37 -07009889 }
9890
Gilles Debunne69340442011-03-31 13:37:51 -07009891 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009892 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009893 Editable editable = (Editable) mText;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009894 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Gilles Debunnee90bed12011-08-30 14:28:27 -07009895
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009896 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9897 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
9898 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
Gilles Debunne28ef9042011-10-11 16:01:22 -07009899 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
9900 // Do not leave two adjacent spaces after deletion, or one at beginning of text
9901 if (spanUnionEnd < editable.length() &&
9902 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
9903 (spanUnionStart == 0 ||
9904 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009905 spanUnionEnd = spanUnionEnd + 1;
Gilles Debunne28ef9042011-10-11 16:01:22 -07009906 }
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009907 deleteText_internal(spanUnionStart, spanUnionEnd);
Gilles Debunne69340442011-03-31 13:37:51 -07009908 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009909 hide();
9910 return;
Gilles Debunne69340442011-03-31 13:37:51 -07009911 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009912
9913 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
9914 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunnee300be92011-12-06 10:15:56 -08009915 if (spanStart < 0 || spanEnd <= spanStart) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009916 // Span has been removed
9917 hide();
9918 return;
9919 }
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009920 final String originalText = mText.toString().substring(spanStart, spanEnd);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009921
9922 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9923 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
9924 intent.putExtra("word", originalText);
Jean Chalard5fa67372011-12-07 13:57:59 +09009925 intent.putExtra("locale", getTextServicesLocale().toString());
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009926 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
9927 getContext().startActivity(intent);
Gilles Debunne845d9c72011-09-23 11:09:46 -07009928 // There is no way to know if the word was indeed added. Re-check.
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009929 // TODO The ExtractEditText should remove the span in the original text instead
Gilles Debunne845d9c72011-09-23 11:09:46 -07009930 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunnec115fa02011-12-07 13:38:31 -08009931 updateSpellCheckSpans(spanStart, spanEnd, false);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009932 } else {
9933 // SuggestionSpans are removed by replace: save them before
9934 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
9935 SuggestionSpan.class);
9936 final int length = suggestionSpans.length;
9937 int[] suggestionSpansStarts = new int[length];
9938 int[] suggestionSpansEnds = new int[length];
9939 int[] suggestionSpansFlags = new int[length];
9940 for (int i = 0; i < length; i++) {
9941 final SuggestionSpan suggestionSpan = suggestionSpans[i];
9942 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
9943 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
9944 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
Gilles Debunne5915c882011-10-10 18:17:22 -07009945
9946 // Remove potential misspelled flags
9947 int suggestionSpanFlags = suggestionSpan.getFlags();
9948 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
9949 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
9950 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
9951 suggestionSpan.setFlags(suggestionSpanFlags);
9952 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009953 }
9954
9955 final int suggestionStart = suggestionInfo.suggestionStart;
9956 final int suggestionEnd = suggestionInfo.suggestionEnd;
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009957 final String suggestion = suggestionInfo.text.subSequence(
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009958 suggestionStart, suggestionEnd).toString();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009959 replaceText_internal(spanStart, spanEnd, suggestion);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009960
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009961 // Notify source IME of the suggestion pick. Do this before swaping texts.
9962 if (!TextUtils.isEmpty(
9963 suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
9964 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009965 if (imm != null) {
9966 imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
9967 suggestionInfo.suggestionIndex);
9968 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009969 }
9970
9971 // Swap text content between actual text and Suggestion span
9972 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
9973 suggestions[suggestionInfo.suggestionIndex] = originalText;
9974
9975 // Restore previous SuggestionSpans
9976 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
9977 for (int i = 0; i < length; i++) {
9978 // Only spans that include the modified region make sense after replacement
9979 // Spans partially included in the replaced region are removed, there is no
9980 // way to assign them a valid range after replacement
9981 if (suggestionSpansStarts[i] <= spanStart &&
9982 suggestionSpansEnds[i] >= spanEnd) {
Gilles Debunnee300be92011-12-06 10:15:56 -08009983 setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009984 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
9985 }
9986 }
9987
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009988 // Move cursor at the end of the replaced word
Gilles Debunnee300be92011-12-06 10:15:56 -08009989 final int newCursorPosition = spanEnd + lengthDifference;
9990 setCursorPosition_internal(newCursorPosition, newCursorPosition);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009991 }
9992
9993 hide();
Gilles Debunne69340442011-03-31 13:37:51 -07009994 }
Gilles Debunne69340442011-03-31 13:37:51 -07009995 }
9996
Luca Zanoline0760452011-09-08 12:03:37 +01009997 /**
9998 * Removes the suggestion spans.
9999 */
10000 CharSequence removeSuggestionSpans(CharSequence text) {
10001 if (text instanceof Spanned) {
10002 Spannable spannable;
10003 if (text instanceof Spannable) {
10004 spannable = (Spannable) text;
10005 } else {
10006 spannable = new SpannableString(text);
10007 text = spannable;
10008 }
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010009
Luca Zanoline0760452011-09-08 12:03:37 +010010010 SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
10011 for (int i = 0; i < spans.length; i++) {
10012 spannable.removeSpan(spans[i]);
10013 }
10014 }
10015 return text;
10016 }
10017
10018 void showSuggestions() {
Gilles Debunne69340442011-03-31 13:37:51 -070010019 if (mSuggestionsPopupWindow == null) {
10020 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
10021 }
10022 hideControllers();
10023 mSuggestionsPopupWindow.show();
10024 }
10025
Gilles Debunne4a7199a2011-07-11 14:58:27 -070010026 boolean areSuggestionsShown() {
10027 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
Gilles Debunne6435a562011-08-04 21:22:30 -070010028 }
10029
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010030 /**
Gilles Debunne6435a562011-08-04 21:22:30 -070010031 * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
10032 * by the IME or by the spell checker as the user types. This is done by adding
10033 * {@link SuggestionSpan}s to the text.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010034 *
10035 * When suggestions are enabled (default), this list of suggestions will be displayed when the
Gilles Debunne6435a562011-08-04 21:22:30 -070010036 * user asks for them on these parts of the text. This value depends on the inputType of this
10037 * TextView.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010038 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010039 * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
10040 *
10041 * In addition, the type variation must be one of
Gilles Debunne248b1122011-08-12 13:24:16 -070010042 * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
10043 * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
10044 * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
10045 * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
10046 * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
10047 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010048 * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010049 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010050 * @return true if the suggestions popup window is enabled, based on the inputType.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010051 */
10052 public boolean isSuggestionsEnabled() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010053 if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false;
Gilles Debunne6435a562011-08-04 21:22:30 -070010054 if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
10055
Luca Zanolin15b80162011-08-17 18:47:27 +010010056 final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Gilles Debunne6435a562011-08-04 21:22:30 -070010057 return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL ||
Gilles Debunne248b1122011-08-12 13:24:16 -070010058 variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT ||
10059 variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE ||
10060 variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE ||
Gilles Debunne6435a562011-08-04 21:22:30 -070010061 variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010062 }
10063
10064 /**
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010065 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
10066 * selection is initiated in this View.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010067 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010068 * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
10069 * Paste actions, depending on what this View supports.
10070 *
10071 * A custom implementation can add new entries in the default menu in its
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010072 * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The
10073 * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and
10074 * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
10075 * or {@link android.R.id#paste} ids as parameters.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010076 *
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010077 * Returning false from
10078 * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent
10079 * the action mode from being started.
Gilles Debunneddd6f392011-01-27 09:48:01 -080010080 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010081 * Action click events should be handled by the custom implementation of
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010082 * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010083 *
10084 * Note that text selection mode is not started when a TextView receives focus and the
10085 * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
10086 * that case, to allow for quick replacement.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010087 */
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010088 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
10089 mCustomSelectionActionModeCallback = actionModeCallback;
10090 }
10091
10092 /**
10093 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
10094 *
10095 * @return The current custom selection callback.
10096 */
10097 public ActionMode.Callback getCustomSelectionActionModeCallback() {
10098 return mCustomSelectionActionModeCallback;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010099 }
10100
10101 /**
10102 *
10103 * @return true if the selection mode was actually started.
10104 */
10105 private boolean startSelectionActionMode() {
10106 if (mSelectionActionMode != null) {
10107 // Selection action mode is already started
10108 return false;
10109 }
10110
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010111 if (!canSelectText() || !requestFocus()) {
10112 Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
10113 return false;
10114 }
10115
Gilles Debunneed674182011-06-02 19:52:22 -070010116 if (!hasSelection()) {
10117 // There may already be a selection on device rotation
Gilles Debunnee1fc4f62011-10-03 17:01:19 -070010118 if (!selectCurrentWord()) {
Gilles Debunneed674182011-06-02 19:52:22 -070010119 // No word found under cursor or text selection not permitted.
10120 return false;
10121 }
Gilles Debunnec01f3fe2010-12-22 17:07:36 -080010122 }
10123
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010124 boolean willExtract = extractedTextModeWillBeStarted();
Gilles Debunne17d31de2011-01-27 11:02:18 -080010125
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010126 // Do not start the action mode when extracted text will show up full screen, thus
10127 // immediately hiding the newly created action bar, which would be visually distracting.
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010128 if (!willExtract) {
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010129 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
10130 mSelectionActionMode = startActionMode(actionModeCallback);
10131 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010132
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010133 final boolean selectionStarted = mSelectionActionMode != null || willExtract;
10134 if (selectionStarted && !mTextIsSelectable && mSoftInputShownOnFocus) {
Gilles Debunne17d31de2011-01-27 11:02:18 -080010135 // Show the IME to be able to replace text, except when selecting non editable text.
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010136 final InputMethodManager imm = InputMethodManager.peekInstance();
10137 if (imm != null) {
10138 imm.showSoftInput(this, 0, null);
10139 }
Gilles Debunne17d31de2011-01-27 11:02:18 -080010140 }
10141
10142 return selectionStarted;
Gilles Debunne05336272010-07-09 20:13:45 -070010143 }
10144
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010145 private boolean extractedTextModeWillBeStarted() {
10146 if (!(this instanceof ExtractEditText)) {
10147 final InputMethodManager imm = InputMethodManager.peekInstance();
10148 return imm != null && imm.isFullscreenMode();
10149 }
10150 return false;
10151 }
10152
Gilles Debunneed279f82010-08-18 21:24:35 -070010153 private void stopSelectionActionMode() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010154 if (mSelectionActionMode != null) {
Gilles Debunned94f8c52011-01-10 11:29:15 -080010155 // This will hide the mSelectionModifierCursorController
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010156 mSelectionActionMode.finish();
10157 }
10158 }
10159
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010160 /**
10161 * Paste clipboard content between min and max positions.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010162 */
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010163 private void paste(int min, int max) {
10164 ClipboardManager clipboard =
10165 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010166 ClipData clip = clipboard.getPrimaryClip();
10167 if (clip != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010168 boolean didFirst = false;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010169 for (int i=0; i<clip.getItemCount(); i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080010170 CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010171 if (paste != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010172 if (!didFirst) {
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010173 long minMax = prepareSpacesAroundPaste(min, max, paste);
10174 min = extractRangeStartFromLong(minMax);
10175 max = extractRangeEndFromLong(minMax);
10176 Selection.setSelection((Spannable) mText, max);
10177 ((Editable) mText).replace(min, max, paste);
Gilles Debunne75beb332011-04-29 11:40:22 -070010178 didFirst = true;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010179 } else {
10180 ((Editable) mText).insert(getSelectionEnd(), "\n");
10181 ((Editable) mText).insert(getSelectionEnd(), paste);
10182 }
10183 }
10184 }
10185 stopSelectionActionMode();
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010186 sLastCutOrCopyTime = 0;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010187 }
10188 }
10189
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010190 private void setPrimaryClip(ClipData clip) {
10191 ClipboardManager clipboard = (ClipboardManager) getContext().
10192 getSystemService(Context.CLIPBOARD_SERVICE);
10193 clipboard.setPrimaryClip(clip);
10194 sLastCutOrCopyTime = SystemClock.uptimeMillis();
10195 }
10196
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010197 /**
10198 * An ActionMode Callback class that is used to provide actions while in text selection mode.
10199 *
10200 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
10201 * on which of these this TextView supports.
10202 */
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010203 private class SelectionActionModeCallback implements ActionMode.Callback {
10204
10205 @Override
10206 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Adam Powelld2b58942011-10-03 13:41:03 -070010207 TypedArray styledAttributes = mContext.obtainStyledAttributes(
10208 com.android.internal.R.styleable.SelectionModeDrawables);
Gilles Debunne78996c92010-10-12 16:01:47 -070010209
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010210 boolean allowText = getContext().getResources().getBoolean(
Adam Powell35aecd52011-07-01 13:43:49 -070010211 com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010212
Gilles Debunne21078e42011-08-02 10:22:35 -070010213 mode.setTitle(allowText ?
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010214 mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010215 mode.setSubtitle(null);
10216
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010217 int selectAllIconId = 0; // No icon by default
10218 if (!allowText) {
10219 // Provide an icon, text will not be displayed on smaller screens.
10220 selectAllIconId = styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010221 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010222 }
10223
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010224 menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010225 setIcon(selectAllIconId).
Adam Powelld8404b22010-10-13 14:26:41 -070010226 setAlphabeticShortcut('a').
10227 setShowAsAction(
10228 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010229
10230 if (canCut()) {
10231 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010232 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010233 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010234 setAlphabeticShortcut('x').
10235 setShowAsAction(
10236 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010237 }
10238
10239 if (canCopy()) {
10240 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010241 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010242 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010243 setAlphabeticShortcut('c').
10244 setShowAsAction(
10245 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010246 }
10247
10248 if (canPaste()) {
10249 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010250 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010251 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010252 setAlphabeticShortcut('v').
10253 setShowAsAction(
10254 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010255 }
10256
Gilles Debunne78996c92010-10-12 16:01:47 -070010257 styledAttributes.recycle();
10258
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010259 if (mCustomSelectionActionModeCallback != null) {
Gilles Debunneddd6f392011-01-27 09:48:01 -080010260 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
10261 // The custom mode can choose to cancel the action mode
10262 return false;
10263 }
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010264 }
10265
10266 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -080010267 getSelectionController().show();
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010268 return true;
10269 } else {
10270 return false;
10271 }
Gilles Debunne05336272010-07-09 20:13:45 -070010272 }
10273
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010274 @Override
10275 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010276 if (mCustomSelectionActionModeCallback != null) {
10277 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
10278 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010279 return true;
10280 }
10281
10282 @Override
10283 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010284 if (mCustomSelectionActionModeCallback != null &&
10285 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
10286 return true;
10287 }
Jeff Brownc1df9072010-12-21 16:38:50 -080010288 return onTextContextMenuItem(item.getItemId());
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010289 }
10290
10291 @Override
10292 public void onDestroyActionMode(ActionMode mode) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010293 if (mCustomSelectionActionModeCallback != null) {
10294 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
10295 }
Gilles Debunned94f8c52011-01-10 11:29:15 -080010296 Selection.setSelection((Spannable) mText, getSelectionEnd());
10297
10298 if (mSelectionModifierCursorController != null) {
10299 mSelectionModifierCursorController.hide();
10300 }
10301
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010302 mSelectionActionMode = null;
10303 }
10304 }
Gilles Debunne05336272010-07-09 20:13:45 -070010305
Gilles Debunne21078e42011-08-02 10:22:35 -070010306 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
10307 private static final int POPUP_TEXT_LAYOUT =
Gilles Debunne646f8562011-07-27 17:44:03 -070010308 com.android.internal.R.layout.text_edit_action_popup_text;
Gilles Debunne646f8562011-07-27 17:44:03 -070010309 private TextView mPasteTextView;
10310 private TextView mReplaceTextView;
Gilles Debunne646f8562011-07-27 17:44:03 -070010311
Gilles Debunne21078e42011-08-02 10:22:35 -070010312 @Override
10313 protected void createPopupWindow() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010314 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010315 com.android.internal.R.attr.textSelectHandleWindowStyle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010316 mPopupWindow.setClippingEnabled(true);
Gilles Debunne21078e42011-08-02 10:22:35 -070010317 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010318
Gilles Debunne21078e42011-08-02 10:22:35 -070010319 @Override
10320 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -070010321 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
10322 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
10323 mContentView = linearLayout;
Gilles Debunne646f8562011-07-27 17:44:03 -070010324 mContentView.setBackgroundResource(
Gilles Debunne0eea6682011-08-29 13:30:31 -070010325 com.android.internal.R.drawable.text_edit_paste_window);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010326
Gilles Debunne646f8562011-07-27 17:44:03 -070010327 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010328 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010329
Gilles Debunne21078e42011-08-02 10:22:35 -070010330 LayoutParams wrapContent = new LayoutParams(
10331 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
10332
10333 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010334 mPasteTextView.setLayoutParams(wrapContent);
10335 mContentView.addView(mPasteTextView);
10336 mPasteTextView.setText(com.android.internal.R.string.paste);
10337 mPasteTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010338
Gilles Debunne21078e42011-08-02 10:22:35 -070010339 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010340 mReplaceTextView.setLayoutParams(wrapContent);
10341 mContentView.addView(mReplaceTextView);
10342 mReplaceTextView.setText(com.android.internal.R.string.replace);
10343 mReplaceTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010344 }
10345
Gilles Debunne21078e42011-08-02 10:22:35 -070010346 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010347 public void show() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010348 boolean canPaste = canPaste();
Gilles Debunne6435a562011-08-04 21:22:30 -070010349 boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
Gilles Debunne248b1122011-08-12 13:24:16 -070010350 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
Gilles Debunne6435a562011-08-04 21:22:30 -070010351 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
Gilles Debunne072b50c2011-08-02 20:22:43 -070010352
Gilles Debunne6435a562011-08-04 21:22:30 -070010353 if (!canPaste && !canSuggest) return;
Gilles Debunne072b50c2011-08-02 20:22:43 -070010354
Gilles Debunne21078e42011-08-02 10:22:35 -070010355 super.show();
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010356 }
10357
10358 @Override
Gilles Debunne646f8562011-07-27 17:44:03 -070010359 public void onClick(View view) {
10360 if (view == mPasteTextView && canPaste()) {
Gilles Debunne459ac632011-07-12 16:36:33 -070010361 onTextContextMenuItem(ID_PASTE);
Gilles Debunne646f8562011-07-27 17:44:03 -070010362 hide();
10363 } else if (view == mReplaceTextView) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010364 final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
10365 stopSelectionActionMode();
10366 Selection.setSelection((Spannable) mText, middle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010367 showSuggestions();
Gilles Debunnee1c14e62010-11-03 19:24:29 -070010368 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010369 }
10370
Gilles Debunne21078e42011-08-02 10:22:35 -070010371 @Override
10372 protected int getTextOffset() {
10373 return (getSelectionStart() + getSelectionEnd()) / 2;
10374 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010375
Gilles Debunne21078e42011-08-02 10:22:35 -070010376 @Override
10377 protected int getVerticalLocalPosition(int line) {
10378 return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
10379 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010380
Gilles Debunne21078e42011-08-02 10:22:35 -070010381 @Override
10382 protected int clipVertically(int positionY) {
10383 if (positionY < 0) {
10384 final int offset = getTextOffset();
10385 final int line = mLayout.getLineForOffset(offset);
10386 positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
10387 positionY += mContentView.getMeasuredHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010388
Gilles Debunne646f8562011-07-27 17:44:03 -070010389 // Assumes insertion and selection handles share the same height
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010390 final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
Gilles Debunne21078e42011-08-02 10:22:35 -070010391 positionY += handle.getIntrinsicHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010392 }
10393
Gilles Debunne21078e42011-08-02 10:22:35 -070010394 return positionY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010395 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010396 }
10397
Gilles Debunne21078e42011-08-02 10:22:35 -070010398 private abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010399 protected Drawable mDrawable;
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010400 protected Drawable mDrawableLtr;
10401 protected Drawable mDrawableRtl;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010402 private final PopupWindow mContainer;
10403 // Position with respect to the parent TextView
10404 private int mPositionX, mPositionY;
Adam Powell879fb6b2010-09-20 11:23:56 -070010405 private boolean mIsDragging;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010406 // Offset from touch position to mPosition
10407 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
Gilles Debunne21078e42011-08-02 10:22:35 -070010408 protected int mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010409 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
Adam Powellfbb3b472010-10-06 21:04:35 -070010410 private float mTouchOffsetY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010411 // Where the touch position should be on the handle to ensure a maximum cursor visibility
10412 private float mIdealVerticalOffset;
Gilles Debunne2037b822011-04-22 13:07:33 -070010413 // Parent's (TextView) previous position in window
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010414 private int mLastParentX, mLastParentY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010415 // Transient action popup window for Paste and Replace actions
10416 protected ActionPopupWindow mActionPopupWindow;
Gilles Debunne21078e42011-08-02 10:22:35 -070010417 // Previous text character offset
10418 private int mPreviousOffset = -1;
10419 // Previous text character offset
10420 private boolean mPositionHasChanged = true;
Gilles Debunne646f8562011-07-27 17:44:03 -070010421 // Used to delay the appearance of the action popup window
10422 private Runnable mActionPopupShower;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010423
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010424 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010425 super(TextView.this.mContext);
10426 mContainer = new PopupWindow(TextView.this.mContext, null,
10427 com.android.internal.R.attr.textSelectHandleWindowStyle);
10428 mContainer.setSplitTouchEnabled(true);
10429 mContainer.setClippingEnabled(false);
10430 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
10431 mContainer.setContentView(this);
10432
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010433 mDrawableLtr = drawableLtr;
10434 mDrawableRtl = drawableRtl;
10435
10436 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010437
10438 final int handleHeight = mDrawable.getIntrinsicHeight();
10439 mTouchOffsetY = -0.3f * handleHeight;
10440 mIdealVerticalOffset = 0.7f * handleHeight;
10441 }
10442
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010443 protected void updateDrawable() {
10444 final int offset = getCurrentCursorOffset();
10445 final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
10446 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
10447 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
10448 }
10449
10450 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070010451
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010452 // Touch-up filter: number of previous positions remembered
10453 private static final int HISTORY_SIZE = 5;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010454 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
10455 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010456 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
10457 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
10458 private int mPreviousOffsetIndex = 0;
10459 private int mNumberPreviousOffsets = 0;
10460
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010461 private void startTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010462 mNumberPreviousOffsets = 0;
10463 addPositionToTouchUpFilter(offset);
10464 }
10465
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010466 private void addPositionToTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010467 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
10468 mPreviousOffsets[mPreviousOffsetIndex] = offset;
10469 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
10470 mNumberPreviousOffsets++;
10471 }
10472
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010473 private void filterOnTouchUp() {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010474 final long now = SystemClock.uptimeMillis();
10475 int i = 0;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010476 int index = mPreviousOffsetIndex;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010477 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010478 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010479 i++;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010480 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010481 }
10482
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010483 if (i > 0 && i < iMax &&
10484 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010485 positionAtCursorOffset(mPreviousOffsets[index], false);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010486 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010487 }
10488
Gilles Debunne040023a2011-08-02 18:23:31 -070010489 public boolean offsetHasBeenChanged() {
10490 return mNumberPreviousOffsets > 1;
10491 }
10492
Adam Powell879fb6b2010-09-20 11:23:56 -070010493 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010494 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010495 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
Adam Powell879fb6b2010-09-20 11:23:56 -070010496 }
10497
10498 public void show() {
Gilles Debunne21078e42011-08-02 10:22:35 -070010499 if (isShowing()) return;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010500
Gilles Debunnef682a772011-08-31 15:49:10 -070010501 getPositionListener().addSubscriber(this, true /* local position may change */);
Gilles Debunne21078e42011-08-02 10:22:35 -070010502
10503 // Make sure the offset is always considered new, even when focusing at same position
10504 mPreviousOffset = -1;
Gilles Debunnef682a772011-08-31 15:49:10 -070010505 positionAtCursorOffset(getCurrentCursorOffset(), false);
Gilles Debunne21078e42011-08-02 10:22:35 -070010506
Gilles Debunne646f8562011-07-27 17:44:03 -070010507 hideActionPopupWindow();
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010508 }
10509
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010510 protected void dismiss() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010511 mIsDragging = false;
10512 mContainer.dismiss();
Gilles Debunne646f8562011-07-27 17:44:03 -070010513 onDetached();
Adam Powell879fb6b2010-09-20 11:23:56 -070010514 }
10515
10516 public void hide() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010517 dismiss();
10518
Gilles Debunne21078e42011-08-02 10:22:35 -070010519 TextView.this.getPositionListener().removeSubscriber(this);
Gilles Debunne646f8562011-07-27 17:44:03 -070010520 }
10521
Gilles Debunnecb2516b2011-08-05 17:02:54 -070010522 void showActionPopupWindow(int delay) {
Gilles Debunne646f8562011-07-27 17:44:03 -070010523 if (mActionPopupWindow == null) {
10524 mActionPopupWindow = new ActionPopupWindow();
10525 }
10526 if (mActionPopupShower == null) {
10527 mActionPopupShower = new Runnable() {
10528 public void run() {
10529 mActionPopupWindow.show();
10530 }
10531 };
10532 } else {
10533 TextView.this.removeCallbacks(mActionPopupShower);
10534 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010535 TextView.this.postDelayed(mActionPopupShower, delay);
10536 }
10537
10538 protected void hideActionPopupWindow() {
10539 if (mActionPopupShower != null) {
10540 TextView.this.removeCallbacks(mActionPopupShower);
10541 }
10542 if (mActionPopupWindow != null) {
10543 mActionPopupWindow.hide();
10544 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010545 }
10546
10547 public boolean isShowing() {
10548 return mContainer.isShowing();
10549 }
10550
Gilles Debunne21078e42011-08-02 10:22:35 -070010551 private boolean isVisible() {
Adam Powellabcbb1a2010-10-04 21:12:19 -070010552 // Always show a dragging handle.
10553 if (mIsDragging) {
10554 return true;
10555 }
10556
Adam Powell965b9692010-10-21 18:44:32 -070010557 if (isInBatchEditMode()) {
10558 return false;
10559 }
10560
Gilles Debunne64901d42011-11-25 10:23:38 +010010561 return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY);
Adam Powell879fb6b2010-09-20 11:23:56 -070010562 }
10563
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010564 public abstract int getCurrentCursorOffset();
10565
Gilles Debunne186aaf92011-09-16 14:26:12 -070010566 protected abstract void updateSelection(int offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010567
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010568 public abstract void updatePosition(float x, float y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010569
Gilles Debunnef682a772011-08-31 15:49:10 -070010570 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010571 // A HandleView relies on the layout, which may be nulled by external methods
Gilles Debunned4bb0b02011-05-27 15:58:55 -070010572 if (mLayout == null) {
10573 // Will update controllers' state, hiding them and stopping selection mode if needed
10574 prepareCursorControllers();
10575 return;
10576 }
10577
Gilles Debunnef682a772011-08-31 15:49:10 -070010578 if (offset != mPreviousOffset || parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010579 updateSelection(offset);
10580 addPositionToTouchUpFilter(offset);
10581 final int line = mLayout.getLineForOffset(offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010582
Gilles Debunne21078e42011-08-02 10:22:35 -070010583 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
10584 mPositionY = mLayout.getLineBottom(line);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010585
Gilles Debunnefec22c62011-10-21 13:48:18 -070010586 // Take TextView's padding and scroll into account.
Gilles Debunne21078e42011-08-02 10:22:35 -070010587 mPositionX += viewportToContentHorizontalOffset();
10588 mPositionY += viewportToContentVerticalOffset();
10589
10590 mPreviousOffset = offset;
10591 mPositionHasChanged = true;
10592 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010593 }
10594
Gilles Debunnef682a772011-08-31 15:49:10 -070010595 public void updatePosition(int parentPositionX, int parentPositionY,
10596 boolean parentPositionChanged, boolean parentScrolled) {
10597 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
10598 if (parentPositionChanged || mPositionHasChanged) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010599 if (mIsDragging) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010600 // Update touchToWindow offset in case of parent scrolling while dragging
10601 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
10602 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
10603 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
10604 mLastParentX = parentPositionX;
10605 mLastParentY = parentPositionY;
Gilles Debunne2037b822011-04-22 13:07:33 -070010606 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010607
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010608 onHandleMoved();
10609 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010610
Gilles Debunne21078e42011-08-02 10:22:35 -070010611 if (isVisible()) {
10612 final int positionX = parentPositionX + mPositionX;
10613 final int positionY = parentPositionY + mPositionY;
10614 if (isShowing()) {
10615 mContainer.update(positionX, positionY, -1, -1);
10616 } else {
10617 mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
10618 positionX, positionY);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010619 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010620 } else {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010621 if (isShowing()) {
10622 dismiss();
10623 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010624 }
Gilles Debunne21078e42011-08-02 10:22:35 -070010625
10626 mPositionHasChanged = false;
Adam Powell879fb6b2010-09-20 11:23:56 -070010627 }
10628 }
10629
10630 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010631 protected void onDraw(Canvas c) {
Adam Powell879fb6b2010-09-20 11:23:56 -070010632 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010633 mDrawable.draw(c);
Adam Powell879fb6b2010-09-20 11:23:56 -070010634 }
10635
10636 @Override
10637 public boolean onTouchEvent(MotionEvent ev) {
10638 switch (ev.getActionMasked()) {
Gilles Debunne874d77c2011-01-25 15:29:13 -080010639 case MotionEvent.ACTION_DOWN: {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010640 startTouchUpFilter(getCurrentCursorOffset());
10641 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
10642 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010643
Gilles Debunne21078e42011-08-02 10:22:35 -070010644 final PositionListener positionListener = getPositionListener();
10645 mLastParentX = positionListener.getPositionX();
10646 mLastParentY = positionListener.getPositionY();
Gilles Debunne874d77c2011-01-25 15:29:13 -080010647 mIsDragging = true;
10648 break;
10649 }
10650
10651 case MotionEvent.ACTION_MOVE: {
10652 final float rawX = ev.getRawX();
10653 final float rawY = ev.getRawY();
Gilles Debunneddf00b82011-02-23 17:25:13 -080010654
10655 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
10656 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
10657 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
10658 float newVerticalOffset;
10659 if (previousVerticalOffset < mIdealVerticalOffset) {
10660 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
10661 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
10662 } else {
10663 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
10664 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
10665 }
10666 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
10667
Gilles Debunne874d77c2011-01-25 15:29:13 -080010668 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010669 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunne874d77c2011-01-25 15:29:13 -080010670
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010671 updatePosition(newPosX, newPosY);
Gilles Debunne874d77c2011-01-25 15:29:13 -080010672 break;
10673 }
10674
10675 case MotionEvent.ACTION_UP:
Gilles Debunne874d77c2011-01-25 15:29:13 -080010676 filterOnTouchUp();
10677 mIsDragging = false;
10678 break;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -080010679
Gilles Debunne874d77c2011-01-25 15:29:13 -080010680 case MotionEvent.ACTION_CANCEL:
10681 mIsDragging = false;
10682 break;
Adam Powell879fb6b2010-09-20 11:23:56 -070010683 }
10684 return true;
10685 }
10686
10687 public boolean isDragging() {
10688 return mIsDragging;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010689 }
Gilles Debunne64e54a62010-09-07 19:07:17 -070010690
Gilles Debunne2037b822011-04-22 13:07:33 -070010691 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010692 hideActionPopupWindow();
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010693 }
Gilles Debunne69340442011-03-31 13:37:51 -070010694
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010695 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010696 hideActionPopupWindow();
Gilles Debunne81f08082011-02-17 14:07:19 -080010697 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010698 }
10699
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010700 private class InsertionHandleView extends HandleView {
Gilles Debunne646f8562011-07-27 17:44:03 -070010701 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010702 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010703
Gilles Debunne646f8562011-07-27 17:44:03 -070010704 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010705 private float mDownPositionX, mDownPositionY;
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010706 private Runnable mHider;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010707
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010708 public InsertionHandleView(Drawable drawable) {
10709 super(drawable, drawable);
10710 }
10711
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010712 @Override
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010713 public void show() {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010714 super.show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010715
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010716 final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
10717 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010718 showActionPopupWindow(0);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010719 }
Gilles Debunne6435a562011-08-04 21:22:30 -070010720
10721 hideAfterDelay();
10722 }
10723
10724 public void showWithActionPopup() {
10725 show();
10726 showActionPopupWindow(0);
Gilles Debunne9948ad72010-11-24 14:00:46 -080010727 }
10728
Gilles Debunne646f8562011-07-27 17:44:03 -070010729 private void hideAfterDelay() {
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010730 removeHiderCallback();
10731 if (mHider == null) {
10732 mHider = new Runnable() {
10733 public void run() {
10734 hide();
10735 }
10736 };
10737 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010738 TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010739 }
10740
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010741 private void removeHiderCallback() {
10742 if (mHider != null) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010743 TextView.this.removeCallbacks(mHider);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010744 }
10745 }
10746
10747 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010748 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10749 return drawable.getIntrinsicWidth() / 2;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010750 }
10751
10752 @Override
10753 public boolean onTouchEvent(MotionEvent ev) {
10754 final boolean result = super.onTouchEvent(ev);
10755
10756 switch (ev.getActionMasked()) {
10757 case MotionEvent.ACTION_DOWN:
10758 mDownPositionX = ev.getRawX();
10759 mDownPositionY = ev.getRawY();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010760 break;
10761
10762 case MotionEvent.ACTION_UP:
Gilles Debunne040023a2011-08-02 18:23:31 -070010763 if (!offsetHasBeenChanged()) {
10764 final float deltaX = mDownPositionX - ev.getRawX();
10765 final float deltaY = mDownPositionY - ev.getRawY();
10766 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10767 if (distanceSquared < mSquaredTouchSlopDistance) {
10768 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
10769 // Tapping on the handle dismisses the displayed action popup
10770 mActionPopupWindow.hide();
10771 } else {
Gilles Debunne6435a562011-08-04 21:22:30 -070010772 showWithActionPopup();
Gilles Debunne040023a2011-08-02 18:23:31 -070010773 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010774 }
10775 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010776 hideAfterDelay();
Gilles Debunne2037b822011-04-22 13:07:33 -070010777 break;
10778
10779 case MotionEvent.ACTION_CANCEL:
Gilles Debunne646f8562011-07-27 17:44:03 -070010780 hideAfterDelay();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010781 break;
10782
10783 default:
10784 break;
10785 }
10786
10787 return result;
10788 }
10789
10790 @Override
10791 public int getCurrentCursorOffset() {
10792 return TextView.this.getSelectionStart();
10793 }
10794
10795 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010796 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010797 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010798 }
10799
10800 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010801 public void updatePosition(float x, float y) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010802 positionAtCursorOffset(getOffsetForPosition(x, y), false);
Gilles Debunne05336272010-07-09 20:13:45 -070010803 }
10804
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010805 @Override
Gilles Debunne2037b822011-04-22 13:07:33 -070010806 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010807 super.onHandleMoved();
Gilles Debunne2037b822011-04-22 13:07:33 -070010808 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010809 }
10810
10811 @Override
10812 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010813 super.onDetached();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010814 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010815 }
10816 }
10817
10818 private class SelectionStartHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010819
10820 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10821 super(drawableLtr, drawableRtl);
10822 }
10823
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010824 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010825 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10826 if (isRtlRun) {
10827 return drawable.getIntrinsicWidth() / 4;
10828 } else {
10829 return (drawable.getIntrinsicWidth() * 3) / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010830 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010831 }
10832
10833 @Override
10834 public int getCurrentCursorOffset() {
10835 return TextView.this.getSelectionStart();
10836 }
10837
10838 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010839 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010840 Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
Gilles Debunne186aaf92011-09-16 14:26:12 -070010841 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010842 }
10843
10844 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010845 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010846 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010847
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010848 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010849 final int selectionEnd = getSelectionEnd();
Gilles Debunne1a22db22011-11-20 22:13:21 +010010850 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010851
Gilles Debunnef682a772011-08-31 15:49:10 -070010852 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010853 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010854
10855 public ActionPopupWindow getActionPopupWindow() {
10856 return mActionPopupWindow;
10857 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010858 }
10859
10860 private class SelectionEndHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010861
10862 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10863 super(drawableLtr, drawableRtl);
10864 }
10865
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010866 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010867 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10868 if (isRtlRun) {
10869 return (drawable.getIntrinsicWidth() * 3) / 4;
10870 } else {
10871 return drawable.getIntrinsicWidth() / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010872 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010873 }
10874
10875 @Override
10876 public int getCurrentCursorOffset() {
10877 return TextView.this.getSelectionEnd();
10878 }
10879
10880 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010881 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010882 Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
Gilles Debunne186aaf92011-09-16 14:26:12 -070010883 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010884 }
10885
10886 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010887 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010888 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010889
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010890 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010891 final int selectionStart = getSelectionStart();
Gilles Debunne1a22db22011-11-20 22:13:21 +010010892 if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length());
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010893
Gilles Debunnef682a772011-08-31 15:49:10 -070010894 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010895 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010896
10897 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
10898 mActionPopupWindow = actionPopupWindow;
10899 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010900 }
10901
10902 /**
10903 * A CursorController instance can be used to control a cursor in the text.
10904 * It is not used outside of {@link TextView}.
10905 * @hide
10906 */
10907 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
10908 /**
10909 * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
10910 * See also {@link #hide()}.
10911 */
10912 public void show();
10913
10914 /**
10915 * Hide the cursor controller from screen.
10916 * See also {@link #show()}.
10917 */
10918 public void hide();
10919
10920 /**
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010921 * Called when the view is detached from window. Perform house keeping task, such as
10922 * stopping Runnable thread that would otherwise keep a reference on the context, thus
10923 * preventing the activity from being recycled.
10924 */
10925 public void onDetached();
10926 }
10927
10928 private class InsertionPointCursorController implements CursorController {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010929 private InsertionHandleView mHandle;
10930
10931 public void show() {
Gilles Debunne6435a562011-08-04 21:22:30 -070010932 getHandle().show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010933 }
10934
Gilles Debunne6435a562011-08-04 21:22:30 -070010935 public void showWithActionPopup() {
10936 getHandle().showWithActionPopup();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010937 }
10938
10939 public void hide() {
10940 if (mHandle != null) {
10941 mHandle.hide();
10942 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010943 }
10944
Adam Powell624380a2010-10-02 18:12:02 -070010945 public void onTouchModeChanged(boolean isInTouchMode) {
10946 if (!isInTouchMode) {
10947 hide();
10948 }
10949 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010950
Gilles Debunne646f8562011-07-27 17:44:03 -070010951 private InsertionHandleView getHandle() {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010952 if (mSelectHandleCenter == null) {
10953 mSelectHandleCenter = mContext.getResources().getDrawable(
10954 mTextSelectHandleRes);
10955 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010956 if (mHandle == null) {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010957 mHandle = new InsertionHandleView(mSelectHandleCenter);
Gilles Debunnec4440f02010-11-24 14:40:48 -080010958 }
10959 return mHandle;
10960 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010961
10962 @Override
10963 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070010964 final ViewTreeObserver observer = getViewTreeObserver();
10965 observer.removeOnTouchModeChangeListener(this);
10966
Gilles Debunne6a855082011-03-11 16:51:24 -080010967 if (mHandle != null) mHandle.onDetached();
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010968 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010969 }
10970
Jeff Brown01ce2e92010-09-26 22:20:12 -070010971 private class SelectionModifierCursorController implements CursorController {
Gilles Debunne6435a562011-08-04 21:22:30 -070010972 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010973 // The cursor controller handles, lazily created when shown.
10974 private SelectionStartHandleView mStartHandle;
10975 private SelectionEndHandleView mEndHandle;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010976 // The offsets of that last touch down event. Remembered to start selection there.
10977 private int mMinTouchOffset, mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070010978
Gilles Debunne5347c582010-10-27 14:22:35 -070010979 // Double tap detection
10980 private long mPreviousTapUpTime = 0;
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010981 private float mPreviousTapPositionX, mPreviousTapPositionY;
Gilles Debunne5347c582010-10-27 14:22:35 -070010982
Gilles Debunne05336272010-07-09 20:13:45 -070010983 SelectionModifierCursorController() {
Gilles Debunne380b6042010-10-08 16:12:11 -070010984 resetTouchOffsets();
Gilles Debunne05336272010-07-09 20:13:45 -070010985 }
10986
10987 public void show() {
Adam Powell965b9692010-10-21 18:44:32 -070010988 if (isInBatchEditMode()) {
10989 return;
10990 }
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010991 initDrawables();
10992 initHandles();
10993 hideInsertionPointCursorController();
10994 }
Adam Powell965b9692010-10-21 18:44:32 -070010995
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010996 private void initDrawables() {
10997 if (mSelectHandleLeft == null) {
10998 mSelectHandleLeft = mContext.getResources().getDrawable(
10999 mTextSelectHandleLeftRes);
11000 }
11001 if (mSelectHandleRight == null) {
11002 mSelectHandleRight = mContext.getResources().getDrawable(
11003 mTextSelectHandleRightRes);
11004 }
11005 }
11006
11007 private void initHandles() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011008 // Lazy object creation has to be done before updatePosition() is called.
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070011009 if (mStartHandle == null) {
11010 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
11011 }
11012 if (mEndHandle == null) {
11013 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
11014 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080011015
Adam Powell879fb6b2010-09-20 11:23:56 -070011016 mStartHandle.show();
11017 mEndHandle.show();
Gilles Debunnec4440f02010-11-24 14:40:48 -080011018
Gilles Debunne646f8562011-07-27 17:44:03 -070011019 // Make sure both left and right handles share the same ActionPopupWindow (so that
11020 // moving any of the handles hides the action popup).
Gilles Debunnecb2516b2011-08-05 17:02:54 -070011021 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
Gilles Debunne646f8562011-07-27 17:44:03 -070011022 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
11023
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011024 hideInsertionPointCursorController();
Gilles Debunne05336272010-07-09 20:13:45 -070011025 }
11026
11027 public void hide() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011028 if (mStartHandle != null) mStartHandle.hide();
11029 if (mEndHandle != null) mEndHandle.hide();
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080011030 }
11031
Gilles Debunnebb588da2011-07-11 18:26:19 -070011032 public void onTouchEvent(MotionEvent event) {
Gilles Debunne528c64882010-10-08 11:56:13 -070011033 // This is done even when the View does not have focus, so that long presses can start
11034 // selection and tap can move cursor from this tap position.
Gilles Debunnebb588da2011-07-11 18:26:19 -070011035 switch (event.getActionMasked()) {
11036 case MotionEvent.ACTION_DOWN:
11037 final float x = event.getX();
11038 final float y = event.getY();
Gilles Debunne05336272010-07-09 20:13:45 -070011039
Gilles Debunnebb588da2011-07-11 18:26:19 -070011040 // Remember finger down position, to be able to start selection from there
11041 mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
Gilles Debunne05336272010-07-09 20:13:45 -070011042
Gilles Debunnebb588da2011-07-11 18:26:19 -070011043 // Double tap detection
11044 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
11045 if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
11046 isPositionOnText(x, y)) {
11047 final float deltaX = x - mPreviousTapPositionX;
11048 final float deltaY = y - mPreviousTapPositionY;
11049 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
11050 if (distanceSquared < mSquaredTouchSlopDistance) {
Gilles Debunne646f8562011-07-27 17:44:03 -070011051 startSelectionActionMode();
Gilles Debunnebb588da2011-07-11 18:26:19 -070011052 mDiscardNextActionUp = true;
Gilles Debunne5347c582010-10-27 14:22:35 -070011053 }
Gilles Debunnebb588da2011-07-11 18:26:19 -070011054 }
Gilles Debunne5347c582010-10-27 14:22:35 -070011055
Gilles Debunnebb588da2011-07-11 18:26:19 -070011056 mPreviousTapPositionX = x;
11057 mPreviousTapPositionY = y;
Gilles Debunnebb588da2011-07-11 18:26:19 -070011058 break;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011059
Gilles Debunnebb588da2011-07-11 18:26:19 -070011060 case MotionEvent.ACTION_POINTER_DOWN:
11061 case MotionEvent.ACTION_POINTER_UP:
11062 // Handle multi-point gestures. Keep min and max offset positions.
11063 // Only activated for devices that correctly handle multi-touch.
11064 if (mContext.getPackageManager().hasSystemFeature(
11065 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
11066 updateMinAndMaxOffsets(event);
11067 }
11068 break;
Gilles Debunne5347c582010-10-27 14:22:35 -070011069
Gilles Debunnebb588da2011-07-11 18:26:19 -070011070 case MotionEvent.ACTION_UP:
11071 mPreviousTapUpTime = SystemClock.uptimeMillis();
11072 break;
Gilles Debunne05336272010-07-09 20:13:45 -070011073 }
11074 }
11075
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011076 /**
11077 * @param event
11078 */
11079 private void updateMinAndMaxOffsets(MotionEvent event) {
11080 int pointerCount = event.getPointerCount();
11081 for (int index = 0; index < pointerCount; index++) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011082 int offset = getOffsetForPosition(event.getX(index), event.getY(index));
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011083 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
11084 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
11085 }
11086 }
11087
11088 public int getMinTouchOffset() {
11089 return mMinTouchOffset;
11090 }
11091
11092 public int getMaxTouchOffset() {
11093 return mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070011094 }
11095
Gilles Debunne380b6042010-10-08 16:12:11 -070011096 public void resetTouchOffsets() {
11097 mMinTouchOffset = mMaxTouchOffset = -1;
11098 }
11099
Gilles Debunne05336272010-07-09 20:13:45 -070011100 /**
11101 * @return true iff this controller is currently used to move the selection start.
11102 */
11103 public boolean isSelectionStartDragged() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011104 return mStartHandle != null && mStartHandle.isDragging();
Gilles Debunne05336272010-07-09 20:13:45 -070011105 }
Adam Powell624380a2010-10-02 18:12:02 -070011106
11107 public void onTouchModeChanged(boolean isInTouchMode) {
11108 if (!isInTouchMode) {
11109 hide();
11110 }
11111 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080011112
11113 @Override
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011114 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070011115 final ViewTreeObserver observer = getViewTreeObserver();
11116 observer.removeOnTouchModeChangeListener(this);
11117
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011118 if (mStartHandle != null) mStartHandle.onDetached();
11119 if (mEndHandle != null) mEndHandle.onDetached();
11120 }
Gilles Debunne05336272010-07-09 20:13:45 -070011121 }
11122
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011123 private void hideInsertionPointCursorController() {
Gilles Debunnee587d832010-11-23 20:20:11 -080011124 // No need to create the controller to hide it.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011125 if (mInsertionPointCursorController != null) {
11126 mInsertionPointCursorController.hide();
11127 }
11128 }
11129
Gilles Debunned94f8c52011-01-10 11:29:15 -080011130 /**
11131 * Hides the insertion controller and stops text selection mode, hiding the selection controller
11132 */
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011133 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +010011134 hideCursorControllers();
11135 hideSpanControllers();
11136 }
Luca Zanoline6d36822011-08-30 18:04:34 +010011137
Luca Zanolin1564fc72011-09-07 00:01:28 +010011138 private void hideSpanControllers() {
Luca Zanoline6d36822011-08-30 18:04:34 +010011139 if (mChangeWatcher != null) {
11140 mChangeWatcher.hideControllers();
11141 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011142 }
11143
Luca Zanolin1564fc72011-09-07 00:01:28 +010011144 private void hideCursorControllers() {
Gilles Debunne26c8b3a2011-10-12 14:06:58 -070011145 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
11146 // Should be done before hide insertion point controller since it triggers a show of it
11147 mSuggestionsPopupWindow.hide();
11148 }
Luca Zanolin1564fc72011-09-07 00:01:28 +010011149 hideInsertionPointCursorController();
11150 stopSelectionActionMode();
11151 }
11152
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011153 /**
Gilles Debunne9511b412011-05-23 18:33:48 -070011154 * Get the character offset closest to the specified absolute position. A typical use case is to
11155 * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011156 *
11157 * @param x The horizontal absolute position of a point on screen
11158 * @param y The vertical absolute position of a point on screen
11159 * @return the character offset for the character whose position is closest to the specified
11160 * position. Returns -1 if there is no layout.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011161 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011162 public int getOffsetForPosition(float x, float y) {
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011163 if (getLayout() == null) return -1;
Gilles Debunne9948ad72010-11-24 14:00:46 -080011164 final int line = getLineAtCoordinate(y);
11165 final int offset = getOffsetAtCoordinate(line, x);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011166 return offset;
11167 }
11168
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011169 private float convertToLocalHorizontalCoordinate(float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011170 x -= getTotalPaddingLeft();
11171 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011172 x = Math.max(0.0f, x);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011173 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11174 x += getScrollX();
11175 return x;
11176 }
11177
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011178 private int getLineAtCoordinate(float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011179 y -= getTotalPaddingTop();
11180 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011181 y = Math.max(0.0f, y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011182 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11183 y += getScrollY();
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011184 return getLayout().getLineForVertical((int) y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011185 }
11186
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011187 private int getOffsetAtCoordinate(int line, float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011188 x = convertToLocalHorizontalCoordinate(x);
11189 return getLayout().getOffsetForHorizontal(line, x);
11190 }
11191
11192 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
11193 * in the view. Returns false when the position is in the empty space of left/right of text.
11194 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011195 private boolean isPositionOnText(float x, float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011196 if (getLayout() == null) return false;
11197
11198 final int line = getLineAtCoordinate(y);
11199 x = convertToLocalHorizontalCoordinate(x);
11200
11201 if (x < getLayout().getLineLeft(line)) return false;
11202 if (x > getLayout().getLineRight(line)) return false;
11203 return true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011204 }
11205
Gilles Debunnef170a342010-11-11 11:08:59 -080011206 @Override
11207 public boolean onDragEvent(DragEvent event) {
11208 switch (event.getAction()) {
11209 case DragEvent.ACTION_DRAG_STARTED:
Gilles Debunnee587d832010-11-23 20:20:11 -080011210 return hasInsertionController();
Gilles Debunnef170a342010-11-11 11:08:59 -080011211
11212 case DragEvent.ACTION_DRAG_ENTERED:
11213 TextView.this.requestFocus();
11214 return true;
11215
Gilles Debunne2226a192010-11-30 18:50:51 -080011216 case DragEvent.ACTION_DRAG_LOCATION:
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011217 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunnef170a342010-11-11 11:08:59 -080011218 Selection.setSelection((Spannable)mText, offset);
11219 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011220
Gilles Debunne2226a192010-11-30 18:50:51 -080011221 case DragEvent.ACTION_DROP:
11222 onDrop(event);
Gilles Debunnef170a342010-11-11 11:08:59 -080011223 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011224
Gilles Debunnef170a342010-11-11 11:08:59 -080011225 case DragEvent.ACTION_DRAG_ENDED:
Gilles Debunne4ae0f292010-11-29 14:56:39 -080011226 case DragEvent.ACTION_DRAG_EXITED:
Gilles Debunnef170a342010-11-11 11:08:59 -080011227 default:
11228 return true;
11229 }
11230 }
11231
Gilles Debunne2226a192010-11-30 18:50:51 -080011232 private void onDrop(DragEvent event) {
11233 StringBuilder content = new StringBuilder("");
11234 ClipData clipData = event.getClipData();
11235 final int itemCount = clipData.getItemCount();
11236 for (int i=0; i < itemCount; i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080011237 Item item = clipData.getItemAt(i);
Gilles Debunne2226a192010-11-30 18:50:51 -080011238 content.append(item.coerceToText(TextView.this.mContext));
11239 }
11240
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011241 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunne2226a192010-11-30 18:50:51 -080011242
Gilles Debunneaaa84792010-12-03 11:10:14 -080011243 Object localState = event.getLocalState();
11244 DragLocalState dragLocalState = null;
11245 if (localState instanceof DragLocalState) {
11246 dragLocalState = (DragLocalState) localState;
11247 }
11248 boolean dragDropIntoItself = dragLocalState != null &&
11249 dragLocalState.sourceTextView == this;
11250
11251 if (dragDropIntoItself) {
11252 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
Gilles Debunne2226a192010-11-30 18:50:51 -080011253 // A drop inside the original selection discards the drop.
11254 return;
11255 }
11256 }
11257
11258 final int originalLength = mText.length();
11259 long minMax = prepareSpacesAroundPaste(offset, offset, content);
11260 int min = extractRangeStartFromLong(minMax);
11261 int max = extractRangeEndFromLong(minMax);
11262
11263 Selection.setSelection((Spannable) mText, max);
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011264 replaceText_internal(min, max, content);
Gilles Debunne2226a192010-11-30 18:50:51 -080011265
Gilles Debunneaaa84792010-12-03 11:10:14 -080011266 if (dragDropIntoItself) {
11267 int dragSourceStart = dragLocalState.start;
11268 int dragSourceEnd = dragLocalState.end;
Gilles Debunne2226a192010-11-30 18:50:51 -080011269 if (max <= dragSourceStart) {
11270 // Inserting text before selection has shifted positions
11271 final int shift = mText.length() - originalLength;
11272 dragSourceStart += shift;
11273 dragSourceEnd += shift;
11274 }
11275
11276 // Delete original selection
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011277 deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunne2226a192010-11-30 18:50:51 -080011278
11279 // Make sure we do not leave two adjacent spaces.
11280 if ((dragSourceStart == 0 ||
11281 Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
11282 (dragSourceStart == mText.length() ||
11283 Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
11284 final int pos = dragSourceStart == mText.length() ?
11285 dragSourceStart - 1 : dragSourceStart;
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011286 deleteText_internal(pos, pos + 1);
Gilles Debunne2226a192010-11-30 18:50:51 -080011287 }
11288 }
11289 }
11290
Adam Powell965b9692010-10-21 18:44:32 -070011291 /**
11292 * @return True if this view supports insertion handles.
11293 */
11294 boolean hasInsertionController() {
11295 return mInsertionControllerEnabled;
11296 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011297
Adam Powell965b9692010-10-21 18:44:32 -070011298 /**
11299 * @return True if this view supports selection handles.
11300 */
11301 boolean hasSelectionController() {
11302 return mSelectionControllerEnabled;
11303 }
11304
Gilles Debunne9948ad72010-11-24 14:00:46 -080011305 InsertionPointCursorController getInsertionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011306 if (!mInsertionControllerEnabled) {
11307 return null;
11308 }
11309
11310 if (mInsertionPointCursorController == null) {
11311 mInsertionPointCursorController = new InsertionPointCursorController();
11312
11313 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011314 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011315 }
11316
11317 return mInsertionPointCursorController;
11318 }
11319
Gilles Debunnee587d832010-11-23 20:20:11 -080011320 SelectionModifierCursorController getSelectionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011321 if (!mSelectionControllerEnabled) {
11322 return null;
11323 }
11324
11325 if (mSelectionModifierCursorController == null) {
11326 mSelectionModifierCursorController = new SelectionModifierCursorController();
11327
11328 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011329 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011330 }
11331
11332 return mSelectionModifierCursorController;
11333 }
11334
11335 boolean isInBatchEditMode() {
11336 final InputMethodState ims = mInputMethodState;
11337 if (ims != null) {
11338 return ims.mBatchEditNesting > 0;
11339 }
11340 return mInBatchEditControllers;
11341 }
11342
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011343 @Override
11344 protected void resolveTextDirection() {
Fabrice Di Meglio7810b5f2011-08-24 18:26:14 -070011345 if (hasPasswordTransformationMethod()) {
11346 mTextDir = TextDirectionHeuristics.LOCALE;
11347 return;
11348 }
11349
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011350 // Always need to resolve layout direction first
11351 final boolean defaultIsRtl = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL);
11352
11353 // Then resolve text direction on the parent
Doug Feltcb3791202011-07-07 11:57:48 -070011354 super.resolveTextDirection();
11355
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011356 // Now, we can select the heuristic
Doug Feltcb3791202011-07-07 11:57:48 -070011357 int textDir = getResolvedTextDirection();
11358 switch (textDir) {
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011359 default:
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011360 case TEXT_DIRECTION_FIRST_STRONG:
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011361 mTextDir = (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11362 TextDirectionHeuristics.FIRSTSTRONG_LTR);
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011363 break;
11364 case TEXT_DIRECTION_ANY_RTL:
Fabrice Di Meglioa6461452011-08-19 15:42:04 -070011365 mTextDir = TextDirectionHeuristics.ANYRTL_LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011366 break;
11367 case TEXT_DIRECTION_LTR:
Doug Feltcb3791202011-07-07 11:57:48 -070011368 mTextDir = TextDirectionHeuristics.LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011369 break;
11370 case TEXT_DIRECTION_RTL:
Doug Feltcb3791202011-07-07 11:57:48 -070011371 mTextDir = TextDirectionHeuristics.RTL;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011372 break;
11373 }
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011374 }
11375
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011376 /**
11377 * Subclasses will need to override this method to implement their own way of resolving
11378 * drawables depending on the layout direction.
11379 *
11380 * A call to the super method will be required from the subclasses implementation.
11381 *
11382 */
11383 protected void resolveDrawables() {
11384 // No need to resolve twice
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011385 if (mResolvedDrawables) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011386 return;
11387 }
11388 // No drawable to resolve
11389 if (mDrawables == null) {
11390 return;
11391 }
11392 // No relative drawable to resolve
11393 if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011394 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011395 return;
11396 }
11397
11398 Drawables dr = mDrawables;
11399 switch(getResolvedLayoutDirection()) {
11400 case LAYOUT_DIRECTION_RTL:
11401 if (dr.mDrawableStart != null) {
11402 dr.mDrawableRight = dr.mDrawableStart;
11403
11404 dr.mDrawableSizeRight = dr.mDrawableSizeStart;
11405 dr.mDrawableHeightRight = dr.mDrawableHeightStart;
11406 }
11407 if (dr.mDrawableEnd != null) {
11408 dr.mDrawableLeft = dr.mDrawableEnd;
11409
11410 dr.mDrawableSizeLeft = dr.mDrawableSizeEnd;
11411 dr.mDrawableHeightLeft = dr.mDrawableHeightEnd;
11412 }
11413 break;
11414
11415 case LAYOUT_DIRECTION_LTR:
11416 default:
11417 if (dr.mDrawableStart != null) {
11418 dr.mDrawableLeft = dr.mDrawableStart;
11419
11420 dr.mDrawableSizeLeft = dr.mDrawableSizeStart;
11421 dr.mDrawableHeightLeft = dr.mDrawableHeightStart;
11422 }
11423 if (dr.mDrawableEnd != null) {
11424 dr.mDrawableRight = dr.mDrawableEnd;
11425
11426 dr.mDrawableSizeRight = dr.mDrawableSizeEnd;
11427 dr.mDrawableHeightRight = dr.mDrawableHeightEnd;
11428 }
11429 break;
11430 }
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011431 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011432 }
11433
11434 protected void resetResolvedDrawables() {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011435 mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011436 }
11437
satoka67a3cf2011-09-07 17:14:03 +090011438 /**
11439 * @hide
11440 */
11441 protected void viewClicked(InputMethodManager imm) {
11442 if (imm != null) {
11443 imm.viewClicked(this);
11444 }
11445 }
11446
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011447 /**
11448 * Deletes the range of text [start, end[.
11449 * @hide
11450 */
11451 protected void deleteText_internal(int start, int end) {
11452 ((Editable) mText).delete(start, end);
11453 }
11454
11455 /**
11456 * Replaces the range of text [start, end[ by replacement text
11457 * @hide
11458 */
11459 protected void replaceText_internal(int start, int end, CharSequence text) {
11460 ((Editable) mText).replace(start, end, text);
11461 }
11462
Gilles Debunnee300be92011-12-06 10:15:56 -080011463 /**
11464 * Sets a span on the specified range of text
11465 * @hide
11466 */
11467 protected void setSpan_internal(Object span, int start, int end, int flags) {
11468 ((Editable) mText).setSpan(span, start, end, flags);
11469 }
11470
11471 /**
11472 * Moves the cursor to the specified offset position in text
11473 * @hide
11474 */
11475 protected void setCursorPosition_internal(int start, int end) {
11476 Selection.setSelection(((Editable) mText), start, end);
11477 }
11478
Romain Guy6d956622010-11-29 11:38:05 -080011479 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011480 private CharSequence mText;
11481 private CharSequence mTransformed;
11482 private BufferType mBufferType = BufferType.NORMAL;
11483
11484 private int mInputType = EditorInfo.TYPE_NULL;
11485 private CharSequence mHint;
11486 private Layout mHintLayout;
11487
11488 private KeyListener mInput;
11489
11490 private MovementMethod mMovement;
11491 private TransformationMethod mTransformation;
Adam Powell7f8f79a2011-07-07 18:35:54 -070011492 private boolean mAllowTransformationLengthChange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011493 private ChangeWatcher mChangeWatcher;
11494
11495 private ArrayList<TextWatcher> mListeners = null;
11496
11497 // display attributes
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011498 private final TextPaint mTextPaint;
Romain Guy939151f2009-04-08 14:22:40 -070011499 private boolean mUserSetTextScaleX;
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011500 private final Paint mHighlightPaint;
Gilles Debunne6fbe5ca2011-09-06 15:24:45 -070011501 private int mHighlightColor = 0x6633B5E5;
Gilles Debunnecdfab192011-11-28 18:05:03 -080011502 private Layout mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011503
11504 private long mShowCursor;
11505 private Blink mBlink;
11506 private boolean mCursorVisible = true;
11507
Adam Powell965b9692010-10-21 18:44:32 -070011508 // Cursor Controllers.
Gilles Debunne9948ad72010-11-24 14:00:46 -080011509 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunnee587d832010-11-23 20:20:11 -080011510 private SelectionModifierCursorController mSelectionModifierCursorController;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011511 private ActionMode mSelectionActionMode;
Adam Powell965b9692010-10-21 18:44:32 -070011512 private boolean mInsertionControllerEnabled;
11513 private boolean mSelectionControllerEnabled;
11514 private boolean mInBatchEditControllers;
11515
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011516 private boolean mSelectAllOnFocus = false;
11517
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -070011518 private int mGravity = Gravity.TOP | Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011519 private boolean mHorizontallyScrolling;
11520
11521 private int mAutoLinkMask;
11522 private boolean mLinksClickable = true;
11523
Gilles Debunne6435a562011-08-04 21:22:30 -070011524 private float mSpacingMult = 1.0f;
11525 private float mSpacingAdd = 0.0f;
Gilles Debunne86b9c782010-11-11 10:43:48 -080011526 private boolean mTextIsSelectable = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011527
11528 private static final int LINES = 1;
11529 private static final int EMS = LINES;
11530 private static final int PIXELS = 2;
11531
11532 private int mMaximum = Integer.MAX_VALUE;
11533 private int mMaxMode = LINES;
11534 private int mMinimum = 0;
11535 private int mMinMode = LINES;
11536
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -070011537 private int mOldMaximum = mMaximum;
11538 private int mOldMaxMode = mMaxMode;
11539
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011540 private int mMaxWidth = Integer.MAX_VALUE;
11541 private int mMaxWidthMode = PIXELS;
11542 private int mMinWidth = 0;
11543 private int mMinWidthMode = PIXELS;
11544
11545 private boolean mSingleLine;
11546 private int mDesiredHeightAtMeasure = -1;
11547 private boolean mIncludePad = true;
11548
11549 // tmp primitives, so we don't alloc them on each draw
11550 private Path mHighlightPath;
11551 private boolean mHighlightPathBogus = true;
11552 private static final RectF sTempRect = new RectF();
Gilles Debunne64901d42011-11-25 10:23:38 +010011553 private static final float[] sTmpPosition = new float[2];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011554
Jeff Brown033a0012011-11-11 15:30:16 -080011555 // XXX should be much larger
11556 private static final int VERY_WIDE = 1024*1024;
11557
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011558 private static final int BLINK = 500;
11559
11560 private static final int ANIMATED_SCROLL_GAP = 250;
11561 private long mLastScroll;
11562 private Scroller mScroller = null;
11563
11564 private BoringLayout.Metrics mBoring;
11565 private BoringLayout.Metrics mHintBoring;
11566
11567 private BoringLayout mSavedLayout, mSavedHintLayout;
11568
Doug Feltcb3791202011-07-07 11:57:48 -070011569 private TextDirectionHeuristic mTextDir = null;
11570
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011571 private static final InputFilter[] NO_FILTERS = new InputFilter[0];
11572 private InputFilter[] mFilters = NO_FILTERS;
11573 private static final Spanned EMPTY_SPANNED = new SpannedString("");
Christopher Tate36d4c3f2011-01-07 13:34:24 -080011574 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080011575 // System wide time for last cut or copy action.
11576 private static long sLastCutOrCopyTime;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080011577 // Used to highlight a word when it is corrected by the IME
11578 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunneda0a3f02010-12-22 10:07:15 -080011579 // New state used to change background based on whether this TextView is multiline.
11580 private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011581}