blob: d78a7a36e67cd4f319b9ee55a47a1b662f7c9bbf [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;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.XmlResourceParser;
30import android.graphics.Canvas;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080031import android.graphics.Color;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080032import android.graphics.Paint;
33import android.graphics.Path;
34import android.graphics.Rect;
35import android.graphics.RectF;
36import android.graphics.Typeface;
37import android.graphics.drawable.Drawable;
Gilles Debunne64e54a62010-09-07 19:07:17 -070038import android.inputmethodservice.ExtractEditText;
Dianne Hackborn23fdaf62010-08-06 12:16:55 -070039import android.net.Uri;
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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800104import android.view.ContextMenu;
Gilles Debunnef170a342010-11-11 11:08:59 -0800105import android.view.DragEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106import android.view.Gravity;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700107import android.view.HapticFeedbackConstants;
Jeff Brown6b53e8d2010-11-10 16:03:06 -0800108import android.view.KeyCharacterMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800109import android.view.KeyEvent;
110import android.view.LayoutInflater;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700111import android.view.Menu;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112import android.view.MenuItem;
113import android.view.MotionEvent;
114import android.view.View;
Gilles Debunne65f60412010-10-15 16:18:35 -0700115import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800116import android.view.ViewDebug;
Adam Powell8c8293b2010-10-12 14:45:12 -0700117import android.view.ViewGroup;
Gilles Debunne27113f82010-08-23 12:09:14 -0700118import android.view.ViewGroup.LayoutParams;
Adam Powellabcbb1a2010-10-04 21:12:19 -0700119import android.view.ViewParent;
Gilles Debunne3784a7f2011-07-15 13:49:38 -0700120import android.view.ViewRootImpl;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800121import android.view.ViewTreeObserver;
Gilles Debunnecbfbb522010-10-07 16:57:31 -0700122import android.view.WindowManager;
svetoslavganov75986cf2009-05-14 22:28:01 -0700123import android.view.accessibility.AccessibilityEvent;
124import android.view.accessibility.AccessibilityManager;
Svetoslav Ganov8643aa02011-04-20 12:12:33 -0700125import android.view.accessibility.AccessibilityNodeInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800126import android.view.animation.AnimationUtils;
127import android.view.inputmethod.BaseInputConnection;
128import android.view.inputmethod.CompletionInfo;
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -0800129import android.view.inputmethod.CorrectionInfo;
svetoslavganov75986cf2009-05-14 22:28:01 -0700130import android.view.inputmethod.EditorInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800131import android.view.inputmethod.ExtractedText;
132import android.view.inputmethod.ExtractedTextRequest;
133import android.view.inputmethod.InputConnection;
134import android.view.inputmethod.InputMethodManager;
Gilles Debunne0eea6682011-08-29 13:30:31 -0700135import android.widget.AdapterView.OnItemClickListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800136import android.widget.RemoteViews.RemoteView;
137
Gilles Debunne22378292011-08-12 10:38:52 -0700138import com.android.internal.util.FastMath;
139import com.android.internal.widget.EditableInputConnection;
140
141import org.xmlpull.v1.XmlPullParserException;
142
Gilles Debunne27113f82010-08-23 12:09:14 -0700143import java.io.IOException;
144import java.lang.ref.WeakReference;
Gilles Debunne214a8622011-04-26 15:44:37 -0700145import java.text.BreakIterator;
Gilles Debunne27113f82010-08-23 12:09:14 -0700146import java.util.ArrayList;
Luca Zanoline3f89c02011-08-01 09:55:17 +0100147import java.util.Arrays;
148import java.util.Comparator;
149import java.util.HashMap;
Gilles Debunne27113f82010-08-23 12:09:14 -0700150
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800151/**
152 * Displays text to the user and optionally allows them to edit it. A TextView
153 * is a complete text editor, however the basic class is configured to not
154 * allow editing; see {@link EditText} for a subclass that configures the text
155 * view for editing.
156 *
157 * <p>
158 * <b>XML attributes</b>
159 * <p>
160 * See {@link android.R.styleable#TextView TextView Attributes},
161 * {@link android.R.styleable#View View Attributes}
162 *
163 * @attr ref android.R.styleable#TextView_text
164 * @attr ref android.R.styleable#TextView_bufferType
165 * @attr ref android.R.styleable#TextView_hint
166 * @attr ref android.R.styleable#TextView_textColor
167 * @attr ref android.R.styleable#TextView_textColorHighlight
168 * @attr ref android.R.styleable#TextView_textColorHint
Romain Guyd6a463a2009-05-21 23:10:10 -0700169 * @attr ref android.R.styleable#TextView_textAppearance
170 * @attr ref android.R.styleable#TextView_textColorLink
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800171 * @attr ref android.R.styleable#TextView_textSize
172 * @attr ref android.R.styleable#TextView_textScaleX
173 * @attr ref android.R.styleable#TextView_typeface
174 * @attr ref android.R.styleable#TextView_textStyle
175 * @attr ref android.R.styleable#TextView_cursorVisible
176 * @attr ref android.R.styleable#TextView_maxLines
177 * @attr ref android.R.styleable#TextView_maxHeight
178 * @attr ref android.R.styleable#TextView_lines
179 * @attr ref android.R.styleable#TextView_height
180 * @attr ref android.R.styleable#TextView_minLines
181 * @attr ref android.R.styleable#TextView_minHeight
182 * @attr ref android.R.styleable#TextView_maxEms
183 * @attr ref android.R.styleable#TextView_maxWidth
184 * @attr ref android.R.styleable#TextView_ems
185 * @attr ref android.R.styleable#TextView_width
186 * @attr ref android.R.styleable#TextView_minEms
187 * @attr ref android.R.styleable#TextView_minWidth
188 * @attr ref android.R.styleable#TextView_gravity
189 * @attr ref android.R.styleable#TextView_scrollHorizontally
190 * @attr ref android.R.styleable#TextView_password
191 * @attr ref android.R.styleable#TextView_singleLine
192 * @attr ref android.R.styleable#TextView_selectAllOnFocus
193 * @attr ref android.R.styleable#TextView_includeFontPadding
194 * @attr ref android.R.styleable#TextView_maxLength
195 * @attr ref android.R.styleable#TextView_shadowColor
196 * @attr ref android.R.styleable#TextView_shadowDx
197 * @attr ref android.R.styleable#TextView_shadowDy
198 * @attr ref android.R.styleable#TextView_shadowRadius
199 * @attr ref android.R.styleable#TextView_autoLink
200 * @attr ref android.R.styleable#TextView_linksClickable
201 * @attr ref android.R.styleable#TextView_numeric
202 * @attr ref android.R.styleable#TextView_digits
203 * @attr ref android.R.styleable#TextView_phoneNumber
204 * @attr ref android.R.styleable#TextView_inputMethod
205 * @attr ref android.R.styleable#TextView_capitalize
206 * @attr ref android.R.styleable#TextView_autoText
207 * @attr ref android.R.styleable#TextView_editable
Romain Guyd6a463a2009-05-21 23:10:10 -0700208 * @attr ref android.R.styleable#TextView_freezesText
209 * @attr ref android.R.styleable#TextView_ellipsize
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800210 * @attr ref android.R.styleable#TextView_drawableTop
211 * @attr ref android.R.styleable#TextView_drawableBottom
212 * @attr ref android.R.styleable#TextView_drawableRight
213 * @attr ref android.R.styleable#TextView_drawableLeft
Romain Guyd6a463a2009-05-21 23:10:10 -0700214 * @attr ref android.R.styleable#TextView_drawablePadding
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 * @attr ref android.R.styleable#TextView_lineSpacingExtra
216 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
217 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
Romain Guyd6a463a2009-05-21 23:10:10 -0700218 * @attr ref android.R.styleable#TextView_inputType
219 * @attr ref android.R.styleable#TextView_imeOptions
220 * @attr ref android.R.styleable#TextView_privateImeOptions
221 * @attr ref android.R.styleable#TextView_imeActionLabel
222 * @attr ref android.R.styleable#TextView_imeActionId
223 * @attr ref android.R.styleable#TextView_editorExtras
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800224 */
225@RemoteView
226public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700227 static final String LOG_TAG = "TextView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800228 static final boolean DEBUG_EXTRACT = false;
Gilles Debunneb7012e842011-02-24 15:40:38 -0800229
Romain Guy8b55f372010-08-18 17:10:07 -0700230 private static final int PRIORITY = 100;
Gilles Debunne3dbf55c2010-12-16 10:31:51 -0800231 private int mCurrentAlpha = 255;
Adam Powell879fb6b2010-09-20 11:23:56 -0700232
Adam Powellabcbb1a2010-10-04 21:12:19 -0700233 final int[] mTempCoords = new int[2];
234 Rect mTempRect;
Adam Powell879fb6b2010-09-20 11:23:56 -0700235
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800236 private ColorStateList mTextColor;
237 private int mCurTextColor;
238 private ColorStateList mHintTextColor;
239 private ColorStateList mLinkTextColor;
240 private int mCurHintTextColor;
241 private boolean mFreezesText;
242 private boolean mFrozenWithFocus;
243 private boolean mTemporaryDetach;
Romain Guya440b002010-02-24 15:57:54 -0800244 private boolean mDispatchTemporaryDetach;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800245
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800246 private boolean mDiscardNextActionUp = false;
247 private boolean mIgnoreActionUpEvent = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800248
249 private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
250 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
251
252 private float mShadowRadius, mShadowDx, mShadowDy;
253
254 private static final int PREDRAW_NOT_REGISTERED = 0;
255 private static final int PREDRAW_PENDING = 1;
256 private static final int PREDRAW_DONE = 2;
257 private int mPreDrawState = PREDRAW_NOT_REGISTERED;
258
259 private TextUtils.TruncateAt mEllipsize = null;
260
261 // Enum for the "typeface" XML parameter.
262 // TODO: How can we get this from the XML instead of hardcoding it here?
263 private static final int SANS = 1;
264 private static final int SERIF = 2;
265 private static final int MONOSPACE = 3;
266
267 // Bitfield for the "numeric" XML parameter.
268 // TODO: How can we get this from the XML instead of hardcoding it here?
269 private static final int SIGNED = 2;
270 private static final int DECIMAL = 4;
271
272 class Drawables {
273 final Rect mCompoundRect = new Rect();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700274 Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight,
275 mDrawableStart, mDrawableEnd;
276 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
277 mDrawableSizeStart, mDrawableSizeEnd;
278 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
279 mDrawableHeightStart, mDrawableHeightEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800280 int mDrawablePadding;
281 }
282 private Drawables mDrawables;
283
284 private CharSequence mError;
285 private boolean mErrorWasChanged;
The Android Open Source Project10592532009-03-18 17:39:46 -0700286 private ErrorPopup mPopup;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 /**
288 * This flag is set if the TextView tries to display an error before it
289 * is attached to the window (so its position is still unknown).
290 * It causes the error to be shown later, when onAttachedToWindow()
291 * is called.
292 */
293 private boolean mShowErrorAfterAttach;
294
295 private CharWrapper mCharWrapper = null;
296
297 private boolean mSelectionMoved = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700298 private boolean mTouchFocusSelected = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800299
300 private Marquee mMarquee;
301 private boolean mRestartMarquee;
302
303 private int mMarqueeRepeatLimit = 3;
304
305 class InputContentType {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700306 int imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800307 String privateImeOptions;
308 CharSequence imeActionLabel;
309 int imeActionId;
310 Bundle extras;
311 OnEditorActionListener onEditorActionListener;
312 boolean enterDown;
313 }
314 InputContentType mInputContentType;
315
316 class InputMethodState {
317 Rect mCursorRectInWindow = new Rect();
318 RectF mTmpRectF = new RectF();
319 float[] mTmpOffset = new float[2];
320 ExtractedTextRequest mExtracting;
321 final ExtractedText mTmpExtracted = new ExtractedText();
322 int mBatchEditNesting;
323 boolean mCursorChanged;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700324 boolean mSelectionModeChanged;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800325 boolean mContentChanged;
326 int mChangedStart, mChangedEnd, mChangedDelta;
327 }
328 InputMethodState mInputMethodState;
329
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800330 private int mTextSelectHandleLeftRes;
331 private int mTextSelectHandleRightRes;
332 private int mTextSelectHandleRes;
Adam Powellfbb3b472010-10-06 21:04:35 -0700333
Gilles Debunne69340442011-03-31 13:37:51 -0700334 private int mTextEditSuggestionItemLayout;
335 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunne214a8622011-04-26 15:44:37 -0700336 private SuggestionRangeSpan mSuggestionRangeSpan;
Gilles Debunne69340442011-03-31 13:37:51 -0700337
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800338 private int mCursorDrawableRes;
339 private final Drawable[] mCursorDrawable = new Drawable[2];
340 private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
341
342 private Drawable mSelectHandleLeft;
343 private Drawable mSelectHandleRight;
344 private Drawable mSelectHandleCenter;
Adam Powellb08013c2010-09-16 16:28:11 -0700345
Gilles Debunne21078e42011-08-02 10:22:35 -0700346 // Global listener that detects changes in the global position of the TextView
347 private PositionListener mPositionListener;
348
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700349 private float mLastDownPositionX, mLastDownPositionY;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800350 private Callback mCustomSelectionActionModeCallback;
Gilles Debunne9948ad72010-11-24 14:00:46 -0800351
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800352 private final int mSquaredTouchSlopDistance;
Gilles Debunnec01f3fe2010-12-22 17:07:36 -0800353 // Set when this TextView gained focus with some text selected. Will start selection mode.
354 private boolean mCreatedWithASelection = false;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800355
Gilles Debunnec59269f2011-04-22 11:46:09 -0700356 private WordIterator mWordIterator;
357
Gilles Debunne6435a562011-08-04 21:22:30 -0700358 private SpellChecker mSpellChecker;
359
Doug Feltc0ccf0c2011-06-23 16:13:18 -0700360 // The alignment to pass to Layout, or null if not resolved.
361 private Layout.Alignment mLayoutAlignment;
362
363 // The default value for mTextAlign.
364 private TextAlign mTextAlign = TextAlign.INHERIT;
365
366 private static enum TextAlign {
367 INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END;
368 }
369
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -0700370 private boolean mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700371
Adam Powell282e3772011-08-30 16:51:11 -0700372 /**
373 * On some devices the fading edges add a performance penalty if used
374 * extensively in the same layout. This mode indicates how the marquee
375 * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
376 */
377 private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
378
379 /**
380 * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
381 * the layout that should be used when the mode switches.
382 */
383 private Layout mSavedMarqueeModeLayout;
384
385 /**
386 * Draw marquee text with fading edges as usual
387 */
388 private static final int MARQUEE_FADE_NORMAL = 0;
389
390 /**
391 * Draw marquee text as ellipsize end while inactive instead of with the fade.
392 * (Useful for devices where the fade can be expensive if overdone)
393 */
394 private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
395
396 /**
397 * Draw marquee text with fading edges because it is currently active/animating.
398 */
399 private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
400
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 /*
402 * Kick-start the font cache for the zygote process (to pay the cost of
403 * initializing freetype for our default font only once).
404 */
405 static {
406 Paint p = new Paint();
407 p.setAntiAlias(true);
408 // We don't care about the result, just the side-effect of measuring.
409 p.measureText("H");
410 }
411
412 /**
413 * Interface definition for a callback to be invoked when an action is
414 * performed on the editor.
415 */
416 public interface OnEditorActionListener {
417 /**
418 * Called when an action is being performed.
419 *
420 * @param v The view that was clicked.
421 * @param actionId Identifier of the action. This will be either the
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700422 * identifier you supplied, or {@link EditorInfo#IME_NULL
423 * EditorInfo.IME_NULL} if being called due to the enter key
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800424 * being pressed.
425 * @param event If triggered by an enter key, this is the event;
426 * otherwise, this is null.
427 * @return Return true if you have consumed the action, else false.
428 */
429 boolean onEditorAction(TextView v, int actionId, KeyEvent event);
430 }
Gilles Debunne21078e42011-08-02 10:22:35 -0700431
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800432 public TextView(Context context) {
433 this(context, null);
434 }
435
436 public TextView(Context context,
437 AttributeSet attrs) {
438 this(context, attrs, com.android.internal.R.attr.textViewStyle);
439 }
440
Gilles Debunnee15b3582010-06-16 15:17:21 -0700441 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800442 public TextView(Context context,
443 AttributeSet attrs,
444 int defStyle) {
445 super(context, attrs, defStyle);
446 mText = "";
447
448 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Eric Fischera9f1dd02009-08-12 15:00:10 -0700449 mTextPaint.density = getResources().getDisplayMetrics().density;
Dianne Hackbornafa78962009-09-28 17:33:54 -0700450 mTextPaint.setCompatibilityScaling(
451 getResources().getCompatibilityInfo().applicationScale);
Gilles Debunne8cbb4c62011-01-24 12:33:56 -0800452
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800453 // If we get the paint from the skin, we should set it to left, since
454 // the layout always wants it to be left.
455 // mTextPaint.setTextAlign(Paint.Align.LEFT);
456
457 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Dianne Hackbornafa78962009-09-28 17:33:54 -0700458 mHighlightPaint.setCompatibilityScaling(
459 getResources().getCompatibilityInfo().applicationScale);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800460
461 mMovement = getDefaultMovementMethod();
462 mTransformation = null;
463
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800464 int textColorHighlight = 0;
465 ColorStateList textColor = null;
466 ColorStateList textColorHint = null;
467 ColorStateList textColorLink = null;
468 int textSize = 15;
469 int typefaceIndex = -1;
470 int styleIndex = -1;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700471 boolean allCaps = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800472
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700473 final Resources.Theme theme = context.getTheme();
474
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800475 /*
476 * Look the appearance up without checking first if it exists because
477 * almost every TextView has one and it greatly simplifies the logic
478 * to be able to parse the appearance first and then let specific tags
479 * for this View override it.
480 */
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700481 TypedArray a = theme.obtainStyledAttributes(
482 attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800483 TypedArray appearance = null;
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700484 int ap = a.getResourceId(
485 com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
486 a.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800487 if (ap != -1) {
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700488 appearance = theme.obtainStyledAttributes(
489 ap, com.android.internal.R.styleable.TextAppearance);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800490 }
491 if (appearance != null) {
492 int n = appearance.getIndexCount();
493 for (int i = 0; i < n; i++) {
494 int attr = appearance.getIndex(i);
495
496 switch (attr) {
497 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
498 textColorHighlight = appearance.getColor(attr, textColorHighlight);
499 break;
500
501 case com.android.internal.R.styleable.TextAppearance_textColor:
502 textColor = appearance.getColorStateList(attr);
503 break;
504
505 case com.android.internal.R.styleable.TextAppearance_textColorHint:
506 textColorHint = appearance.getColorStateList(attr);
507 break;
508
509 case com.android.internal.R.styleable.TextAppearance_textColorLink:
510 textColorLink = appearance.getColorStateList(attr);
511 break;
512
513 case com.android.internal.R.styleable.TextAppearance_textSize:
514 textSize = appearance.getDimensionPixelSize(attr, textSize);
515 break;
516
517 case com.android.internal.R.styleable.TextAppearance_typeface:
518 typefaceIndex = appearance.getInt(attr, -1);
519 break;
520
521 case com.android.internal.R.styleable.TextAppearance_textStyle:
522 styleIndex = appearance.getInt(attr, -1);
523 break;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700524
525 case com.android.internal.R.styleable.TextAppearance_textAllCaps:
526 allCaps = appearance.getBoolean(attr, false);
527 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800528 }
529 }
530
531 appearance.recycle();
532 }
533
534 boolean editable = getDefaultEditable();
535 CharSequence inputMethod = null;
536 int numeric = 0;
537 CharSequence digits = null;
538 boolean phone = false;
539 boolean autotext = false;
540 int autocap = -1;
541 int buffertype = 0;
542 boolean selectallonfocus = false;
543 Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700544 drawableBottom = null, drawableStart = null, drawableEnd = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800545 int drawablePadding = 0;
546 int ellipsize = -1;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700547 boolean singleLine = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800548 int maxlength = -1;
549 CharSequence text = "";
Romain Guy4dc4f732009-06-19 15:16:40 -0700550 CharSequence hint = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800551 int shadowcolor = 0;
552 float dx = 0, dy = 0, r = 0;
553 boolean password = false;
554 int inputType = EditorInfo.TYPE_NULL;
555
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700556 a = theme.obtainStyledAttributes(
557 attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
558
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800559 int n = a.getIndexCount();
560 for (int i = 0; i < n; i++) {
561 int attr = a.getIndex(i);
562
563 switch (attr) {
564 case com.android.internal.R.styleable.TextView_editable:
565 editable = a.getBoolean(attr, editable);
566 break;
567
568 case com.android.internal.R.styleable.TextView_inputMethod:
569 inputMethod = a.getText(attr);
570 break;
571
572 case com.android.internal.R.styleable.TextView_numeric:
573 numeric = a.getInt(attr, numeric);
574 break;
575
576 case com.android.internal.R.styleable.TextView_digits:
577 digits = a.getText(attr);
578 break;
579
580 case com.android.internal.R.styleable.TextView_phoneNumber:
581 phone = a.getBoolean(attr, phone);
582 break;
583
584 case com.android.internal.R.styleable.TextView_autoText:
585 autotext = a.getBoolean(attr, autotext);
586 break;
587
588 case com.android.internal.R.styleable.TextView_capitalize:
589 autocap = a.getInt(attr, autocap);
590 break;
591
592 case com.android.internal.R.styleable.TextView_bufferType:
593 buffertype = a.getInt(attr, buffertype);
594 break;
595
596 case com.android.internal.R.styleable.TextView_selectAllOnFocus:
597 selectallonfocus = a.getBoolean(attr, selectallonfocus);
598 break;
599
600 case com.android.internal.R.styleable.TextView_autoLink:
601 mAutoLinkMask = a.getInt(attr, 0);
602 break;
603
604 case com.android.internal.R.styleable.TextView_linksClickable:
605 mLinksClickable = a.getBoolean(attr, true);
606 break;
607
608 case com.android.internal.R.styleable.TextView_drawableLeft:
609 drawableLeft = a.getDrawable(attr);
610 break;
611
612 case com.android.internal.R.styleable.TextView_drawableTop:
613 drawableTop = a.getDrawable(attr);
614 break;
615
616 case com.android.internal.R.styleable.TextView_drawableRight:
617 drawableRight = a.getDrawable(attr);
618 break;
619
620 case com.android.internal.R.styleable.TextView_drawableBottom:
621 drawableBottom = a.getDrawable(attr);
622 break;
623
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700624 case com.android.internal.R.styleable.TextView_drawableStart:
625 drawableStart = a.getDrawable(attr);
626 break;
627
628 case com.android.internal.R.styleable.TextView_drawableEnd:
629 drawableEnd = a.getDrawable(attr);
630 break;
631
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800632 case com.android.internal.R.styleable.TextView_drawablePadding:
633 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
634 break;
635
636 case com.android.internal.R.styleable.TextView_maxLines:
637 setMaxLines(a.getInt(attr, -1));
638 break;
639
640 case com.android.internal.R.styleable.TextView_maxHeight:
641 setMaxHeight(a.getDimensionPixelSize(attr, -1));
642 break;
643
644 case com.android.internal.R.styleable.TextView_lines:
645 setLines(a.getInt(attr, -1));
646 break;
647
648 case com.android.internal.R.styleable.TextView_height:
649 setHeight(a.getDimensionPixelSize(attr, -1));
650 break;
651
652 case com.android.internal.R.styleable.TextView_minLines:
653 setMinLines(a.getInt(attr, -1));
654 break;
655
656 case com.android.internal.R.styleable.TextView_minHeight:
657 setMinHeight(a.getDimensionPixelSize(attr, -1));
658 break;
659
660 case com.android.internal.R.styleable.TextView_maxEms:
661 setMaxEms(a.getInt(attr, -1));
662 break;
663
664 case com.android.internal.R.styleable.TextView_maxWidth:
665 setMaxWidth(a.getDimensionPixelSize(attr, -1));
666 break;
667
668 case com.android.internal.R.styleable.TextView_ems:
669 setEms(a.getInt(attr, -1));
670 break;
671
672 case com.android.internal.R.styleable.TextView_width:
673 setWidth(a.getDimensionPixelSize(attr, -1));
674 break;
675
676 case com.android.internal.R.styleable.TextView_minEms:
677 setMinEms(a.getInt(attr, -1));
678 break;
679
680 case com.android.internal.R.styleable.TextView_minWidth:
681 setMinWidth(a.getDimensionPixelSize(attr, -1));
682 break;
683
684 case com.android.internal.R.styleable.TextView_gravity:
685 setGravity(a.getInt(attr, -1));
686 break;
687
688 case com.android.internal.R.styleable.TextView_hint:
Romain Guy4dc4f732009-06-19 15:16:40 -0700689 hint = a.getText(attr);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800690 break;
691
692 case com.android.internal.R.styleable.TextView_text:
693 text = a.getText(attr);
694 break;
695
696 case com.android.internal.R.styleable.TextView_scrollHorizontally:
697 if (a.getBoolean(attr, false)) {
698 setHorizontallyScrolling(true);
699 }
700 break;
701
702 case com.android.internal.R.styleable.TextView_singleLine:
703 singleLine = a.getBoolean(attr, singleLine);
704 break;
705
706 case com.android.internal.R.styleable.TextView_ellipsize:
707 ellipsize = a.getInt(attr, ellipsize);
708 break;
709
710 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
711 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
712 break;
713
714 case com.android.internal.R.styleable.TextView_includeFontPadding:
715 if (!a.getBoolean(attr, true)) {
716 setIncludeFontPadding(false);
717 }
718 break;
719
720 case com.android.internal.R.styleable.TextView_cursorVisible:
721 if (!a.getBoolean(attr, true)) {
722 setCursorVisible(false);
723 }
724 break;
725
726 case com.android.internal.R.styleable.TextView_maxLength:
727 maxlength = a.getInt(attr, -1);
728 break;
729
730 case com.android.internal.R.styleable.TextView_textScaleX:
731 setTextScaleX(a.getFloat(attr, 1.0f));
732 break;
733
734 case com.android.internal.R.styleable.TextView_freezesText:
735 mFreezesText = a.getBoolean(attr, false);
736 break;
737
738 case com.android.internal.R.styleable.TextView_shadowColor:
739 shadowcolor = a.getInt(attr, 0);
740 break;
741
742 case com.android.internal.R.styleable.TextView_shadowDx:
743 dx = a.getFloat(attr, 0);
744 break;
745
746 case com.android.internal.R.styleable.TextView_shadowDy:
747 dy = a.getFloat(attr, 0);
748 break;
749
750 case com.android.internal.R.styleable.TextView_shadowRadius:
751 r = a.getFloat(attr, 0);
752 break;
753
754 case com.android.internal.R.styleable.TextView_enabled:
755 setEnabled(a.getBoolean(attr, isEnabled()));
756 break;
757
758 case com.android.internal.R.styleable.TextView_textColorHighlight:
759 textColorHighlight = a.getColor(attr, textColorHighlight);
760 break;
761
762 case com.android.internal.R.styleable.TextView_textColor:
763 textColor = a.getColorStateList(attr);
764 break;
765
766 case com.android.internal.R.styleable.TextView_textColorHint:
767 textColorHint = a.getColorStateList(attr);
768 break;
769
770 case com.android.internal.R.styleable.TextView_textColorLink:
771 textColorLink = a.getColorStateList(attr);
772 break;
773
774 case com.android.internal.R.styleable.TextView_textSize:
775 textSize = a.getDimensionPixelSize(attr, textSize);
776 break;
777
778 case com.android.internal.R.styleable.TextView_typeface:
779 typefaceIndex = a.getInt(attr, typefaceIndex);
780 break;
781
782 case com.android.internal.R.styleable.TextView_textStyle:
783 styleIndex = a.getInt(attr, styleIndex);
784 break;
785
786 case com.android.internal.R.styleable.TextView_password:
787 password = a.getBoolean(attr, password);
788 break;
789
790 case com.android.internal.R.styleable.TextView_lineSpacingExtra:
791 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
792 break;
793
794 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
795 mSpacingMult = a.getFloat(attr, mSpacingMult);
796 break;
797
798 case com.android.internal.R.styleable.TextView_inputType:
799 inputType = a.getInt(attr, mInputType);
800 break;
801
802 case com.android.internal.R.styleable.TextView_imeOptions:
803 if (mInputContentType == null) {
804 mInputContentType = new InputContentType();
805 }
806 mInputContentType.imeOptions = a.getInt(attr,
807 mInputContentType.imeOptions);
808 break;
809
810 case com.android.internal.R.styleable.TextView_imeActionLabel:
811 if (mInputContentType == null) {
812 mInputContentType = new InputContentType();
813 }
814 mInputContentType.imeActionLabel = a.getText(attr);
815 break;
816
817 case com.android.internal.R.styleable.TextView_imeActionId:
818 if (mInputContentType == null) {
819 mInputContentType = new InputContentType();
820 }
821 mInputContentType.imeActionId = a.getInt(attr,
822 mInputContentType.imeActionId);
823 break;
824
825 case com.android.internal.R.styleable.TextView_privateImeOptions:
826 setPrivateImeOptions(a.getString(attr));
827 break;
828
829 case com.android.internal.R.styleable.TextView_editorExtras:
830 try {
831 setInputExtras(a.getResourceId(attr, 0));
832 } catch (XmlPullParserException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700833 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800834 } catch (IOException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700835 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 }
837 break;
Adam Powellb08013c2010-09-16 16:28:11 -0700838
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800839 case com.android.internal.R.styleable.TextView_textCursorDrawable:
840 mCursorDrawableRes = a.getResourceId(attr, 0);
841 break;
842
Adam Powellb08013c2010-09-16 16:28:11 -0700843 case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
844 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
845 break;
846
847 case com.android.internal.R.styleable.TextView_textSelectHandleRight:
848 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
849 break;
850
851 case com.android.internal.R.styleable.TextView_textSelectHandle:
852 mTextSelectHandleRes = a.getResourceId(attr, 0);
853 break;
Gilles Debunne7b9652b2010-10-26 16:27:12 -0700854
Gilles Debunne69340442011-03-31 13:37:51 -0700855 case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
856 mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
857 break;
858
Gilles Debunne86b9c782010-11-11 10:43:48 -0800859 case com.android.internal.R.styleable.TextView_textIsSelectable:
860 mTextIsSelectable = a.getBoolean(attr, false);
861 break;
Gilles Debunnef3a135b2011-05-23 16:28:47 -0700862
Adam Powell7f8f79a2011-07-07 18:35:54 -0700863 case com.android.internal.R.styleable.TextView_textAllCaps:
864 allCaps = a.getBoolean(attr, false);
865 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800866 }
867 }
868 a.recycle();
869
870 BufferType bufferType = BufferType.EDITABLE;
871
Gilles Debunned7483bf2010-11-10 10:47:45 -0800872 final int variation =
873 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
874 final boolean passwordInputType = variation
875 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
876 final boolean webPasswordInputType = variation
877 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
Ken Wakasa82d731a2010-12-24 23:42:41 +0900878 final boolean numberPasswordInputType = variation
879 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Gilles Debunned7483bf2010-11-10 10:47:45 -0800880
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800881 if (inputMethod != null) {
Gilles Debunnee15b3582010-06-16 15:17:21 -0700882 Class<?> c;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800883
884 try {
885 c = Class.forName(inputMethod.toString());
886 } catch (ClassNotFoundException ex) {
887 throw new RuntimeException(ex);
888 }
889
890 try {
891 mInput = (KeyListener) c.newInstance();
892 } catch (InstantiationException ex) {
893 throw new RuntimeException(ex);
894 } catch (IllegalAccessException ex) {
895 throw new RuntimeException(ex);
896 }
897 try {
898 mInputType = inputType != EditorInfo.TYPE_NULL
899 ? inputType
900 : mInput.getInputType();
901 } catch (IncompatibleClassChangeError e) {
902 mInputType = EditorInfo.TYPE_CLASS_TEXT;
903 }
904 } else if (digits != null) {
905 mInput = DigitsKeyListener.getInstance(digits.toString());
Dianne Hackborn7ed6ee52009-09-10 18:41:28 -0700906 // If no input type was specified, we will default to generic
907 // text, since we can't tell the IME about the set of digits
908 // that was selected.
909 mInputType = inputType != EditorInfo.TYPE_NULL
910 ? inputType : EditorInfo.TYPE_CLASS_TEXT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800911 } else if (inputType != EditorInfo.TYPE_NULL) {
912 setInputType(inputType, true);
Gilles Debunne91a08cf2010-11-08 17:34:49 -0800913 // If set, the input type overrides what was set using the deprecated singleLine flag.
914 singleLine = !isMultilineInputType(inputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800915 } else if (phone) {
916 mInput = DialerKeyListener.getInstance();
Dianne Hackborn49a1a9b52009-03-24 20:06:11 -0700917 mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800918 } else if (numeric != 0) {
919 mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
920 (numeric & DECIMAL) != 0);
921 inputType = EditorInfo.TYPE_CLASS_NUMBER;
922 if ((numeric & SIGNED) != 0) {
923 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
924 }
925 if ((numeric & DECIMAL) != 0) {
926 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
927 }
928 mInputType = inputType;
929 } else if (autotext || autocap != -1) {
930 TextKeyListener.Capitalize cap;
931
932 inputType = EditorInfo.TYPE_CLASS_TEXT;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700933
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800934 switch (autocap) {
935 case 1:
936 cap = TextKeyListener.Capitalize.SENTENCES;
937 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
938 break;
939
940 case 2:
941 cap = TextKeyListener.Capitalize.WORDS;
942 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
943 break;
944
945 case 3:
946 cap = TextKeyListener.Capitalize.CHARACTERS;
947 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
948 break;
949
950 default:
951 cap = TextKeyListener.Capitalize.NONE;
952 break;
953 }
954
955 mInput = TextKeyListener.getInstance(autotext, cap);
956 mInputType = inputType;
Gilles Debunne86b9c782010-11-11 10:43:48 -0800957 } else if (mTextIsSelectable) {
958 // Prevent text changes from keyboard.
959 mInputType = EditorInfo.TYPE_NULL;
960 mInput = null;
961 bufferType = BufferType.SPANNABLE;
Gilles Debunnecbcb3452010-12-17 15:31:02 -0800962 // Required to request focus while in touch mode.
963 setFocusableInTouchMode(true);
Gilles Debunne86b9c782010-11-11 10:43:48 -0800964 // So that selection can be changed using arrow keys and touch is handled.
965 setMovementMethod(ArrowKeyMovementMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800966 } else if (editable) {
967 mInput = TextKeyListener.getInstance();
968 mInputType = EditorInfo.TYPE_CLASS_TEXT;
969 } else {
970 mInput = null;
971
972 switch (buffertype) {
973 case 0:
974 bufferType = BufferType.NORMAL;
975 break;
976 case 1:
977 bufferType = BufferType.SPANNABLE;
978 break;
979 case 2:
980 bufferType = BufferType.EDITABLE;
981 break;
982 }
983 }
984
Gilles Debunned7483bf2010-11-10 10:47:45 -0800985 // mInputType has been set from inputType, possibly modified by mInputMethod.
986 // Specialize mInputType to [web]password if we have a text class and the original input
987 // type was a password.
988 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
989 if (password || passwordInputType) {
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -0400990 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
Gilles Debunned7483bf2010-11-10 10:47:45 -0800991 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -0400992 }
Gilles Debunned7483bf2010-11-10 10:47:45 -0800993 if (webPasswordInputType) {
994 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
995 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
996 }
Ken Wakasa82d731a2010-12-24 23:42:41 +0900997 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
998 if (numberPasswordInputType) {
999 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1000 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
1001 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001002 }
1003
1004 if (selectallonfocus) {
1005 mSelectAllOnFocus = true;
1006
1007 if (bufferType == BufferType.NORMAL)
1008 bufferType = BufferType.SPANNABLE;
1009 }
1010
1011 setCompoundDrawablesWithIntrinsicBounds(
1012 drawableLeft, drawableTop, drawableRight, drawableBottom);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001013 setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001014 setCompoundDrawablePadding(drawablePadding);
1015
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08001016 // Same as setSingleLine(), but make sure the transformation method and the maximum number
Gilles Debunne066460f2010-12-15 17:31:51 -08001017 // of lines of height are unchanged for multi-line TextViews.
Gilles Debunned7483bf2010-11-10 10:47:45 -08001018 setInputTypeSingleLine(singleLine);
Gilles Debunne066460f2010-12-15 17:31:51 -08001019 applySingleLine(singleLine, singleLine, singleLine);
Gilles Debunned7483bf2010-11-10 10:47:45 -08001020
Gilles Debunne91a08cf2010-11-08 17:34:49 -08001021 if (singleLine && mInput == null && ellipsize < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001022 ellipsize = 3; // END
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001023 }
1024
1025 switch (ellipsize) {
1026 case 1:
1027 setEllipsize(TextUtils.TruncateAt.START);
1028 break;
1029 case 2:
1030 setEllipsize(TextUtils.TruncateAt.MIDDLE);
1031 break;
1032 case 3:
1033 setEllipsize(TextUtils.TruncateAt.END);
1034 break;
1035 case 4:
Adam Powell282e3772011-08-30 16:51:11 -07001036 if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
1037 setHorizontalFadingEdgeEnabled(true);
1038 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
1039 } else {
1040 setHorizontalFadingEdgeEnabled(false);
1041 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
1042 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001043 setEllipsize(TextUtils.TruncateAt.MARQUEE);
1044 break;
1045 }
1046
1047 setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
1048 setHintTextColor(textColorHint);
1049 setLinkTextColor(textColorLink);
1050 if (textColorHighlight != 0) {
1051 setHighlightColor(textColorHighlight);
1052 }
1053 setRawTextSize(textSize);
1054
Adam Powell7f8f79a2011-07-07 18:35:54 -07001055 if (allCaps) {
1056 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
1057 }
1058
Ken Wakasa82d731a2010-12-24 23:42:41 +09001059 if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001060 setTransformationMethod(PasswordTransformationMethod.getInstance());
1061 typefaceIndex = MONOSPACE;
Gilles Debunned7483bf2010-11-10 10:47:45 -08001062 } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1063 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08001064 typefaceIndex = MONOSPACE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001065 }
1066
1067 setTypefaceByIndex(typefaceIndex, styleIndex);
1068
1069 if (shadowcolor != 0) {
1070 setShadowLayer(r, dx, dy, shadowcolor);
1071 }
1072
1073 if (maxlength >= 0) {
1074 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1075 } else {
1076 setFilters(NO_FILTERS);
1077 }
1078
1079 setText(text, bufferType);
Romain Guy4dc4f732009-06-19 15:16:40 -07001080 if (hint != null) setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001081
1082 /*
1083 * Views are not normally focusable unless specified to be.
1084 * However, TextViews that have input or movement methods *are*
1085 * focusable by default.
1086 */
1087 a = context.obtainStyledAttributes(attrs,
1088 com.android.internal.R.styleable.View,
1089 defStyle, 0);
1090
1091 boolean focusable = mMovement != null || mInput != null;
1092 boolean clickable = focusable;
1093 boolean longClickable = focusable;
1094
1095 n = a.getIndexCount();
1096 for (int i = 0; i < n; i++) {
1097 int attr = a.getIndex(i);
1098
1099 switch (attr) {
1100 case com.android.internal.R.styleable.View_focusable:
1101 focusable = a.getBoolean(attr, focusable);
1102 break;
1103
1104 case com.android.internal.R.styleable.View_clickable:
1105 clickable = a.getBoolean(attr, clickable);
1106 break;
1107
1108 case com.android.internal.R.styleable.View_longClickable:
1109 longClickable = a.getBoolean(attr, longClickable);
1110 break;
1111 }
1112 }
1113 a.recycle();
1114
1115 setFocusable(focusable);
1116 setClickable(clickable);
1117 setLongClickable(longClickable);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07001118
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001119 prepareCursorControllers();
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08001120
1121 final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1122 final int touchSlop = viewConfiguration.getScaledTouchSlop();
1123 mSquaredTouchSlopDistance = touchSlop * touchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001124 }
1125
1126 private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1127 Typeface tf = null;
1128 switch (typefaceIndex) {
1129 case SANS:
1130 tf = Typeface.SANS_SERIF;
1131 break;
1132
1133 case SERIF:
1134 tf = Typeface.SERIF;
1135 break;
1136
1137 case MONOSPACE:
1138 tf = Typeface.MONOSPACE;
1139 break;
1140 }
1141
1142 setTypeface(tf, styleIndex);
1143 }
1144
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001145 private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
1146 boolean hasRelativeDrawables = (start != null) || (end != null);
1147 if (hasRelativeDrawables) {
1148 Drawables dr = mDrawables;
1149 if (dr == null) {
1150 mDrawables = dr = new Drawables();
1151 }
1152 final Rect compoundRect = dr.mCompoundRect;
1153 int[] state = getDrawableState();
1154 if (start != null) {
1155 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1156 start.setState(state);
1157 start.copyBounds(compoundRect);
1158 start.setCallback(this);
1159
1160 dr.mDrawableStart = start;
1161 dr.mDrawableSizeStart = compoundRect.width();
1162 dr.mDrawableHeightStart = compoundRect.height();
1163 } else {
1164 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1165 }
1166 if (end != null) {
1167 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1168 end.setState(state);
1169 end.copyBounds(compoundRect);
1170 end.setCallback(this);
1171
1172 dr.mDrawableEnd = end;
1173 dr.mDrawableSizeEnd = compoundRect.width();
1174 dr.mDrawableHeightEnd = compoundRect.height();
1175 } else {
1176 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1177 }
1178 }
1179 }
1180
Janos Levai042856c2010-10-15 02:53:58 +03001181 @Override
1182 public void setEnabled(boolean enabled) {
1183 if (enabled == isEnabled()) {
1184 return;
1185 }
1186
1187 if (!enabled) {
1188 // Hide the soft input if the currently active TextView is disabled
1189 InputMethodManager imm = InputMethodManager.peekInstance();
1190 if (imm != null && imm.isActive(this)) {
1191 imm.hideSoftInputFromWindow(getWindowToken(), 0);
1192 }
1193 }
1194 super.setEnabled(enabled);
Gilles Debunnebb588da2011-07-11 18:26:19 -07001195 prepareCursorControllers();
Janos Levai042856c2010-10-15 02:53:58 +03001196 }
1197
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001198 /**
1199 * Sets the typeface and style in which the text should be displayed,
1200 * and turns on the fake bold and italic bits in the Paint if the
1201 * Typeface that you provided does not have all the bits in the
1202 * style that you specified.
1203 *
1204 * @attr ref android.R.styleable#TextView_typeface
1205 * @attr ref android.R.styleable#TextView_textStyle
1206 */
1207 public void setTypeface(Typeface tf, int style) {
1208 if (style > 0) {
1209 if (tf == null) {
1210 tf = Typeface.defaultFromStyle(style);
1211 } else {
1212 tf = Typeface.create(tf, style);
1213 }
1214
1215 setTypeface(tf);
1216 // now compute what (if any) algorithmic styling is needed
1217 int typefaceStyle = tf != null ? tf.getStyle() : 0;
1218 int need = style & ~typefaceStyle;
1219 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1220 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1221 } else {
1222 mTextPaint.setFakeBoldText(false);
1223 mTextPaint.setTextSkewX(0);
1224 setTypeface(tf);
1225 }
1226 }
1227
1228 /**
1229 * Subclasses override this to specify that they have a KeyListener
1230 * by default even if not specifically called for in the XML options.
1231 */
1232 protected boolean getDefaultEditable() {
1233 return false;
1234 }
1235
1236 /**
1237 * Subclasses override this to specify a default movement method.
1238 */
1239 protected MovementMethod getDefaultMovementMethod() {
1240 return null;
1241 }
1242
1243 /**
1244 * Return the text the TextView is displaying. If setText() was called with
1245 * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1246 * the return value from this method to Spannable or Editable, respectively.
1247 *
1248 * Note: The content of the return value should not be modified. If you want
1249 * a modifiable one, you should make your own copy first.
1250 */
1251 @ViewDebug.CapturedViewProperty
1252 public CharSequence getText() {
1253 return mText;
1254 }
1255
1256 /**
1257 * Returns the length, in characters, of the text managed by this TextView
1258 */
1259 public int length() {
1260 return mText.length();
1261 }
1262
1263 /**
1264 * Return the text the TextView is displaying as an Editable object. If
1265 * the text is not editable, null is returned.
1266 *
1267 * @see #getText
1268 */
1269 public Editable getEditableText() {
1270 return (mText instanceof Editable) ? (Editable)mText : null;
1271 }
1272
1273 /**
1274 * @return the height of one standard line in pixels. Note that markup
1275 * within the text can cause individual lines to be taller or shorter
1276 * than this height, and the layout may contain additional first-
1277 * or last-line padding.
1278 */
1279 public int getLineHeight() {
Gilles Debunne96e6b8b2010-12-14 13:43:45 -08001280 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001281 }
1282
1283 /**
1284 * @return the Layout that is currently being used to display the text.
1285 * This can be null if the text or width has recently changes.
1286 */
1287 public final Layout getLayout() {
1288 return mLayout;
1289 }
1290
1291 /**
1292 * @return the current key listener for this TextView.
1293 * This will frequently be null for non-EditText TextViews.
1294 */
1295 public final KeyListener getKeyListener() {
1296 return mInput;
1297 }
1298
1299 /**
1300 * Sets the key listener to be used with this TextView. This can be null
1301 * to disallow user input. Note that this method has significant and
1302 * subtle interactions with soft keyboards and other input method:
1303 * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1304 * for important details. Calling this method will replace the current
1305 * content type of the text view with the content type returned by the
1306 * key listener.
1307 * <p>
1308 * Be warned that if you want a TextView with a key listener or movement
1309 * method not to be focusable, or if you want a TextView without a
1310 * key listener or movement method to be focusable, you must call
1311 * {@link #setFocusable} again after calling this to get the focusability
1312 * back the way you want it.
1313 *
1314 * @attr ref android.R.styleable#TextView_numeric
1315 * @attr ref android.R.styleable#TextView_digits
1316 * @attr ref android.R.styleable#TextView_phoneNumber
1317 * @attr ref android.R.styleable#TextView_inputMethod
1318 * @attr ref android.R.styleable#TextView_capitalize
1319 * @attr ref android.R.styleable#TextView_autoText
1320 */
1321 public void setKeyListener(KeyListener input) {
1322 setKeyListenerOnly(input);
1323 fixFocusableAndClickableSettings();
1324
1325 if (input != null) {
1326 try {
1327 mInputType = mInput.getInputType();
1328 } catch (IncompatibleClassChangeError e) {
1329 mInputType = EditorInfo.TYPE_CLASS_TEXT;
1330 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001331 // Change inputType, without affecting transformation.
1332 // No need to applySingleLine since mSingleLine is unchanged.
1333 setInputTypeSingleLine(mSingleLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001334 } else {
1335 mInputType = EditorInfo.TYPE_NULL;
1336 }
1337
1338 InputMethodManager imm = InputMethodManager.peekInstance();
1339 if (imm != null) imm.restartInput(this);
1340 }
1341
1342 private void setKeyListenerOnly(KeyListener input) {
1343 mInput = input;
1344 if (mInput != null && !(mText instanceof Editable))
1345 setText(mText);
1346
1347 setFilters((Editable) mText, mFilters);
1348 }
1349
1350 /**
1351 * @return the movement method being used for this TextView.
1352 * This will frequently be null for non-EditText TextViews.
1353 */
1354 public final MovementMethod getMovementMethod() {
1355 return mMovement;
1356 }
1357
1358 /**
1359 * Sets the movement method (arrow key handler) to be used for
1360 * this TextView. This can be null to disallow using the arrow keys
1361 * to move the cursor or scroll the view.
1362 * <p>
1363 * Be warned that if you want a TextView with a key listener or movement
1364 * method not to be focusable, or if you want a TextView without a
1365 * key listener or movement method to be focusable, you must call
1366 * {@link #setFocusable} again after calling this to get the focusability
1367 * back the way you want it.
1368 */
1369 public final void setMovementMethod(MovementMethod movement) {
1370 mMovement = movement;
1371
1372 if (mMovement != null && !(mText instanceof Spannable))
1373 setText(mText);
1374
1375 fixFocusableAndClickableSettings();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001376
Gilles Debunnebaaace52010-10-01 15:47:13 -07001377 // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001378 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001379 }
1380
1381 private void fixFocusableAndClickableSettings() {
1382 if ((mMovement != null) || mInput != null) {
1383 setFocusable(true);
1384 setClickable(true);
1385 setLongClickable(true);
1386 } else {
1387 setFocusable(false);
1388 setClickable(false);
1389 setLongClickable(false);
1390 }
1391 }
1392
1393 /**
1394 * @return the current transformation method for this TextView.
1395 * This will frequently be null except for single-line and password
1396 * fields.
1397 */
1398 public final TransformationMethod getTransformationMethod() {
1399 return mTransformation;
1400 }
1401
1402 /**
1403 * Sets the transformation that is applied to the text that this
1404 * TextView is displaying.
1405 *
1406 * @attr ref android.R.styleable#TextView_password
1407 * @attr ref android.R.styleable#TextView_singleLine
1408 */
1409 public final void setTransformationMethod(TransformationMethod method) {
1410 if (method == mTransformation) {
1411 // Avoid the setText() below if the transformation is
1412 // the same.
1413 return;
1414 }
1415 if (mTransformation != null) {
1416 if (mText instanceof Spannable) {
1417 ((Spannable) mText).removeSpan(mTransformation);
1418 }
1419 }
1420
1421 mTransformation = method;
1422
Adam Powell7f8f79a2011-07-07 18:35:54 -07001423 if (method instanceof TransformationMethod2) {
1424 TransformationMethod2 method2 = (TransformationMethod2) method;
1425 mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable);
1426 method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
1427 } else {
1428 mAllowTransformationLengthChange = false;
1429 }
1430
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001431 setText(mText);
1432 }
1433
1434 /**
1435 * Returns the top padding of the view, plus space for the top
1436 * Drawable if any.
1437 */
1438 public int getCompoundPaddingTop() {
1439 final Drawables dr = mDrawables;
1440 if (dr == null || dr.mDrawableTop == null) {
1441 return mPaddingTop;
1442 } else {
1443 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1444 }
1445 }
1446
1447 /**
1448 * Returns the bottom padding of the view, plus space for the bottom
1449 * Drawable if any.
1450 */
1451 public int getCompoundPaddingBottom() {
1452 final Drawables dr = mDrawables;
1453 if (dr == null || dr.mDrawableBottom == null) {
1454 return mPaddingBottom;
1455 } else {
1456 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1457 }
1458 }
1459
1460 /**
1461 * Returns the left padding of the view, plus space for the left
1462 * Drawable if any.
1463 */
1464 public int getCompoundPaddingLeft() {
1465 final Drawables dr = mDrawables;
1466 if (dr == null || dr.mDrawableLeft == null) {
1467 return mPaddingLeft;
1468 } else {
1469 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1470 }
1471 }
1472
1473 /**
1474 * Returns the right padding of the view, plus space for the right
1475 * Drawable if any.
1476 */
1477 public int getCompoundPaddingRight() {
1478 final Drawables dr = mDrawables;
1479 if (dr == null || dr.mDrawableRight == null) {
1480 return mPaddingRight;
1481 } else {
1482 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1483 }
1484 }
1485
1486 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001487 * Returns the start padding of the view, plus space for the start
1488 * Drawable if any.
1489 *
1490 * @hide
1491 */
1492 public int getCompoundPaddingStart() {
1493 resolveDrawables();
1494 switch(getResolvedLayoutDirection()) {
1495 default:
1496 case LAYOUT_DIRECTION_LTR:
1497 return getCompoundPaddingLeft();
1498 case LAYOUT_DIRECTION_RTL:
1499 return getCompoundPaddingRight();
1500 }
1501 }
1502
1503 /**
1504 * Returns the end padding of the view, plus space for the end
1505 * Drawable if any.
1506 *
1507 * @hide
1508 */
1509 public int getCompoundPaddingEnd() {
1510 resolveDrawables();
1511 switch(getResolvedLayoutDirection()) {
1512 default:
1513 case LAYOUT_DIRECTION_LTR:
1514 return getCompoundPaddingRight();
1515 case LAYOUT_DIRECTION_RTL:
1516 return getCompoundPaddingLeft();
1517 }
1518 }
1519
1520 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001521 * Returns the extended top padding of the view, including both the
1522 * top Drawable if any and any extra space to keep more than maxLines
1523 * of text from showing. It is only valid to call this after measuring.
1524 */
1525 public int getExtendedPaddingTop() {
1526 if (mMaxMode != LINES) {
1527 return getCompoundPaddingTop();
1528 }
1529
1530 if (mLayout.getLineCount() <= mMaximum) {
1531 return getCompoundPaddingTop();
1532 }
1533
1534 int top = getCompoundPaddingTop();
1535 int bottom = getCompoundPaddingBottom();
1536 int viewht = getHeight() - top - bottom;
1537 int layoutht = mLayout.getLineTop(mMaximum);
1538
1539 if (layoutht >= viewht) {
1540 return top;
1541 }
1542
1543 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1544 if (gravity == Gravity.TOP) {
1545 return top;
1546 } else if (gravity == Gravity.BOTTOM) {
1547 return top + viewht - layoutht;
1548 } else { // (gravity == Gravity.CENTER_VERTICAL)
1549 return top + (viewht - layoutht) / 2;
1550 }
1551 }
1552
1553 /**
1554 * Returns the extended bottom padding of the view, including both the
1555 * bottom Drawable if any and any extra space to keep more than maxLines
1556 * of text from showing. It is only valid to call this after measuring.
1557 */
1558 public int getExtendedPaddingBottom() {
1559 if (mMaxMode != LINES) {
1560 return getCompoundPaddingBottom();
1561 }
1562
1563 if (mLayout.getLineCount() <= mMaximum) {
1564 return getCompoundPaddingBottom();
1565 }
1566
1567 int top = getCompoundPaddingTop();
1568 int bottom = getCompoundPaddingBottom();
1569 int viewht = getHeight() - top - bottom;
1570 int layoutht = mLayout.getLineTop(mMaximum);
1571
1572 if (layoutht >= viewht) {
1573 return bottom;
1574 }
1575
1576 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1577 if (gravity == Gravity.TOP) {
1578 return bottom + viewht - layoutht;
1579 } else if (gravity == Gravity.BOTTOM) {
1580 return bottom;
1581 } else { // (gravity == Gravity.CENTER_VERTICAL)
1582 return bottom + (viewht - layoutht) / 2;
1583 }
1584 }
1585
1586 /**
1587 * Returns the total left padding of the view, including the left
1588 * Drawable if any.
1589 */
1590 public int getTotalPaddingLeft() {
1591 return getCompoundPaddingLeft();
1592 }
1593
1594 /**
1595 * Returns the total right padding of the view, including the right
1596 * Drawable if any.
1597 */
1598 public int getTotalPaddingRight() {
1599 return getCompoundPaddingRight();
1600 }
1601
1602 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001603 * Returns the total start padding of the view, including the start
1604 * Drawable if any.
1605 *
1606 * @hide
1607 */
1608 public int getTotalPaddingStart() {
1609 return getCompoundPaddingStart();
1610 }
1611
1612 /**
1613 * Returns the total end padding of the view, including the end
1614 * Drawable if any.
1615 *
1616 * @hide
1617 */
1618 public int getTotalPaddingEnd() {
1619 return getCompoundPaddingEnd();
1620 }
1621
1622 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001623 * Returns the total top padding of the view, including the top
1624 * Drawable if any, the extra space to keep more than maxLines
1625 * from showing, and the vertical offset for gravity, if any.
1626 */
1627 public int getTotalPaddingTop() {
1628 return getExtendedPaddingTop() + getVerticalOffset(true);
1629 }
1630
1631 /**
1632 * Returns the total bottom padding of the view, including the bottom
1633 * Drawable if any, the extra space to keep more than maxLines
1634 * from showing, and the vertical offset for gravity, if any.
1635 */
1636 public int getTotalPaddingBottom() {
1637 return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1638 }
1639
1640 /**
1641 * Sets the Drawables (if any) to appear to the left of, above,
1642 * to the right of, and below the text. Use null if you do not
1643 * want a Drawable there. The Drawables must already have had
1644 * {@link Drawable#setBounds} called.
1645 *
1646 * @attr ref android.R.styleable#TextView_drawableLeft
1647 * @attr ref android.R.styleable#TextView_drawableTop
1648 * @attr ref android.R.styleable#TextView_drawableRight
1649 * @attr ref android.R.styleable#TextView_drawableBottom
1650 */
1651 public void setCompoundDrawables(Drawable left, Drawable top,
1652 Drawable right, Drawable bottom) {
1653 Drawables dr = mDrawables;
1654
1655 final boolean drawables = left != null || top != null
1656 || right != null || bottom != null;
1657
1658 if (!drawables) {
1659 // Clearing drawables... can we free the data structure?
1660 if (dr != null) {
1661 if (dr.mDrawablePadding == 0) {
1662 mDrawables = null;
1663 } else {
1664 // We need to retain the last set padding, so just clear
1665 // out all of the fields in the existing structure.
Romain Guy48540eb2009-05-19 16:44:57 -07001666 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001667 dr.mDrawableLeft = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001668 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001669 dr.mDrawableTop = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001670 if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001671 dr.mDrawableRight = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001672 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001673 dr.mDrawableBottom = null;
1674 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1675 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1676 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1677 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1678 }
1679 }
1680 } else {
1681 if (dr == null) {
1682 mDrawables = dr = new Drawables();
1683 }
1684
Romain Guy48540eb2009-05-19 16:44:57 -07001685 if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1686 dr.mDrawableLeft.setCallback(null);
1687 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001688 dr.mDrawableLeft = left;
Romain Guy8e618e52010-03-08 12:18:20 -08001689
1690 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001691 dr.mDrawableTop.setCallback(null);
1692 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001693 dr.mDrawableTop = top;
Romain Guy8e618e52010-03-08 12:18:20 -08001694
1695 if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001696 dr.mDrawableRight.setCallback(null);
1697 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001698 dr.mDrawableRight = right;
Romain Guy8e618e52010-03-08 12:18:20 -08001699
1700 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001701 dr.mDrawableBottom.setCallback(null);
1702 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001703 dr.mDrawableBottom = bottom;
1704
1705 final Rect compoundRect = dr.mCompoundRect;
Romain Guy48540eb2009-05-19 16:44:57 -07001706 int[] state;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001707
1708 state = getDrawableState();
1709
1710 if (left != null) {
1711 left.setState(state);
1712 left.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001713 left.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001714 dr.mDrawableSizeLeft = compoundRect.width();
1715 dr.mDrawableHeightLeft = compoundRect.height();
1716 } else {
1717 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1718 }
1719
1720 if (right != null) {
1721 right.setState(state);
1722 right.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001723 right.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001724 dr.mDrawableSizeRight = compoundRect.width();
1725 dr.mDrawableHeightRight = compoundRect.height();
1726 } else {
1727 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1728 }
1729
1730 if (top != null) {
1731 top.setState(state);
1732 top.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001733 top.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001734 dr.mDrawableSizeTop = compoundRect.height();
1735 dr.mDrawableWidthTop = compoundRect.width();
1736 } else {
1737 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1738 }
1739
1740 if (bottom != null) {
1741 bottom.setState(state);
1742 bottom.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001743 bottom.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001744 dr.mDrawableSizeBottom = compoundRect.height();
1745 dr.mDrawableWidthBottom = compoundRect.width();
1746 } else {
1747 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1748 }
1749 }
1750
1751 invalidate();
1752 requestLayout();
1753 }
1754
1755 /**
1756 * Sets the Drawables (if any) to appear to the left of, above,
1757 * to the right of, and below the text. Use 0 if you do not
1758 * want a Drawable there. The Drawables' bounds will be set to
1759 * their intrinsic bounds.
1760 *
1761 * @param left Resource identifier of the left Drawable.
1762 * @param top Resource identifier of the top Drawable.
1763 * @param right Resource identifier of the right Drawable.
1764 * @param bottom Resource identifier of the bottom Drawable.
1765 *
1766 * @attr ref android.R.styleable#TextView_drawableLeft
1767 * @attr ref android.R.styleable#TextView_drawableTop
1768 * @attr ref android.R.styleable#TextView_drawableRight
1769 * @attr ref android.R.styleable#TextView_drawableBottom
1770 */
1771 public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1772 final Resources resources = getContext().getResources();
1773 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1774 top != 0 ? resources.getDrawable(top) : null,
1775 right != 0 ? resources.getDrawable(right) : null,
1776 bottom != 0 ? resources.getDrawable(bottom) : null);
1777 }
1778
1779 /**
1780 * Sets the Drawables (if any) to appear to the left of, above,
1781 * to the right of, and below the text. Use null if you do not
1782 * want a Drawable there. The Drawables' bounds will be set to
1783 * their intrinsic bounds.
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(Drawable left, Drawable top,
1791 Drawable right, Drawable bottom) {
1792
1793 if (left != null) {
1794 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1795 }
1796 if (right != null) {
1797 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1798 }
1799 if (top != null) {
1800 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1801 }
1802 if (bottom != null) {
1803 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1804 }
1805 setCompoundDrawables(left, top, right, bottom);
1806 }
1807
1808 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001809 * Sets the Drawables (if any) to appear to the start of, above,
1810 * to the end of, and below the text. Use null if you do not
1811 * want a Drawable there. The Drawables must already have had
1812 * {@link Drawable#setBounds} called.
1813 *
1814 * @attr ref android.R.styleable#TextView_drawableStart
1815 * @attr ref android.R.styleable#TextView_drawableTop
1816 * @attr ref android.R.styleable#TextView_drawableEnd
1817 * @attr ref android.R.styleable#TextView_drawableBottom
1818 *
1819 * @hide
1820 */
1821 public void setCompoundDrawablesRelative(Drawable start, Drawable top,
1822 Drawable end, Drawable bottom) {
1823 Drawables dr = mDrawables;
1824
1825 final boolean drawables = start != null || top != null
1826 || end != null || bottom != null;
1827
1828 if (!drawables) {
1829 // Clearing drawables... can we free the data structure?
1830 if (dr != null) {
1831 if (dr.mDrawablePadding == 0) {
1832 mDrawables = null;
1833 } else {
1834 // We need to retain the last set padding, so just clear
1835 // out all of the fields in the existing structure.
1836 if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
1837 dr.mDrawableStart = null;
1838 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1839 dr.mDrawableTop = null;
1840 if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
1841 dr.mDrawableEnd = null;
1842 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1843 dr.mDrawableBottom = null;
1844 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1845 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1846 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1847 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1848 }
1849 }
1850 } else {
1851 if (dr == null) {
1852 mDrawables = dr = new Drawables();
1853 }
1854
1855 if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
1856 dr.mDrawableStart.setCallback(null);
1857 }
1858 dr.mDrawableStart = start;
1859
1860 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1861 dr.mDrawableTop.setCallback(null);
1862 }
1863 dr.mDrawableTop = top;
1864
1865 if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
1866 dr.mDrawableEnd.setCallback(null);
1867 }
1868 dr.mDrawableEnd = end;
1869
1870 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1871 dr.mDrawableBottom.setCallback(null);
1872 }
1873 dr.mDrawableBottom = bottom;
1874
1875 final Rect compoundRect = dr.mCompoundRect;
1876 int[] state;
1877
1878 state = getDrawableState();
1879
1880 if (start != null) {
1881 start.setState(state);
1882 start.copyBounds(compoundRect);
1883 start.setCallback(this);
1884 dr.mDrawableSizeStart = compoundRect.width();
1885 dr.mDrawableHeightStart = compoundRect.height();
1886 } else {
1887 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1888 }
1889
1890 if (end != null) {
1891 end.setState(state);
1892 end.copyBounds(compoundRect);
1893 end.setCallback(this);
1894 dr.mDrawableSizeEnd = compoundRect.width();
1895 dr.mDrawableHeightEnd = compoundRect.height();
1896 } else {
1897 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1898 }
1899
1900 if (top != null) {
1901 top.setState(state);
1902 top.copyBounds(compoundRect);
1903 top.setCallback(this);
1904 dr.mDrawableSizeTop = compoundRect.height();
1905 dr.mDrawableWidthTop = compoundRect.width();
1906 } else {
1907 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1908 }
1909
1910 if (bottom != null) {
1911 bottom.setState(state);
1912 bottom.copyBounds(compoundRect);
1913 bottom.setCallback(this);
1914 dr.mDrawableSizeBottom = compoundRect.height();
1915 dr.mDrawableWidthBottom = compoundRect.width();
1916 } else {
1917 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1918 }
1919 }
1920
1921 resolveDrawables();
1922 invalidate();
1923 requestLayout();
1924 }
1925
1926 /**
1927 * Sets the Drawables (if any) to appear to the start of, above,
1928 * to the end of, and below the text. Use 0 if you do not
1929 * want a Drawable there. The Drawables' bounds will be set to
1930 * their intrinsic bounds.
1931 *
1932 * @param start Resource identifier of the start Drawable.
1933 * @param top Resource identifier of the top Drawable.
1934 * @param end Resource identifier of the end Drawable.
1935 * @param bottom Resource identifier of the bottom Drawable.
1936 *
1937 * @attr ref android.R.styleable#TextView_drawableStart
1938 * @attr ref android.R.styleable#TextView_drawableTop
1939 * @attr ref android.R.styleable#TextView_drawableEnd
1940 * @attr ref android.R.styleable#TextView_drawableBottom
1941 *
1942 * @hide
1943 */
1944 public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end,
1945 int bottom) {
1946 resetResolvedDrawables();
1947 final Resources resources = getContext().getResources();
1948 setCompoundDrawablesRelativeWithIntrinsicBounds(
1949 start != 0 ? resources.getDrawable(start) : null,
1950 top != 0 ? resources.getDrawable(top) : null,
1951 end != 0 ? resources.getDrawable(end) : null,
1952 bottom != 0 ? resources.getDrawable(bottom) : null);
1953 }
1954
1955 /**
1956 * Sets the Drawables (if any) to appear to the start of, above,
1957 * to the end of, and below the text. Use null if you do not
1958 * want a Drawable there. The Drawables' bounds will be set to
1959 * their intrinsic bounds.
1960 *
1961 * @attr ref android.R.styleable#TextView_drawableStart
1962 * @attr ref android.R.styleable#TextView_drawableTop
1963 * @attr ref android.R.styleable#TextView_drawableEnd
1964 * @attr ref android.R.styleable#TextView_drawableBottom
1965 *
1966 * @hide
1967 */
1968 public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top,
1969 Drawable end, Drawable bottom) {
1970
1971 resetResolvedDrawables();
1972 if (start != null) {
1973 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1974 }
1975 if (end != null) {
1976 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1977 }
1978 if (top != null) {
1979 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1980 }
1981 if (bottom != null) {
1982 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1983 }
1984 setCompoundDrawablesRelative(start, top, end, bottom);
1985 }
1986
1987 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001988 * Returns drawables for the left, top, right, and bottom borders.
1989 */
1990 public Drawable[] getCompoundDrawables() {
1991 final Drawables dr = mDrawables;
1992 if (dr != null) {
1993 return new Drawable[] {
1994 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
1995 };
1996 } else {
1997 return new Drawable[] { null, null, null, null };
1998 }
1999 }
2000
2001 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002002 * Returns drawables for the start, top, end, and bottom borders.
2003 *
2004 * @hide
2005 */
2006 public Drawable[] getCompoundDrawablesRelative() {
2007 final Drawables dr = mDrawables;
2008 if (dr != null) {
2009 return new Drawable[] {
2010 dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom
2011 };
2012 } else {
2013 return new Drawable[] { null, null, null, null };
2014 }
2015 }
2016
2017 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002018 * Sets the size of the padding between the compound drawables and
2019 * the text.
2020 *
2021 * @attr ref android.R.styleable#TextView_drawablePadding
2022 */
2023 public void setCompoundDrawablePadding(int pad) {
2024 Drawables dr = mDrawables;
2025 if (pad == 0) {
2026 if (dr != null) {
2027 dr.mDrawablePadding = pad;
2028 }
2029 } else {
2030 if (dr == null) {
2031 mDrawables = dr = new Drawables();
2032 }
2033 dr.mDrawablePadding = pad;
2034 }
2035
2036 invalidate();
2037 requestLayout();
2038 }
2039
2040 /**
2041 * Returns the padding between the compound drawables and the text.
2042 */
2043 public int getCompoundDrawablePadding() {
2044 final Drawables dr = mDrawables;
2045 return dr != null ? dr.mDrawablePadding : 0;
2046 }
2047
2048 @Override
2049 public void setPadding(int left, int top, int right, int bottom) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07002050 if (left != mPaddingLeft ||
2051 right != mPaddingRight ||
2052 top != mPaddingTop ||
2053 bottom != mPaddingBottom) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002054 nullLayouts();
2055 }
2056
2057 // the super call will requestLayout()
2058 super.setPadding(left, top, right, bottom);
2059 invalidate();
2060 }
2061
2062 /**
2063 * Gets the autolink mask of the text. See {@link
2064 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2065 * possible values.
2066 *
2067 * @attr ref android.R.styleable#TextView_autoLink
2068 */
2069 public final int getAutoLinkMask() {
2070 return mAutoLinkMask;
2071 }
2072
2073 /**
2074 * Sets the text color, size, style, hint color, and highlight color
2075 * from the specified TextAppearance resource.
2076 */
2077 public void setTextAppearance(Context context, int resid) {
2078 TypedArray appearance =
2079 context.obtainStyledAttributes(resid,
2080 com.android.internal.R.styleable.TextAppearance);
2081
2082 int color;
2083 ColorStateList colors;
2084 int ts;
2085
2086 color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
2087 if (color != 0) {
2088 setHighlightColor(color);
2089 }
2090
2091 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2092 TextAppearance_textColor);
2093 if (colors != null) {
2094 setTextColor(colors);
2095 }
2096
2097 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
2098 TextAppearance_textSize, 0);
2099 if (ts != 0) {
2100 setRawTextSize(ts);
2101 }
2102
2103 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2104 TextAppearance_textColorHint);
2105 if (colors != null) {
2106 setHintTextColor(colors);
2107 }
2108
2109 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2110 TextAppearance_textColorLink);
2111 if (colors != null) {
2112 setLinkTextColor(colors);
2113 }
2114
2115 int typefaceIndex, styleIndex;
2116
2117 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
2118 TextAppearance_typeface, -1);
2119 styleIndex = appearance.getInt(com.android.internal.R.styleable.
2120 TextAppearance_textStyle, -1);
2121
2122 setTypefaceByIndex(typefaceIndex, styleIndex);
Gilles Debunne21078e42011-08-02 10:22:35 -07002123
Adam Powell7f8f79a2011-07-07 18:35:54 -07002124 if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
2125 false)) {
2126 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
2127 }
2128
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002129 appearance.recycle();
2130 }
2131
2132 /**
2133 * @return the size (in pixels) of the default text size in this TextView.
2134 */
2135 public float getTextSize() {
2136 return mTextPaint.getTextSize();
2137 }
2138
2139 /**
2140 * Set the default text size to the given value, interpreted as "scaled
2141 * pixel" units. This size is adjusted based on the current density and
2142 * user font size preference.
2143 *
2144 * @param size The scaled pixel size.
2145 *
2146 * @attr ref android.R.styleable#TextView_textSize
2147 */
2148 @android.view.RemotableViewMethod
2149 public void setTextSize(float size) {
2150 setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
2151 }
2152
2153 /**
2154 * Set the default text size to a given unit and value. See {@link
2155 * TypedValue} for the possible dimension units.
2156 *
2157 * @param unit The desired dimension unit.
2158 * @param size The desired size in the given units.
2159 *
2160 * @attr ref android.R.styleable#TextView_textSize
2161 */
2162 public void setTextSize(int unit, float size) {
2163 Context c = getContext();
2164 Resources r;
2165
2166 if (c == null)
2167 r = Resources.getSystem();
2168 else
2169 r = c.getResources();
2170
2171 setRawTextSize(TypedValue.applyDimension(
2172 unit, size, r.getDisplayMetrics()));
2173 }
2174
2175 private void setRawTextSize(float size) {
2176 if (size != mTextPaint.getTextSize()) {
2177 mTextPaint.setTextSize(size);
2178
2179 if (mLayout != null) {
2180 nullLayouts();
2181 requestLayout();
2182 invalidate();
2183 }
2184 }
2185 }
2186
2187 /**
2188 * @return the extent by which text is currently being stretched
2189 * horizontally. This will usually be 1.
2190 */
2191 public float getTextScaleX() {
2192 return mTextPaint.getTextScaleX();
2193 }
2194
2195 /**
2196 * Sets the extent by which text should be stretched horizontally.
2197 *
2198 * @attr ref android.R.styleable#TextView_textScaleX
2199 */
2200 @android.view.RemotableViewMethod
2201 public void setTextScaleX(float size) {
2202 if (size != mTextPaint.getTextScaleX()) {
Romain Guy939151f2009-04-08 14:22:40 -07002203 mUserSetTextScaleX = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002204 mTextPaint.setTextScaleX(size);
2205
2206 if (mLayout != null) {
2207 nullLayouts();
2208 requestLayout();
2209 invalidate();
2210 }
2211 }
2212 }
2213
2214 /**
2215 * Sets the typeface and style in which the text should be displayed.
2216 * Note that not all Typeface families actually have bold and italic
2217 * variants, so you may need to use
2218 * {@link #setTypeface(Typeface, int)} to get the appearance
2219 * that you actually want.
2220 *
2221 * @attr ref android.R.styleable#TextView_typeface
2222 * @attr ref android.R.styleable#TextView_textStyle
2223 */
2224 public void setTypeface(Typeface tf) {
2225 if (mTextPaint.getTypeface() != tf) {
2226 mTextPaint.setTypeface(tf);
2227
2228 if (mLayout != null) {
2229 nullLayouts();
2230 requestLayout();
2231 invalidate();
2232 }
2233 }
2234 }
2235
2236 /**
2237 * @return the current typeface and style in which the text is being
2238 * displayed.
2239 */
2240 public Typeface getTypeface() {
2241 return mTextPaint.getTypeface();
2242 }
2243
2244 /**
2245 * Sets the text color for all the states (normal, selected,
2246 * focused) to be this color.
2247 *
2248 * @attr ref android.R.styleable#TextView_textColor
2249 */
2250 @android.view.RemotableViewMethod
2251 public void setTextColor(int color) {
2252 mTextColor = ColorStateList.valueOf(color);
2253 updateTextColors();
2254 }
2255
2256 /**
2257 * Sets the text color.
2258 *
2259 * @attr ref android.R.styleable#TextView_textColor
2260 */
2261 public void setTextColor(ColorStateList colors) {
2262 if (colors == null) {
2263 throw new NullPointerException();
2264 }
2265
2266 mTextColor = colors;
2267 updateTextColors();
2268 }
2269
2270 /**
2271 * Return the set of text colors.
2272 *
2273 * @return Returns the set of text colors.
2274 */
2275 public final ColorStateList getTextColors() {
2276 return mTextColor;
2277 }
2278
2279 /**
2280 * <p>Return the current color selected for normal text.</p>
2281 *
2282 * @return Returns the current text color.
2283 */
2284 public final int getCurrentTextColor() {
2285 return mCurTextColor;
2286 }
2287
2288 /**
2289 * Sets the color used to display the selection highlight.
2290 *
2291 * @attr ref android.R.styleable#TextView_textColorHighlight
2292 */
2293 @android.view.RemotableViewMethod
2294 public void setHighlightColor(int color) {
2295 if (mHighlightColor != color) {
2296 mHighlightColor = color;
2297 invalidate();
2298 }
2299 }
2300
2301 /**
2302 * Gives the text a shadow of the specified radius and color, the specified
2303 * distance from its normal position.
2304 *
2305 * @attr ref android.R.styleable#TextView_shadowColor
2306 * @attr ref android.R.styleable#TextView_shadowDx
2307 * @attr ref android.R.styleable#TextView_shadowDy
2308 * @attr ref android.R.styleable#TextView_shadowRadius
2309 */
2310 public void setShadowLayer(float radius, float dx, float dy, int color) {
2311 mTextPaint.setShadowLayer(radius, dx, dy, color);
2312
2313 mShadowRadius = radius;
2314 mShadowDx = dx;
2315 mShadowDy = dy;
2316
2317 invalidate();
2318 }
2319
2320 /**
2321 * @return the base paint used for the text. Please use this only to
2322 * consult the Paint's properties and not to change them.
2323 */
2324 public TextPaint getPaint() {
2325 return mTextPaint;
2326 }
2327
2328 /**
2329 * Sets the autolink mask of the text. See {@link
2330 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2331 * possible values.
2332 *
2333 * @attr ref android.R.styleable#TextView_autoLink
2334 */
2335 @android.view.RemotableViewMethod
2336 public final void setAutoLinkMask(int mask) {
2337 mAutoLinkMask = mask;
2338 }
2339
2340 /**
2341 * Sets whether the movement method will automatically be set to
2342 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2343 * set to nonzero and links are detected in {@link #setText}.
2344 * The default is true.
2345 *
2346 * @attr ref android.R.styleable#TextView_linksClickable
2347 */
2348 @android.view.RemotableViewMethod
2349 public final void setLinksClickable(boolean whether) {
2350 mLinksClickable = whether;
2351 }
2352
2353 /**
2354 * Returns whether the movement method will automatically be set to
2355 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2356 * set to nonzero and links are detected in {@link #setText}.
2357 * The default is true.
2358 *
2359 * @attr ref android.R.styleable#TextView_linksClickable
2360 */
2361 public final boolean getLinksClickable() {
2362 return mLinksClickable;
2363 }
2364
2365 /**
2366 * Returns the list of URLSpans attached to the text
2367 * (by {@link Linkify} or otherwise) if any. You can call
2368 * {@link URLSpan#getURL} on them to find where they link to
2369 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
2370 * to find the region of the text they are attached to.
2371 */
2372 public URLSpan[] getUrls() {
2373 if (mText instanceof Spanned) {
2374 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
2375 } else {
2376 return new URLSpan[0];
2377 }
2378 }
2379
2380 /**
2381 * Sets the color of the hint text.
2382 *
2383 * @attr ref android.R.styleable#TextView_textColorHint
2384 */
2385 @android.view.RemotableViewMethod
2386 public final void setHintTextColor(int color) {
2387 mHintTextColor = ColorStateList.valueOf(color);
2388 updateTextColors();
2389 }
2390
2391 /**
2392 * Sets the color of the hint text.
2393 *
2394 * @attr ref android.R.styleable#TextView_textColorHint
2395 */
2396 public final void setHintTextColor(ColorStateList colors) {
2397 mHintTextColor = colors;
2398 updateTextColors();
2399 }
2400
2401 /**
2402 * <p>Return the color used to paint the hint text.</p>
2403 *
2404 * @return Returns the list of hint text colors.
2405 */
2406 public final ColorStateList getHintTextColors() {
2407 return mHintTextColor;
2408 }
2409
2410 /**
2411 * <p>Return the current color selected to paint the hint text.</p>
2412 *
2413 * @return Returns the current hint text color.
2414 */
2415 public final int getCurrentHintTextColor() {
2416 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2417 }
2418
2419 /**
2420 * Sets the color of links in the text.
2421 *
2422 * @attr ref android.R.styleable#TextView_textColorLink
2423 */
2424 @android.view.RemotableViewMethod
2425 public final void setLinkTextColor(int color) {
2426 mLinkTextColor = ColorStateList.valueOf(color);
2427 updateTextColors();
2428 }
2429
2430 /**
2431 * Sets the color of links in the text.
2432 *
2433 * @attr ref android.R.styleable#TextView_textColorLink
2434 */
2435 public final void setLinkTextColor(ColorStateList colors) {
2436 mLinkTextColor = colors;
2437 updateTextColors();
2438 }
2439
2440 /**
2441 * <p>Returns the color used to paint links in the text.</p>
2442 *
2443 * @return Returns the list of link text colors.
2444 */
2445 public final ColorStateList getLinkTextColors() {
2446 return mLinkTextColor;
2447 }
2448
2449 /**
2450 * Sets the horizontal alignment of the text and the
2451 * vertical gravity that will be used when there is extra space
2452 * in the TextView beyond what is required for the text itself.
2453 *
2454 * @see android.view.Gravity
2455 * @attr ref android.R.styleable#TextView_gravity
2456 */
2457 public void setGravity(int gravity) {
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002458 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -07002459 gravity |= Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002460 }
2461 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2462 gravity |= Gravity.TOP;
2463 }
2464
2465 boolean newLayout = false;
2466
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002467 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=
2468 (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002469 newLayout = true;
2470 }
2471
2472 if (gravity != mGravity) {
2473 invalidate();
2474 }
2475
2476 mGravity = gravity;
2477
2478 if (mLayout != null && newLayout) {
2479 // XXX this is heavy-handed because no actual content changes.
2480 int want = mLayout.getWidth();
2481 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2482
2483 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2484 mRight - mLeft - getCompoundPaddingLeft() -
2485 getCompoundPaddingRight(), true);
2486 }
2487 }
2488
2489 /**
2490 * Returns the horizontal and vertical alignment of this TextView.
2491 *
2492 * @see android.view.Gravity
2493 * @attr ref android.R.styleable#TextView_gravity
2494 */
2495 public int getGravity() {
2496 return mGravity;
2497 }
2498
2499 /**
2500 * @return the flags on the Paint being used to display the text.
2501 * @see Paint#getFlags
2502 */
2503 public int getPaintFlags() {
2504 return mTextPaint.getFlags();
2505 }
2506
2507 /**
2508 * Sets flags on the Paint being used to display the text and
2509 * reflows the text if they are different from the old flags.
2510 * @see Paint#setFlags
2511 */
2512 @android.view.RemotableViewMethod
2513 public void setPaintFlags(int flags) {
2514 if (mTextPaint.getFlags() != flags) {
2515 mTextPaint.setFlags(flags);
2516
2517 if (mLayout != null) {
2518 nullLayouts();
2519 requestLayout();
2520 invalidate();
2521 }
2522 }
2523 }
2524
2525 /**
2526 * Sets whether the text should be allowed to be wider than the
2527 * View is. If false, it will be wrapped to the width of the View.
2528 *
2529 * @attr ref android.R.styleable#TextView_scrollHorizontally
2530 */
2531 public void setHorizontallyScrolling(boolean whether) {
Gilles Debunne22378292011-08-12 10:38:52 -07002532 if (mHorizontallyScrolling != whether) {
2533 mHorizontallyScrolling = whether;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002534
Gilles Debunne22378292011-08-12 10:38:52 -07002535 if (mLayout != null) {
2536 nullLayouts();
2537 requestLayout();
2538 invalidate();
2539 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002540 }
2541 }
2542
2543 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002544 * Makes the TextView at least this many lines tall.
2545 *
2546 * Setting this value overrides any other (minimum) height setting. A single line TextView will
2547 * set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002548 *
2549 * @attr ref android.R.styleable#TextView_minLines
2550 */
2551 @android.view.RemotableViewMethod
2552 public void setMinLines(int minlines) {
2553 mMinimum = minlines;
2554 mMinMode = LINES;
2555
2556 requestLayout();
2557 invalidate();
2558 }
2559
2560 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002561 * Makes the TextView at least this many pixels tall.
2562 *
2563 * Setting this value overrides any other (minimum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002564 *
2565 * @attr ref android.R.styleable#TextView_minHeight
2566 */
2567 @android.view.RemotableViewMethod
2568 public void setMinHeight(int minHeight) {
2569 mMinimum = minHeight;
2570 mMinMode = PIXELS;
2571
2572 requestLayout();
2573 invalidate();
2574 }
2575
2576 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002577 * Makes the TextView at most this many lines tall.
2578 *
2579 * Setting this value overrides any other (maximum) height setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002580 *
2581 * @attr ref android.R.styleable#TextView_maxLines
2582 */
2583 @android.view.RemotableViewMethod
2584 public void setMaxLines(int maxlines) {
2585 mMaximum = maxlines;
2586 mMaxMode = LINES;
2587
2588 requestLayout();
2589 invalidate();
2590 }
2591
2592 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002593 * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the
2594 * {@link #setMaxLines(int)} method.
2595 *
2596 * Setting this value overrides any other (maximum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002597 *
2598 * @attr ref android.R.styleable#TextView_maxHeight
2599 */
2600 @android.view.RemotableViewMethod
2601 public void setMaxHeight(int maxHeight) {
2602 mMaximum = maxHeight;
2603 mMaxMode = PIXELS;
2604
2605 requestLayout();
2606 invalidate();
2607 }
2608
2609 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002610 * Makes the TextView exactly this many lines tall.
2611 *
2612 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2613 * height setting. A single line TextView will set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002614 *
2615 * @attr ref android.R.styleable#TextView_lines
2616 */
2617 @android.view.RemotableViewMethod
2618 public void setLines(int lines) {
2619 mMaximum = mMinimum = lines;
2620 mMaxMode = mMinMode = LINES;
2621
2622 requestLayout();
2623 invalidate();
2624 }
2625
2626 /**
2627 * Makes the TextView exactly this many pixels tall.
2628 * You could do the same thing by specifying this number in the
2629 * LayoutParams.
2630 *
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002631 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2632 * height setting.
2633 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002634 * @attr ref android.R.styleable#TextView_height
2635 */
2636 @android.view.RemotableViewMethod
2637 public void setHeight(int pixels) {
2638 mMaximum = mMinimum = pixels;
2639 mMaxMode = mMinMode = PIXELS;
2640
2641 requestLayout();
2642 invalidate();
2643 }
2644
2645 /**
2646 * Makes the TextView at least this many ems wide
2647 *
2648 * @attr ref android.R.styleable#TextView_minEms
2649 */
2650 @android.view.RemotableViewMethod
2651 public void setMinEms(int minems) {
2652 mMinWidth = minems;
2653 mMinWidthMode = EMS;
2654
2655 requestLayout();
2656 invalidate();
2657 }
2658
2659 /**
2660 * Makes the TextView at least this many pixels wide
2661 *
2662 * @attr ref android.R.styleable#TextView_minWidth
2663 */
2664 @android.view.RemotableViewMethod
2665 public void setMinWidth(int minpixels) {
2666 mMinWidth = minpixels;
2667 mMinWidthMode = PIXELS;
2668
2669 requestLayout();
2670 invalidate();
2671 }
2672
2673 /**
2674 * Makes the TextView at most this many ems wide
2675 *
2676 * @attr ref android.R.styleable#TextView_maxEms
2677 */
2678 @android.view.RemotableViewMethod
2679 public void setMaxEms(int maxems) {
2680 mMaxWidth = maxems;
2681 mMaxWidthMode = EMS;
2682
2683 requestLayout();
2684 invalidate();
2685 }
2686
2687 /**
2688 * Makes the TextView at most this many pixels wide
2689 *
2690 * @attr ref android.R.styleable#TextView_maxWidth
2691 */
2692 @android.view.RemotableViewMethod
2693 public void setMaxWidth(int maxpixels) {
2694 mMaxWidth = maxpixels;
2695 mMaxWidthMode = PIXELS;
2696
2697 requestLayout();
2698 invalidate();
2699 }
2700
2701 /**
2702 * Makes the TextView exactly this many ems wide
2703 *
2704 * @attr ref android.R.styleable#TextView_ems
2705 */
2706 @android.view.RemotableViewMethod
2707 public void setEms(int ems) {
2708 mMaxWidth = mMinWidth = ems;
2709 mMaxWidthMode = mMinWidthMode = EMS;
2710
2711 requestLayout();
2712 invalidate();
2713 }
2714
2715 /**
2716 * Makes the TextView exactly this many pixels wide.
2717 * You could do the same thing by specifying this number in the
2718 * LayoutParams.
2719 *
2720 * @attr ref android.R.styleable#TextView_width
2721 */
2722 @android.view.RemotableViewMethod
2723 public void setWidth(int pixels) {
2724 mMaxWidth = mMinWidth = pixels;
2725 mMaxWidthMode = mMinWidthMode = PIXELS;
2726
2727 requestLayout();
2728 invalidate();
2729 }
2730
2731
2732 /**
2733 * Sets line spacing for this TextView. Each line will have its height
2734 * multiplied by <code>mult</code> and have <code>add</code> added to it.
2735 *
2736 * @attr ref android.R.styleable#TextView_lineSpacingExtra
2737 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2738 */
2739 public void setLineSpacing(float add, float mult) {
Gilles Debunne22378292011-08-12 10:38:52 -07002740 if (mSpacingAdd != add || mSpacingMult != mult) {
2741 mSpacingAdd = add;
2742 mSpacingMult = mult;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002743
Gilles Debunne22378292011-08-12 10:38:52 -07002744 if (mLayout != null) {
2745 nullLayouts();
2746 requestLayout();
2747 invalidate();
2748 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002749 }
2750 }
2751
2752 /**
2753 * Convenience method: Append the specified text to the TextView's
2754 * display buffer, upgrading it to BufferType.EDITABLE if it was
2755 * not already editable.
2756 */
2757 public final void append(CharSequence text) {
2758 append(text, 0, text.length());
2759 }
2760
2761 /**
2762 * Convenience method: Append the specified text slice to the TextView's
2763 * display buffer, upgrading it to BufferType.EDITABLE if it was
2764 * not already editable.
2765 */
2766 public void append(CharSequence text, int start, int end) {
2767 if (!(mText instanceof Editable)) {
2768 setText(mText, BufferType.EDITABLE);
2769 }
2770
2771 ((Editable) mText).append(text, start, end);
2772 }
2773
2774 private void updateTextColors() {
2775 boolean inval = false;
2776 int color = mTextColor.getColorForState(getDrawableState(), 0);
2777 if (color != mCurTextColor) {
2778 mCurTextColor = color;
2779 inval = true;
2780 }
2781 if (mLinkTextColor != null) {
2782 color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2783 if (color != mTextPaint.linkColor) {
2784 mTextPaint.linkColor = color;
2785 inval = true;
2786 }
2787 }
2788 if (mHintTextColor != null) {
2789 color = mHintTextColor.getColorForState(getDrawableState(), 0);
2790 if (color != mCurHintTextColor && mText.length() == 0) {
2791 mCurHintTextColor = color;
2792 inval = true;
2793 }
2794 }
2795 if (inval) {
2796 invalidate();
2797 }
2798 }
2799
2800 @Override
2801 protected void drawableStateChanged() {
2802 super.drawableStateChanged();
2803 if (mTextColor != null && mTextColor.isStateful()
2804 || (mHintTextColor != null && mHintTextColor.isStateful())
2805 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2806 updateTextColors();
2807 }
2808
2809 final Drawables dr = mDrawables;
2810 if (dr != null) {
2811 int[] state = getDrawableState();
2812 if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2813 dr.mDrawableTop.setState(state);
2814 }
2815 if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2816 dr.mDrawableBottom.setState(state);
2817 }
2818 if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2819 dr.mDrawableLeft.setState(state);
2820 }
2821 if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2822 dr.mDrawableRight.setState(state);
2823 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002824 if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) {
2825 dr.mDrawableStart.setState(state);
2826 }
2827 if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) {
2828 dr.mDrawableEnd.setState(state);
2829 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002830 }
2831 }
2832
2833 /**
2834 * User interface state that is stored by TextView for implementing
2835 * {@link View#onSaveInstanceState}.
2836 */
2837 public static class SavedState extends BaseSavedState {
2838 int selStart;
2839 int selEnd;
2840 CharSequence text;
2841 boolean frozenWithFocus;
The Android Open Source Project4df24232009-03-05 14:34:35 -08002842 CharSequence error;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002843
2844 SavedState(Parcelable superState) {
2845 super(superState);
2846 }
2847
2848 @Override
2849 public void writeToParcel(Parcel out, int flags) {
2850 super.writeToParcel(out, flags);
2851 out.writeInt(selStart);
2852 out.writeInt(selEnd);
2853 out.writeInt(frozenWithFocus ? 1 : 0);
2854 TextUtils.writeToParcel(text, out, flags);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002855
2856 if (error == null) {
2857 out.writeInt(0);
2858 } else {
2859 out.writeInt(1);
2860 TextUtils.writeToParcel(error, out, flags);
2861 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002862 }
2863
2864 @Override
2865 public String toString() {
2866 String str = "TextView.SavedState{"
2867 + Integer.toHexString(System.identityHashCode(this))
2868 + " start=" + selStart + " end=" + selEnd;
2869 if (text != null) {
2870 str += " text=" + text;
2871 }
2872 return str + "}";
2873 }
2874
Gilles Debunnee15b3582010-06-16 15:17:21 -07002875 @SuppressWarnings("hiding")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002876 public static final Parcelable.Creator<SavedState> CREATOR
2877 = new Parcelable.Creator<SavedState>() {
2878 public SavedState createFromParcel(Parcel in) {
2879 return new SavedState(in);
2880 }
2881
2882 public SavedState[] newArray(int size) {
2883 return new SavedState[size];
2884 }
2885 };
2886
2887 private SavedState(Parcel in) {
2888 super(in);
2889 selStart = in.readInt();
2890 selEnd = in.readInt();
2891 frozenWithFocus = (in.readInt() != 0);
2892 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002893
2894 if (in.readInt() != 0) {
2895 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2896 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002897 }
2898 }
2899
2900 @Override
2901 public Parcelable onSaveInstanceState() {
2902 Parcelable superState = super.onSaveInstanceState();
2903
2904 // Save state if we are forced to
2905 boolean save = mFreezesText;
2906 int start = 0;
2907 int end = 0;
2908
2909 if (mText != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07002910 start = getSelectionStart();
2911 end = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002912 if (start >= 0 || end >= 0) {
2913 // Or save state if there is a selection
2914 save = true;
2915 }
2916 }
2917
2918 if (save) {
2919 SavedState ss = new SavedState(superState);
2920 // XXX Should also save the current scroll position!
2921 ss.selStart = start;
2922 ss.selEnd = end;
2923
2924 if (mText instanceof Spanned) {
2925 /*
2926 * Calling setText() strips off any ChangeWatchers;
2927 * strip them now to avoid leaking references.
2928 * But do it to a copy so that if there are any
2929 * further changes to the text of this view, it
2930 * won't get into an inconsistent state.
2931 */
2932
2933 Spannable sp = new SpannableString(mText);
2934
2935 for (ChangeWatcher cw :
2936 sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
2937 sp.removeSpan(cw);
2938 }
2939
Gilles Debunneaa67eef2011-06-01 18:03:37 -07002940 sp.removeSpan(mSuggestionRangeSpan);
2941
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002942 ss.text = sp;
2943 } else {
2944 ss.text = mText.toString();
2945 }
2946
2947 if (isFocused() && start >= 0 && end >= 0) {
2948 ss.frozenWithFocus = true;
2949 }
2950
The Android Open Source Project4df24232009-03-05 14:34:35 -08002951 ss.error = mError;
2952
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002953 return ss;
2954 }
2955
2956 return superState;
2957 }
2958
2959 @Override
2960 public void onRestoreInstanceState(Parcelable state) {
2961 if (!(state instanceof SavedState)) {
2962 super.onRestoreInstanceState(state);
2963 return;
2964 }
2965
2966 SavedState ss = (SavedState)state;
2967 super.onRestoreInstanceState(ss.getSuperState());
2968
2969 // XXX restore buffer type too, as well as lots of other stuff
2970 if (ss.text != null) {
2971 setText(ss.text);
2972 }
2973
2974 if (ss.selStart >= 0 && ss.selEnd >= 0) {
2975 if (mText instanceof Spannable) {
2976 int len = mText.length();
2977
2978 if (ss.selStart > len || ss.selEnd > len) {
2979 String restored = "";
2980
2981 if (ss.text != null) {
2982 restored = "(restored) ";
2983 }
2984
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07002985 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002986 "/" + ss.selEnd + " out of range for " + restored +
2987 "text " + mText);
2988 } else {
2989 Selection.setSelection((Spannable) mText, ss.selStart,
2990 ss.selEnd);
2991
2992 if (ss.frozenWithFocus) {
2993 mFrozenWithFocus = true;
2994 }
2995 }
2996 }
2997 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08002998
2999 if (ss.error != null) {
Romain Guy9bc9fa12009-07-21 16:57:29 -07003000 final CharSequence error = ss.error;
3001 // Display the error later, after the first layout pass
3002 post(new Runnable() {
3003 public void run() {
3004 setError(error);
3005 }
3006 });
The Android Open Source Project4df24232009-03-05 14:34:35 -08003007 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003008 }
3009
3010 /**
3011 * Control whether this text view saves its entire text contents when
3012 * freezing to an icicle, in addition to dynamic state such as cursor
3013 * position. By default this is false, not saving the text. Set to true
3014 * if the text in the text view is not being saved somewhere else in
3015 * persistent storage (such as in a content provider) so that if the
3016 * view is later thawed the user will not lose their data.
3017 *
3018 * @param freezesText Controls whether a frozen icicle should include the
3019 * entire text data: true to include it, false to not.
3020 *
3021 * @attr ref android.R.styleable#TextView_freezesText
3022 */
3023 @android.view.RemotableViewMethod
3024 public void setFreezesText(boolean freezesText) {
3025 mFreezesText = freezesText;
3026 }
3027
3028 /**
3029 * Return whether this text view is including its entire text contents
3030 * in frozen icicles.
3031 *
3032 * @return Returns true if text is included, false if it isn't.
3033 *
3034 * @see #setFreezesText
3035 */
3036 public boolean getFreezesText() {
3037 return mFreezesText;
3038 }
3039
3040 ///////////////////////////////////////////////////////////////////////////
3041
3042 /**
3043 * Sets the Factory used to create new Editables.
3044 */
3045 public final void setEditableFactory(Editable.Factory factory) {
3046 mEditableFactory = factory;
3047 setText(mText);
3048 }
3049
3050 /**
3051 * Sets the Factory used to create new Spannables.
3052 */
3053 public final void setSpannableFactory(Spannable.Factory factory) {
3054 mSpannableFactory = factory;
3055 setText(mText);
3056 }
3057
3058 /**
3059 * Sets the string value of the TextView. TextView <em>does not</em> accept
3060 * HTML-like formatting, which you can do with text strings in XML resource files.
3061 * To style your strings, attach android.text.style.* objects to a
3062 * {@link android.text.SpannableString SpannableString}, or see the
3063 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
Gilles Debunne21078e42011-08-02 10:22:35 -07003064 * Available Resource Types</a> documentation for an example of setting
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003065 * formatted text in the XML resource file.
3066 *
3067 * @attr ref android.R.styleable#TextView_text
3068 */
3069 @android.view.RemotableViewMethod
3070 public final void setText(CharSequence text) {
3071 setText(text, mBufferType);
3072 }
3073
3074 /**
3075 * Like {@link #setText(CharSequence)},
3076 * except that the cursor position (if any) is retained in the new text.
3077 *
3078 * @param text The new text to place in the text view.
3079 *
3080 * @see #setText(CharSequence)
3081 */
3082 @android.view.RemotableViewMethod
3083 public final void setTextKeepState(CharSequence text) {
3084 setTextKeepState(text, mBufferType);
3085 }
3086
3087 /**
3088 * Sets the text that this TextView is to display (see
3089 * {@link #setText(CharSequence)}) and also sets whether it is stored
3090 * in a styleable/spannable buffer and whether it is editable.
3091 *
3092 * @attr ref android.R.styleable#TextView_text
3093 * @attr ref android.R.styleable#TextView_bufferType
3094 */
3095 public void setText(CharSequence text, BufferType type) {
3096 setText(text, type, true, 0);
3097
3098 if (mCharWrapper != null) {
3099 mCharWrapper.mChars = null;
3100 }
3101 }
3102
3103 private void setText(CharSequence text, BufferType type,
3104 boolean notifyBefore, int oldlen) {
3105 if (text == null) {
3106 text = "";
3107 }
3108
Luca Zanoline0760452011-09-08 12:03:37 +01003109 // If suggestions are not enabled, remove the suggestion spans from the text
3110 if (!isSuggestionsEnabled()) {
3111 text = removeSuggestionSpans(text);
3112 }
3113
Romain Guy939151f2009-04-08 14:22:40 -07003114 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
3115
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003116 if (text instanceof Spanned &&
3117 ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
Adam Powell282e3772011-08-30 16:51:11 -07003118 if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
3119 setHorizontalFadingEdgeEnabled(true);
3120 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
3121 } else {
3122 setHorizontalFadingEdgeEnabled(false);
3123 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
3124 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003125 setEllipsize(TextUtils.TruncateAt.MARQUEE);
3126 }
3127
3128 int n = mFilters.length;
3129 for (int i = 0; i < n; i++) {
3130 CharSequence out = mFilters[i].filter(text, 0, text.length(),
3131 EMPTY_SPANNED, 0, 0);
3132 if (out != null) {
3133 text = out;
3134 }
3135 }
3136
3137 if (notifyBefore) {
3138 if (mText != null) {
3139 oldlen = mText.length();
3140 sendBeforeTextChanged(mText, 0, oldlen, text.length());
3141 } else {
3142 sendBeforeTextChanged("", 0, 0, text.length());
3143 }
3144 }
3145
3146 boolean needEditableForNotification = false;
Gilles Debunne6435a562011-08-04 21:22:30 -07003147 boolean startSpellCheck = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003148
3149 if (mListeners != null && mListeners.size() != 0) {
3150 needEditableForNotification = true;
3151 }
3152
Gilles Debunne6435a562011-08-04 21:22:30 -07003153 if (type == BufferType.EDITABLE || mInput != null || needEditableForNotification) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003154 Editable t = mEditableFactory.newEditable(text);
3155 text = t;
3156 setFilters(t, mFilters);
3157 InputMethodManager imm = InputMethodManager.peekInstance();
3158 if (imm != null) imm.restartInput(this);
Gilles Debunne6435a562011-08-04 21:22:30 -07003159 startSpellCheck = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003160 } else if (type == BufferType.SPANNABLE || mMovement != null) {
3161 text = mSpannableFactory.newSpannable(text);
3162 } else if (!(text instanceof CharWrapper)) {
3163 text = TextUtils.stringOrSpannedString(text);
3164 }
3165
3166 if (mAutoLinkMask != 0) {
3167 Spannable s2;
3168
3169 if (type == BufferType.EDITABLE || text instanceof Spannable) {
3170 s2 = (Spannable) text;
3171 } else {
3172 s2 = mSpannableFactory.newSpannable(text);
3173 }
3174
3175 if (Linkify.addLinks(s2, mAutoLinkMask)) {
3176 text = s2;
3177 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
3178
3179 /*
3180 * We must go ahead and set the text before changing the
3181 * movement method, because setMovementMethod() may call
3182 * setText() again to try to upgrade the buffer type.
3183 */
3184 mText = text;
3185
Gilles Debunnecbcb3452010-12-17 15:31:02 -08003186 // Do not change the movement method for text that support text selection as it
3187 // would prevent an arbitrary cursor displacement.
Gilles Debunnebb588da2011-07-11 18:26:19 -07003188 if (mLinksClickable && !textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003189 setMovementMethod(LinkMovementMethod.getInstance());
3190 }
3191 }
3192 }
3193
3194 mBufferType = type;
3195 mText = text;
3196
Adam Powell7f8f79a2011-07-07 18:35:54 -07003197 if (mTransformation == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003198 mTransformed = text;
Adam Powell7f8f79a2011-07-07 18:35:54 -07003199 } else {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003200 mTransformed = mTransformation.getTransformation(text, this);
Adam Powell7f8f79a2011-07-07 18:35:54 -07003201 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003202
3203 final int textLength = text.length();
3204
Adam Powell7f8f79a2011-07-07 18:35:54 -07003205 if (text instanceof Spannable && !mAllowTransformationLengthChange) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003206 Spannable sp = (Spannable) text;
3207
3208 // Remove any ChangeWatchers that might have come
3209 // from other TextViews.
3210 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
3211 final int count = watchers.length;
3212 for (int i = 0; i < count; i++)
3213 sp.removeSpan(watchers[i]);
3214
3215 if (mChangeWatcher == null)
3216 mChangeWatcher = new ChangeWatcher();
3217
3218 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
3219 (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
3220
3221 if (mInput != null) {
3222 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
3223 }
3224
3225 if (mTransformation != null) {
3226 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003227 }
3228
3229 if (mMovement != null) {
3230 mMovement.initialize(this, (Spannable) text);
3231
3232 /*
3233 * Initializing the movement method will have set the
3234 * selection, so reset mSelectionMoved to keep that from
3235 * interfering with the normal on-focus selection-setting.
3236 */
3237 mSelectionMoved = false;
3238 }
3239 }
3240
3241 if (mLayout != null) {
3242 checkForRelayout();
3243 }
3244
3245 sendOnTextChanged(text, 0, oldlen, textLength);
3246 onTextChanged(text, 0, oldlen, textLength);
3247
Gilles Debunne6435a562011-08-04 21:22:30 -07003248 if (startSpellCheck) {
3249 updateSpellCheckSpans(0, textLength);
3250 }
3251
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003252 if (needEditableForNotification) {
3253 sendAfterTextChanged((Editable) text);
3254 }
Gilles Debunne05336272010-07-09 20:13:45 -07003255
Gilles Debunnebaaace52010-10-01 15:47:13 -07003256 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
Gilles Debunnef788a9f2010-07-22 10:17:23 -07003257 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003258 }
3259
3260 /**
3261 * Sets the TextView to display the specified slice of the specified
3262 * char array. You must promise that you will not change the contents
3263 * of the array except for right before another call to setText(),
3264 * since the TextView has no way to know that the text
3265 * has changed and that it needs to invalidate and re-layout.
3266 */
3267 public final void setText(char[] text, int start, int len) {
3268 int oldlen = 0;
3269
3270 if (start < 0 || len < 0 || start + len > text.length) {
3271 throw new IndexOutOfBoundsException(start + ", " + len);
3272 }
3273
3274 /*
3275 * We must do the before-notification here ourselves because if
3276 * the old text is a CharWrapper we destroy it before calling
3277 * into the normal path.
3278 */
3279 if (mText != null) {
3280 oldlen = mText.length();
3281 sendBeforeTextChanged(mText, 0, oldlen, len);
3282 } else {
3283 sendBeforeTextChanged("", 0, 0, len);
3284 }
3285
3286 if (mCharWrapper == null) {
3287 mCharWrapper = new CharWrapper(text, start, len);
3288 } else {
3289 mCharWrapper.set(text, start, len);
3290 }
3291
3292 setText(mCharWrapper, mBufferType, false, oldlen);
3293 }
3294
Gilles Debunne3bca69b2011-05-23 18:20:22 -07003295 private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003296 private char[] mChars;
3297 private int mStart, mLength;
3298
3299 public CharWrapper(char[] chars, int start, int len) {
3300 mChars = chars;
3301 mStart = start;
3302 mLength = len;
3303 }
3304
3305 /* package */ void set(char[] chars, int start, int len) {
3306 mChars = chars;
3307 mStart = start;
3308 mLength = len;
3309 }
3310
3311 public int length() {
3312 return mLength;
3313 }
3314
3315 public char charAt(int off) {
3316 return mChars[off + mStart];
3317 }
3318
Gilles Debunnee15b3582010-06-16 15:17:21 -07003319 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003320 public String toString() {
3321 return new String(mChars, mStart, mLength);
3322 }
3323
3324 public CharSequence subSequence(int start, int end) {
3325 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3326 throw new IndexOutOfBoundsException(start + ", " + end);
3327 }
3328
3329 return new String(mChars, start + mStart, end - start);
3330 }
3331
3332 public void getChars(int start, int end, char[] buf, int off) {
3333 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3334 throw new IndexOutOfBoundsException(start + ", " + end);
3335 }
3336
3337 System.arraycopy(mChars, start + mStart, buf, off, end - start);
3338 }
3339
3340 public void drawText(Canvas c, int start, int end,
3341 float x, float y, Paint p) {
3342 c.drawText(mChars, start + mStart, end - start, x, y, p);
3343 }
3344
Doug Feltf47d7402010-04-21 16:01:52 -07003345 public void drawTextRun(Canvas c, int start, int end,
Doug Felt0c702b82010-05-14 10:55:42 -07003346 int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
3347 int count = end - start;
3348 int contextCount = contextEnd - contextStart;
3349 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
3350 contextCount, x, y, flags, p);
Doug Feltf47d7402010-04-21 16:01:52 -07003351 }
3352
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003353 public float measureText(int start, int end, Paint p) {
3354 return p.measureText(mChars, start + mStart, end - start);
3355 }
3356
3357 public int getTextWidths(int start, int end, float[] widths, Paint p) {
3358 return p.getTextWidths(mChars, start + mStart, end - start, widths);
3359 }
Doug Felt0c702b82010-05-14 10:55:42 -07003360
3361 public float getTextRunAdvances(int start, int end, int contextStart,
3362 int contextEnd, int flags, float[] advances, int advancesIndex,
3363 Paint p) {
3364 int count = end - start;
3365 int contextCount = contextEnd - contextStart;
3366 return p.getTextRunAdvances(mChars, start + mStart, count,
3367 contextStart + mStart, contextCount, flags, advances,
3368 advancesIndex);
3369 }
3370
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003371 public float getTextRunAdvances(int start, int end, int contextStart,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003372 int contextEnd, int flags, float[] advances, int advancesIndex,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003373 Paint p, int reserved) {
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003374 int count = end - start;
3375 int contextCount = contextEnd - contextStart;
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003376 return p.getTextRunAdvances(mChars, start + mStart, count,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003377 contextStart + mStart, contextCount, flags, advances,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003378 advancesIndex, reserved);
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003379 }
3380
Doug Felt0c702b82010-05-14 10:55:42 -07003381 public int getTextRunCursor(int contextStart, int contextEnd, int flags,
3382 int offset, int cursorOpt, Paint p) {
3383 int contextCount = contextEnd - contextStart;
3384 return p.getTextRunCursor(mChars, contextStart + mStart,
3385 contextCount, flags, offset + mStart, cursorOpt);
3386 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003387 }
3388
3389 /**
3390 * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3391 * except that the cursor position (if any) is retained in the new text.
3392 *
3393 * @see #setText(CharSequence, android.widget.TextView.BufferType)
3394 */
3395 public final void setTextKeepState(CharSequence text, BufferType type) {
3396 int start = getSelectionStart();
3397 int end = getSelectionEnd();
3398 int len = text.length();
3399
3400 setText(text, type);
3401
3402 if (start >= 0 || end >= 0) {
3403 if (mText instanceof Spannable) {
3404 Selection.setSelection((Spannable) mText,
3405 Math.max(0, Math.min(start, len)),
3406 Math.max(0, Math.min(end, len)));
3407 }
3408 }
3409 }
3410
3411 @android.view.RemotableViewMethod
3412 public final void setText(int resid) {
3413 setText(getContext().getResources().getText(resid));
3414 }
3415
3416 public final void setText(int resid, BufferType type) {
3417 setText(getContext().getResources().getText(resid), type);
3418 }
3419
3420 /**
3421 * Sets the text to be displayed when the text of the TextView is empty.
3422 * Null means to use the normal empty text. The hint does not currently
3423 * participate in determining the size of the view.
3424 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003425 * @attr ref android.R.styleable#TextView_hint
3426 */
3427 @android.view.RemotableViewMethod
3428 public final void setHint(CharSequence hint) {
3429 mHint = TextUtils.stringOrSpannedString(hint);
3430
3431 if (mLayout != null) {
3432 checkForRelayout();
3433 }
3434
Romain Guy4dc4f732009-06-19 15:16:40 -07003435 if (mText.length() == 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003436 invalidate();
Romain Guy4dc4f732009-06-19 15:16:40 -07003437 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003438 }
3439
3440 /**
3441 * Sets the text to be displayed when the text of the TextView is empty,
3442 * from a resource.
3443 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003444 * @attr ref android.R.styleable#TextView_hint
3445 */
3446 @android.view.RemotableViewMethod
3447 public final void setHint(int resid) {
3448 setHint(getContext().getResources().getText(resid));
3449 }
3450
3451 /**
3452 * Returns the hint that is displayed when the text of the TextView
3453 * is empty.
3454 *
3455 * @attr ref android.R.styleable#TextView_hint
3456 */
3457 @ViewDebug.CapturedViewProperty
3458 public CharSequence getHint() {
3459 return mHint;
3460 }
3461
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003462 private static boolean isMultilineInputType(int type) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003463 return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3464 (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3465 }
3466
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003467 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003468 * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3469 * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3470 * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
3471 * then a soft keyboard will not be displayed for this text view.
3472 *
3473 * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3474 * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3475 * type.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003476 *
3477 * @see #getInputType()
3478 * @see #setRawInputType(int)
3479 * @see android.text.InputType
3480 * @attr ref android.R.styleable#TextView_inputType
3481 */
3482 public void setInputType(int type) {
Bjorn Bringertad8da912009-09-17 10:47:35 +01003483 final boolean wasPassword = isPasswordInputType(mInputType);
3484 final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003485 setInputType(type, false);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003486 final boolean isPassword = isPasswordInputType(type);
3487 final boolean isVisiblePassword = isVisiblePasswordInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003488 boolean forceUpdate = false;
3489 if (isPassword) {
3490 setTransformationMethod(PasswordTransformationMethod.getInstance());
3491 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003492 } else if (isVisiblePassword) {
Amith Yamasania8c0edb2009-09-27 16:51:21 -07003493 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3494 forceUpdate = true;
3495 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003496 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003497 } else if (wasPassword || wasVisiblePassword) {
3498 // not in password mode, clean up typeface and transformation
3499 setTypefaceByIndex(-1, -1);
3500 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3501 forceUpdate = true;
3502 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003503 }
3504
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003505 boolean singleLine = !isMultilineInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003506
3507 // We need to update the single line mode if it has changed or we
3508 // were previously in password mode.
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003509 if (mSingleLine != singleLine || forceUpdate) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003510 // Change single line mode, but only change the transformation if
3511 // we are not in password mode.
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003512 applySingleLine(singleLine, !isPassword, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003513 }
3514
Luca Zanoline0760452011-09-08 12:03:37 +01003515 if (!isSuggestionsEnabled()) {
3516 mText = removeSuggestionSpans(mText);
3517 }
3518
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003519 InputMethodManager imm = InputMethodManager.peekInstance();
3520 if (imm != null) imm.restartInput(this);
3521 }
3522
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07003523 /**
3524 * It would be better to rely on the input type for everything. A password inputType should have
3525 * a password transformation. We should hence use isPasswordInputType instead of this method.
3526 *
3527 * We should:
3528 * - Call setInputType in setKeyListener instead of changing the input type directly (which
3529 * would install the correct transformation).
3530 * - Refuse the installation of a non-password transformation in setTransformation if the input
3531 * type is password.
3532 *
3533 * However, this is like this for legacy reasons and we cannot break existing apps. This method
3534 * is useful since it matches what the user can see (obfuscated text or not).
3535 *
3536 * @return true if the current transformation method is of the password type.
3537 */
3538 private boolean hasPasswordTransformationMethod() {
3539 return mTransformation instanceof PasswordTransformationMethod;
3540 }
3541
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003542 private static boolean isPasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003543 final int variation =
3544 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003545 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003546 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3547 || variation
Ken Wakasa82d731a2010-12-24 23:42:41 +09003548 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3549 || variation
3550 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003551 }
3552
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003553 private static boolean isVisiblePasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003554 final int variation =
3555 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003556 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003557 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003558 }
3559
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003560 /**
3561 * Directly change the content type integer of the text view, without
3562 * modifying any other state.
3563 * @see #setInputType(int)
3564 * @see android.text.InputType
3565 * @attr ref android.R.styleable#TextView_inputType
3566 */
3567 public void setRawInputType(int type) {
3568 mInputType = type;
3569 }
3570
3571 private void setInputType(int type, boolean direct) {
3572 final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3573 KeyListener input;
3574 if (cls == EditorInfo.TYPE_CLASS_TEXT) {
Gilles Debunnee67b58a2010-08-31 15:55:31 -07003575 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003576 TextKeyListener.Capitalize cap;
3577 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3578 cap = TextKeyListener.Capitalize.CHARACTERS;
3579 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3580 cap = TextKeyListener.Capitalize.WORDS;
3581 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3582 cap = TextKeyListener.Capitalize.SENTENCES;
3583 } else {
3584 cap = TextKeyListener.Capitalize.NONE;
3585 }
3586 input = TextKeyListener.getInstance(autotext, cap);
3587 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3588 input = DigitsKeyListener.getInstance(
3589 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3590 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3591 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3592 switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3593 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3594 input = DateKeyListener.getInstance();
3595 break;
3596 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3597 input = TimeKeyListener.getInstance();
3598 break;
3599 default:
3600 input = DateTimeKeyListener.getInstance();
3601 break;
3602 }
3603 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3604 input = DialerKeyListener.getInstance();
3605 } else {
3606 input = TextKeyListener.getInstance();
3607 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003608 setRawInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003609 if (direct) mInput = input;
3610 else {
3611 setKeyListenerOnly(input);
3612 }
3613 }
3614
3615 /**
3616 * Get the type of the content.
3617 *
3618 * @see #setInputType(int)
3619 * @see android.text.InputType
3620 */
3621 public int getInputType() {
3622 return mInputType;
3623 }
3624
3625 /**
3626 * Change the editor type integer associated with the text view, which
3627 * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3628 * has focus.
3629 * @see #getImeOptions
3630 * @see android.view.inputmethod.EditorInfo
3631 * @attr ref android.R.styleable#TextView_imeOptions
3632 */
3633 public void setImeOptions(int imeOptions) {
3634 if (mInputContentType == null) {
3635 mInputContentType = new InputContentType();
3636 }
3637 mInputContentType.imeOptions = imeOptions;
3638 }
3639
3640 /**
3641 * Get the type of the IME editor.
3642 *
3643 * @see #setImeOptions(int)
3644 * @see android.view.inputmethod.EditorInfo
3645 */
3646 public int getImeOptions() {
3647 return mInputContentType != null
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003648 ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003649 }
3650
3651 /**
3652 * Change the custom IME action associated with the text view, which
3653 * will be reported to an IME with {@link EditorInfo#actionLabel}
3654 * and {@link EditorInfo#actionId} when it has focus.
3655 * @see #getImeActionLabel
3656 * @see #getImeActionId
3657 * @see android.view.inputmethod.EditorInfo
3658 * @attr ref android.R.styleable#TextView_imeActionLabel
3659 * @attr ref android.R.styleable#TextView_imeActionId
3660 */
3661 public void setImeActionLabel(CharSequence label, int actionId) {
3662 if (mInputContentType == null) {
3663 mInputContentType = new InputContentType();
3664 }
3665 mInputContentType.imeActionLabel = label;
3666 mInputContentType.imeActionId = actionId;
3667 }
3668
3669 /**
3670 * Get the IME action label previous set with {@link #setImeActionLabel}.
3671 *
3672 * @see #setImeActionLabel
3673 * @see android.view.inputmethod.EditorInfo
3674 */
3675 public CharSequence getImeActionLabel() {
3676 return mInputContentType != null
3677 ? mInputContentType.imeActionLabel : null;
3678 }
3679
3680 /**
3681 * Get the IME action ID previous set with {@link #setImeActionLabel}.
3682 *
3683 * @see #setImeActionLabel
3684 * @see android.view.inputmethod.EditorInfo
3685 */
3686 public int getImeActionId() {
3687 return mInputContentType != null
3688 ? mInputContentType.imeActionId : 0;
3689 }
3690
3691 /**
3692 * Set a special listener to be called when an action is performed
3693 * on the text view. This will be called when the enter key is pressed,
3694 * or when an action supplied to the IME is selected by the user. Setting
3695 * this means that the normal hard key event will not insert a newline
3696 * into the text view, even if it is multi-line; holding down the ALT
3697 * modifier will, however, allow the user to insert a newline character.
3698 */
3699 public void setOnEditorActionListener(OnEditorActionListener l) {
3700 if (mInputContentType == null) {
3701 mInputContentType = new InputContentType();
3702 }
3703 mInputContentType.onEditorActionListener = l;
3704 }
3705
3706 /**
3707 * Called when an attached input method calls
3708 * {@link InputConnection#performEditorAction(int)
3709 * InputConnection.performEditorAction()}
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003710 * for this text view. The default implementation will call your action
3711 * listener supplied to {@link #setOnEditorActionListener}, or perform
3712 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003713 * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3714 * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003715 * EditorInfo.IME_ACTION_DONE}.
3716 *
3717 * <p>For backwards compatibility, if no IME options have been set and the
3718 * text view would not normally advance focus on enter, then
3719 * the NEXT and DONE actions received here will be turned into an enter
3720 * key down/up pair to go through the normal key handling.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003721 *
3722 * @param actionCode The code of the action being performed.
3723 *
3724 * @see #setOnEditorActionListener
3725 */
3726 public void onEditorAction(int actionCode) {
3727 final InputContentType ict = mInputContentType;
3728 if (ict != null) {
3729 if (ict.onEditorActionListener != null) {
3730 if (ict.onEditorActionListener.onEditorAction(this,
3731 actionCode, null)) {
3732 return;
3733 }
3734 }
The Android Open Source Project10592532009-03-18 17:39:46 -07003735
The Android Open Source Project4df24232009-03-05 14:34:35 -08003736 // This is the handling for some default action.
3737 // Note that for backwards compatibility we don't do this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003738 // default handling if explicit ime options have not been given,
The Android Open Source Project10592532009-03-18 17:39:46 -07003739 // instead turning this into the normal enter key codes that an
The Android Open Source Project4df24232009-03-05 14:34:35 -08003740 // app may be expecting.
3741 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003742 View v = focusSearch(FOCUS_FORWARD);
The Android Open Source Project4df24232009-03-05 14:34:35 -08003743 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003744 if (!v.requestFocus(FOCUS_FORWARD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003745 throw new IllegalStateException("focus search returned a view " +
3746 "that wasn't able to take focus!");
3747 }
3748 }
3749 return;
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003750
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003751 } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003752 View v = focusSearch(FOCUS_BACKWARD);
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003753 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003754 if (!v.requestFocus(FOCUS_BACKWARD)) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003755 throw new IllegalStateException("focus search returned a view " +
3756 "that wasn't able to take focus!");
3757 }
3758 }
3759 return;
3760
The Android Open Source Project4df24232009-03-05 14:34:35 -08003761 } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3762 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08003763 if (imm != null && imm.isActive(this)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003764 imm.hideSoftInputFromWindow(getWindowToken(), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003765 }
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003766 clearFocus();
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003767 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003768 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003769 }
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003770
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003771 Handler h = getHandler();
The Android Open Source Project10592532009-03-18 17:39:46 -07003772 if (h != null) {
3773 long eventTime = SystemClock.uptimeMillis();
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003774 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003775 new KeyEvent(eventTime, eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003776 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3777 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003778 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3779 | KeyEvent.FLAG_EDITOR_ACTION)));
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003780 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003781 new KeyEvent(SystemClock.uptimeMillis(), eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003782 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3783 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003784 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3785 | KeyEvent.FLAG_EDITOR_ACTION)));
3786 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003787 }
3788
3789 /**
3790 * Set the private content type of the text, which is the
3791 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3792 * field that will be filled in when creating an input connection.
3793 *
3794 * @see #getPrivateImeOptions()
3795 * @see EditorInfo#privateImeOptions
3796 * @attr ref android.R.styleable#TextView_privateImeOptions
3797 */
3798 public void setPrivateImeOptions(String type) {
3799 if (mInputContentType == null) mInputContentType = new InputContentType();
3800 mInputContentType.privateImeOptions = type;
3801 }
3802
3803 /**
3804 * Get the private type of the content.
3805 *
3806 * @see #setPrivateImeOptions(String)
3807 * @see EditorInfo#privateImeOptions
3808 */
3809 public String getPrivateImeOptions() {
3810 return mInputContentType != null
3811 ? mInputContentType.privateImeOptions : null;
3812 }
3813
3814 /**
3815 * Set the extra input data of the text, which is the
3816 * {@link EditorInfo#extras TextBoxAttribute.extras}
3817 * Bundle that will be filled in when creating an input connection. The
3818 * given integer is the resource ID of an XML resource holding an
3819 * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3820 *
3821 * @see #getInputExtras(boolean)
3822 * @see EditorInfo#extras
3823 * @attr ref android.R.styleable#TextView_editorExtras
3824 */
3825 public void setInputExtras(int xmlResId)
3826 throws XmlPullParserException, IOException {
3827 XmlResourceParser parser = getResources().getXml(xmlResId);
3828 if (mInputContentType == null) mInputContentType = new InputContentType();
3829 mInputContentType.extras = new Bundle();
3830 getResources().parseBundleExtras(parser, mInputContentType.extras);
3831 }
3832
3833 /**
3834 * Retrieve the input extras currently associated with the text view, which
3835 * can be viewed as well as modified.
3836 *
3837 * @param create If true, the extras will be created if they don't already
3838 * exist. Otherwise, null will be returned if none have been created.
Gilles Debunnee15b3582010-06-16 15:17:21 -07003839 * @see #setInputExtras(int)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003840 * @see EditorInfo#extras
3841 * @attr ref android.R.styleable#TextView_editorExtras
3842 */
3843 public Bundle getInputExtras(boolean create) {
3844 if (mInputContentType == null) {
3845 if (!create) return null;
3846 mInputContentType = new InputContentType();
3847 }
3848 if (mInputContentType.extras == null) {
3849 if (!create) return null;
3850 mInputContentType.extras = new Bundle();
3851 }
3852 return mInputContentType.extras;
3853 }
3854
3855 /**
3856 * Returns the error message that was set to be displayed with
3857 * {@link #setError}, or <code>null</code> if no error was set
3858 * or if it the error was cleared by the widget after user input.
3859 */
3860 public CharSequence getError() {
3861 return mError;
3862 }
3863
3864 /**
3865 * Sets the right-hand compound drawable of the TextView to the "error"
3866 * icon and sets an error message that will be displayed in a popup when
3867 * the TextView has focus. The icon and error message will be reset to
3868 * null when any key events cause changes to the TextView's text. If the
3869 * <code>error</code> is <code>null</code>, the error message and icon
3870 * will be cleared.
3871 */
3872 @android.view.RemotableViewMethod
3873 public void setError(CharSequence error) {
3874 if (error == null) {
3875 setError(null, null);
3876 } else {
3877 Drawable dr = getContext().getResources().
Gilles Debunnea85467b2011-01-19 16:53:31 -08003878 getDrawable(com.android.internal.R.drawable.indicator_input_error);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003879
3880 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3881 setError(error, dr);
3882 }
3883 }
3884
3885 /**
3886 * Sets the right-hand compound drawable of the TextView to the specified
3887 * icon and sets an error message that will be displayed in a popup when
3888 * the TextView has focus. The icon and error message will be reset to
3889 * null when any key events cause changes to the TextView's text. The
3890 * drawable must already have had {@link Drawable#setBounds} set on it.
3891 * If the <code>error</code> is <code>null</code>, the error message will
3892 * be cleared (and you should provide a <code>null</code> icon as well).
3893 */
3894 public void setError(CharSequence error, Drawable icon) {
3895 error = TextUtils.stringOrSpannedString(error);
3896
3897 mError = error;
3898 mErrorWasChanged = true;
3899 final Drawables dr = mDrawables;
3900 if (dr != null) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07003901 switch (getResolvedLayoutDirection()) {
3902 default:
3903 case LAYOUT_DIRECTION_LTR:
3904 setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
3905 dr.mDrawableBottom);
3906 break;
3907 case LAYOUT_DIRECTION_RTL:
3908 setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
3909 dr.mDrawableBottom);
3910 break;
3911 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003912 } else {
3913 setCompoundDrawables(null, null, icon, null);
3914 }
3915
3916 if (error == null) {
3917 if (mPopup != null) {
3918 if (mPopup.isShowing()) {
3919 mPopup.dismiss();
3920 }
3921
3922 mPopup = null;
3923 }
3924 } else {
3925 if (isFocused()) {
3926 showError();
3927 }
3928 }
3929 }
3930
3931 private void showError() {
3932 if (getWindowToken() == null) {
3933 mShowErrorAfterAttach = true;
3934 return;
3935 }
3936
3937 if (mPopup == null) {
3938 LayoutInflater inflater = LayoutInflater.from(getContext());
Gilles Debunnea85467b2011-01-19 16:53:31 -08003939 final TextView err = (TextView) inflater.inflate(
3940 com.android.internal.R.layout.textview_hint, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003941
Romain Guy9bc9fa12009-07-21 16:57:29 -07003942 final float scale = getResources().getDisplayMetrics().density;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003943 mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003944 mPopup.setFocusable(false);
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -07003945 // The user is entering text, so the input method is needed. We
3946 // don't want the popup to be displayed on top of it.
3947 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003948 }
3949
3950 TextView tv = (TextView) mPopup.getContentView();
3951 chooseSize(mPopup, mError, tv);
3952 tv.setText(mError);
3953
3954 mPopup.showAsDropDown(this, getErrorX(), getErrorY());
The Android Open Source Project10592532009-03-18 17:39:46 -07003955 mPopup.fixDirection(mPopup.isAboveAnchor());
3956 }
3957
3958 private static class ErrorPopup extends PopupWindow {
3959 private boolean mAbove = false;
Gilles Debunnee15b3582010-06-16 15:17:21 -07003960 private final TextView mView;
Gilles Debunne5f059e42011-01-12 17:49:12 -08003961 private int mPopupInlineErrorBackgroundId = 0;
3962 private int mPopupInlineErrorAboveBackgroundId = 0;
The Android Open Source Project10592532009-03-18 17:39:46 -07003963
3964 ErrorPopup(TextView v, int width, int height) {
3965 super(v, width, height);
3966 mView = v;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003967 // Make sure the TextView has a background set as it will be used the first time it is
3968 // shown and positionned. Initialized with below background, which should have
3969 // dimensions identical to the above version for this to work (and is more likely).
3970 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3971 com.android.internal.R.styleable.Theme_errorMessageBackground);
3972 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
The Android Open Source Project10592532009-03-18 17:39:46 -07003973 }
3974
3975 void fixDirection(boolean above) {
3976 mAbove = above;
3977
3978 if (above) {
Gilles Debunne5f059e42011-01-12 17:49:12 -08003979 mPopupInlineErrorAboveBackgroundId =
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003980 getResourceId(mPopupInlineErrorAboveBackgroundId,
3981 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07003982 } else {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003983 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3984 com.android.internal.R.styleable.Theme_errorMessageBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07003985 }
Gilles Debunne5f059e42011-01-12 17:49:12 -08003986
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003987 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3988 mPopupInlineErrorBackgroundId);
Gilles Debunne5f059e42011-01-12 17:49:12 -08003989 }
3990
3991 private int getResourceId(int currentId, int index) {
3992 if (currentId == 0) {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003993 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3994 R.styleable.Theme);
Gilles Debunne5f059e42011-01-12 17:49:12 -08003995 currentId = styledAttributes.getResourceId(index, 0);
3996 styledAttributes.recycle();
3997 }
3998 return currentId;
The Android Open Source Project10592532009-03-18 17:39:46 -07003999 }
4000
4001 @Override
4002 public void update(int x, int y, int w, int h, boolean force) {
4003 super.update(x, y, w, h, force);
4004
4005 boolean above = isAboveAnchor();
4006 if (above != mAbove) {
4007 fixDirection(above);
4008 }
4009 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004010 }
4011
4012 /**
4013 * Returns the Y offset to make the pointy top of the error point
4014 * at the middle of the error icon.
4015 */
4016 private int getErrorX() {
4017 /*
4018 * The "25" is the distance between the point and the right edge
4019 * of the background
4020 */
Romain Guy9bc9fa12009-07-21 16:57:29 -07004021 final float scale = getResources().getDisplayMetrics().density;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004022
4023 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004024 return getWidth() - mPopup.getWidth() - getPaddingRight() -
4025 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004026 }
4027
4028 /**
4029 * Returns the Y offset to make the pointy top of the error point
4030 * at the bottom of the error icon.
4031 */
4032 private int getErrorY() {
4033 /*
4034 * Compound, not extended, because the icon is not clipped
4035 * if the text height is smaller.
4036 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004037 final int compoundPaddingTop = getCompoundPaddingTop();
4038 int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004039
4040 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004041 int icontop = compoundPaddingTop +
4042 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004043
4044 /*
4045 * The "2" is the distance between the point and the top edge
4046 * of the background.
4047 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004048 final float scale = getResources().getDisplayMetrics().density;
4049 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
4050 (int) (2 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004051 }
4052
4053 private void hideError() {
4054 if (mPopup != null) {
4055 if (mPopup.isShowing()) {
4056 mPopup.dismiss();
4057 }
4058 }
4059
4060 mShowErrorAfterAttach = false;
4061 }
4062
4063 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
4064 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
4065 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
4066
Fabrice Di Meglioe4231462011-09-08 18:15:50 -07004067 int defaultWidthInPixels = getResources().getDimensionPixelSize(
4068 com.android.internal.R.dimen.textview_error_popup_default_width);
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004069 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004070 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
4071 float max = 0;
4072 for (int i = 0; i < l.getLineCount(); i++) {
4073 max = Math.max(max, l.getLineWidth(i));
4074 }
4075
4076 /*
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004077 * Now set the popup size to be big enough for the text plus the border capped
4078 * to DEFAULT_MAX_POPUP_WIDTH
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004079 */
4080 pop.setWidth(wid + (int) Math.ceil(max));
4081 pop.setHeight(ht + l.getHeight());
4082 }
4083
4084
4085 @Override
4086 protected boolean setFrame(int l, int t, int r, int b) {
4087 boolean result = super.setFrame(l, t, r, b);
4088
4089 if (mPopup != null) {
4090 TextView tv = (TextView) mPopup.getContentView();
4091 chooseSize(mPopup, mError, tv);
Eric Fischerfa0d2532009-09-17 17:01:59 -07004092 mPopup.update(this, getErrorX(), getErrorY(),
4093 mPopup.getWidth(), mPopup.getHeight());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004094 }
4095
Romain Guy986003d2009-03-25 17:42:35 -07004096 restartMarqueeIfNeeded();
4097
4098 return result;
4099 }
4100
4101 private void restartMarqueeIfNeeded() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004102 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4103 mRestartMarquee = false;
4104 startMarquee();
4105 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004106 }
4107
4108 /**
4109 * Sets the list of input filters that will be used if the buffer is
4110 * Editable. Has no effect otherwise.
4111 *
4112 * @attr ref android.R.styleable#TextView_maxLength
4113 */
4114 public void setFilters(InputFilter[] filters) {
4115 if (filters == null) {
4116 throw new IllegalArgumentException();
4117 }
4118
4119 mFilters = filters;
4120
4121 if (mText instanceof Editable) {
4122 setFilters((Editable) mText, filters);
4123 }
4124 }
4125
4126 /**
4127 * Sets the list of input filters on the specified Editable,
4128 * and includes mInput in the list if it is an InputFilter.
4129 */
4130 private void setFilters(Editable e, InputFilter[] filters) {
4131 if (mInput instanceof InputFilter) {
4132 InputFilter[] nf = new InputFilter[filters.length + 1];
4133
4134 System.arraycopy(filters, 0, nf, 0, filters.length);
4135 nf[filters.length] = (InputFilter) mInput;
4136
4137 e.setFilters(nf);
4138 } else {
4139 e.setFilters(filters);
4140 }
4141 }
4142
4143 /**
4144 * Returns the current list of input filters.
4145 */
4146 public InputFilter[] getFilters() {
4147 return mFilters;
4148 }
4149
4150 /////////////////////////////////////////////////////////////////////////
4151
4152 private int getVerticalOffset(boolean forceNormal) {
4153 int voffset = 0;
4154 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4155
4156 Layout l = mLayout;
4157 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4158 l = mHintLayout;
4159 }
4160
4161 if (gravity != Gravity.TOP) {
4162 int boxht;
4163
4164 if (l == mHintLayout) {
4165 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4166 getCompoundPaddingBottom();
4167 } else {
4168 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4169 getExtendedPaddingBottom();
4170 }
4171 int textht = l.getHeight();
4172
4173 if (textht < boxht) {
4174 if (gravity == Gravity.BOTTOM)
4175 voffset = boxht - textht;
4176 else // (gravity == Gravity.CENTER_VERTICAL)
4177 voffset = (boxht - textht) >> 1;
4178 }
4179 }
4180 return voffset;
4181 }
4182
4183 private int getBottomVerticalOffset(boolean forceNormal) {
4184 int voffset = 0;
4185 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4186
4187 Layout l = mLayout;
4188 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4189 l = mHintLayout;
4190 }
4191
4192 if (gravity != Gravity.BOTTOM) {
4193 int boxht;
4194
4195 if (l == mHintLayout) {
4196 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4197 getCompoundPaddingBottom();
4198 } else {
4199 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4200 getExtendedPaddingBottom();
4201 }
4202 int textht = l.getHeight();
4203
4204 if (textht < boxht) {
4205 if (gravity == Gravity.TOP)
4206 voffset = boxht - textht;
4207 else // (gravity == Gravity.CENTER_VERTICAL)
4208 voffset = (boxht - textht) >> 1;
4209 }
4210 }
4211 return voffset;
4212 }
4213
4214 private void invalidateCursorPath() {
4215 if (mHighlightPathBogus) {
4216 invalidateCursor();
4217 } else {
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004218 final int horizontalPadding = getCompoundPaddingLeft();
4219 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004220
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004221 if (mCursorCount == 0) {
4222 synchronized (sTempRect) {
4223 /*
4224 * The reason for this concern about the thickness of the
4225 * cursor and doing the floor/ceil on the coordinates is that
4226 * some EditTexts (notably textfields in the Browser) have
4227 * anti-aliased text where not all the characters are
4228 * necessarily at integer-multiple locations. This should
4229 * make sure the entire cursor gets invalidated instead of
4230 * sometimes missing half a pixel.
4231 */
4232 float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
4233 if (thick < 1.0f) {
4234 thick = 1.0f;
4235 }
4236
4237 thick /= 2.0f;
4238
4239 mHighlightPath.computeBounds(sTempRect, false);
4240
4241 invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
4242 (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
4243 (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
4244 (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004245 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004246 } else {
4247 for (int i = 0; i < mCursorCount; i++) {
4248 Rect bounds = mCursorDrawable[i].getBounds();
4249 invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
4250 bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
4251 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004252 }
4253 }
4254 }
4255
4256 private void invalidateCursor() {
Gilles Debunne05336272010-07-09 20:13:45 -07004257 int where = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004258
4259 invalidateCursor(where, where, where);
4260 }
4261
4262 private void invalidateCursor(int a, int b, int c) {
4263 if (mLayout == null) {
4264 invalidate();
4265 } else {
4266 if (a >= 0 || b >= 0 || c >= 0) {
4267 int first = Math.min(Math.min(a, b), c);
4268 int last = Math.max(Math.max(a, b), c);
4269
4270 int line = mLayout.getLineForOffset(first);
4271 int top = mLayout.getLineTop(line);
4272
4273 // This is ridiculous, but the descent from the line above
4274 // can hang down into the line we really want to redraw,
4275 // so we have to invalidate part of the line above to make
4276 // sure everything that needs to be redrawn really is.
4277 // (But not the whole line above, because that would cause
4278 // the same problem with the descenders on the line above it!)
4279 if (line > 0) {
4280 top -= mLayout.getLineDescent(line - 1);
4281 }
4282
4283 int line2;
4284
4285 if (first == last)
4286 line2 = line;
4287 else
4288 line2 = mLayout.getLineForOffset(last);
4289
4290 int bottom = mLayout.getLineTop(line2 + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004291
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004292 final int horizontalPadding = getCompoundPaddingLeft();
4293 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
4294
4295 // If used, the cursor drawables can have an arbitrary dimension that can go beyond
4296 // the invalidated lines specified above.
4297 for (int i = 0; i < mCursorCount; i++) {
4298 Rect bounds = mCursorDrawable[i].getBounds();
4299 top = Math.min(top, bounds.top);
4300 bottom = Math.max(bottom, bounds.bottom);
4301 // Horizontal bounds are already full width, no need to update
4302 }
4303
4304 invalidate(horizontalPadding + mScrollX, top + verticalPadding,
4305 horizontalPadding + mScrollX + getWidth() -
4306 getCompoundPaddingLeft() - getCompoundPaddingRight(),
4307 bottom + verticalPadding);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004308 }
4309 }
4310 }
4311
4312 private void registerForPreDraw() {
4313 final ViewTreeObserver observer = getViewTreeObserver();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004314
4315 if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
4316 observer.addOnPreDrawListener(this);
4317 mPreDrawState = PREDRAW_PENDING;
4318 } else if (mPreDrawState == PREDRAW_DONE) {
4319 mPreDrawState = PREDRAW_PENDING;
4320 }
4321
4322 // else state is PREDRAW_PENDING, so keep waiting.
4323 }
4324
4325 /**
4326 * {@inheritDoc}
4327 */
4328 public boolean onPreDraw() {
4329 if (mPreDrawState != PREDRAW_PENDING) {
4330 return true;
4331 }
4332
4333 if (mLayout == null) {
4334 assumeLayout();
4335 }
4336
4337 boolean changed = false;
4338
4339 if (mMovement != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07004340 /* This code also provides auto-scrolling when a cursor is moved using a
4341 * CursorController (insertion point or selection limits).
4342 * For selection, ensure start or end is visible depending on controller's state.
4343 */
4344 int curs = getSelectionEnd();
Gilles Debunnee587d832010-11-23 20:20:11 -08004345 // Do not create the controller if it is not already created.
4346 if (mSelectionModifierCursorController != null &&
4347 mSelectionModifierCursorController.isSelectionStartDragged()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07004348 curs = getSelectionStart();
Gilles Debunne05336272010-07-09 20:13:45 -07004349 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004350
4351 /*
4352 * TODO: This should really only keep the end in view if
4353 * it already was before the text changed. I'm not sure
4354 * of a good way to tell from here if it was.
4355 */
4356 if (curs < 0 &&
4357 (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
4358 curs = mText.length();
4359 }
4360
4361 if (curs >= 0) {
4362 changed = bringPointIntoView(curs);
4363 }
4364 } else {
4365 changed = bringTextIntoView();
4366 }
4367
Gilles Debunne64e54a62010-09-07 19:07:17 -07004368 // This has to be checked here since:
4369 // - onFocusChanged cannot start it when focus is given to a view with selected text (after
4370 // a screen rotation) since layout is not yet initialized at that point.
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08004371 if (mCreatedWithASelection) {
4372 startSelectionActionMode();
4373 mCreatedWithASelection = false;
4374 }
4375
4376 // Phone specific code (there is no ExtractEditText on tablets).
4377 // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
4378 // not be set. Do the test here instead.
4379 if (this instanceof ExtractEditText && hasSelection()) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004380 startSelectionActionMode();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004381 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07004382
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004383 mPreDrawState = PREDRAW_DONE;
4384 return !changed;
4385 }
4386
4387 @Override
4388 protected void onAttachedToWindow() {
4389 super.onAttachedToWindow();
4390
4391 mTemporaryDetach = false;
4392
4393 if (mShowErrorAfterAttach) {
4394 showError();
4395 mShowErrorAfterAttach = false;
4396 }
Adam Powell624380a2010-10-02 18:12:02 -07004397
4398 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004399 // No need to create the controller.
4400 // The get method will add the listener on controller creation.
4401 if (mInsertionPointCursorController != null) {
4402 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4403 }
4404 if (mSelectionModifierCursorController != null) {
4405 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell624380a2010-10-02 18:12:02 -07004406 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004407
4408 // Resolve drawables as the layout direction has been resolved
4409 resolveDrawables();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004410 }
4411
4412 @Override
4413 protected void onDetachedFromWindow() {
4414 super.onDetachedFromWindow();
4415
Adam Powell624380a2010-10-02 18:12:02 -07004416 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004417 if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4418 observer.removeOnPreDrawListener(this);
4419 mPreDrawState = PREDRAW_NOT_REGISTERED;
4420 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004421
4422 if (mError != null) {
4423 hideError();
4424 }
Adam Powellba0a2c32010-09-28 17:41:23 -07004425
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004426 if (mBlink != null) {
Gilles Debunne3d010062011-02-18 14:16:41 -08004427 mBlink.removeCallbacks(mBlink);
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004428 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08004429
4430 if (mInsertionPointCursorController != null) {
4431 mInsertionPointCursorController.onDetached();
4432 }
4433
4434 if (mSelectionModifierCursorController != null) {
4435 mSelectionModifierCursorController.onDetached();
4436 }
4437
Adam Powellba0a2c32010-09-28 17:41:23 -07004438 hideControllers();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004439
4440 resetResolvedDrawables();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004441 }
4442
4443 @Override
4444 protected boolean isPaddingOffsetRequired() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004445 return mShadowRadius != 0 || mDrawables != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004446 }
4447
4448 @Override
4449 protected int getLeftPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004450 return getCompoundPaddingLeft() - mPaddingLeft +
4451 (int) Math.min(0, mShadowDx - mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004452 }
4453
4454 @Override
4455 protected int getTopPaddingOffset() {
4456 return (int) Math.min(0, mShadowDy - mShadowRadius);
4457 }
4458
4459 @Override
4460 protected int getBottomPaddingOffset() {
4461 return (int) Math.max(0, mShadowDy + mShadowRadius);
4462 }
4463
4464 @Override
4465 protected int getRightPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004466 return -(getCompoundPaddingRight() - mPaddingRight) +
4467 (int) Math.max(0, mShadowDx + mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004468 }
4469
4470 @Override
4471 protected boolean verifyDrawable(Drawable who) {
4472 final boolean verified = super.verifyDrawable(who);
4473 if (!verified && mDrawables != null) {
4474 return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004475 who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom ||
4476 who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004477 }
4478 return verified;
4479 }
4480
4481 @Override
Dianne Hackborne2136772010-11-04 15:08:59 -07004482 public void jumpDrawablesToCurrentState() {
4483 super.jumpDrawablesToCurrentState();
4484 if (mDrawables != null) {
4485 if (mDrawables.mDrawableLeft != null) {
4486 mDrawables.mDrawableLeft.jumpToCurrentState();
4487 }
4488 if (mDrawables.mDrawableTop != null) {
4489 mDrawables.mDrawableTop.jumpToCurrentState();
4490 }
4491 if (mDrawables.mDrawableRight != null) {
4492 mDrawables.mDrawableRight.jumpToCurrentState();
4493 }
4494 if (mDrawables.mDrawableBottom != null) {
4495 mDrawables.mDrawableBottom.jumpToCurrentState();
4496 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004497 if (mDrawables.mDrawableStart != null) {
4498 mDrawables.mDrawableStart.jumpToCurrentState();
4499 }
4500 if (mDrawables.mDrawableEnd != null) {
4501 mDrawables.mDrawableEnd.jumpToCurrentState();
4502 }
Dianne Hackborne2136772010-11-04 15:08:59 -07004503 }
4504 }
4505
4506 @Override
Romain Guy3c77d392009-05-20 11:26:50 -07004507 public void invalidateDrawable(Drawable drawable) {
4508 if (verifyDrawable(drawable)) {
4509 final Rect dirty = drawable.getBounds();
4510 int scrollX = mScrollX;
4511 int scrollY = mScrollY;
4512
4513 // IMPORTANT: The coordinates below are based on the coordinates computed
4514 // for each compound drawable in onDraw(). Make sure to update each section
4515 // accordingly.
4516 final TextView.Drawables drawables = mDrawables;
Romain Guya6cd4e02009-05-20 15:09:21 -07004517 if (drawables != null) {
4518 if (drawable == drawables.mDrawableLeft) {
4519 final int compoundPaddingTop = getCompoundPaddingTop();
4520 final int compoundPaddingBottom = getCompoundPaddingBottom();
4521 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004522
Romain Guya6cd4e02009-05-20 15:09:21 -07004523 scrollX += mPaddingLeft;
4524 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4525 } else if (drawable == drawables.mDrawableRight) {
4526 final int compoundPaddingTop = getCompoundPaddingTop();
4527 final int compoundPaddingBottom = getCompoundPaddingBottom();
4528 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004529
Romain Guya6cd4e02009-05-20 15:09:21 -07004530 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4531 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4532 } else if (drawable == drawables.mDrawableTop) {
4533 final int compoundPaddingLeft = getCompoundPaddingLeft();
4534 final int compoundPaddingRight = getCompoundPaddingRight();
4535 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004536
Romain Guya6cd4e02009-05-20 15:09:21 -07004537 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4538 scrollY += mPaddingTop;
4539 } else if (drawable == drawables.mDrawableBottom) {
4540 final int compoundPaddingLeft = getCompoundPaddingLeft();
4541 final int compoundPaddingRight = getCompoundPaddingRight();
4542 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004543
Romain Guya6cd4e02009-05-20 15:09:21 -07004544 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4545 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4546 }
Romain Guy3c77d392009-05-20 11:26:50 -07004547 }
4548
4549 invalidate(dirty.left + scrollX, dirty.top + scrollY,
4550 dirty.right + scrollX, dirty.bottom + scrollY);
4551 }
4552 }
4553
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004554 /**
4555 * @hide
4556 */
Romain Guy3c77d392009-05-20 11:26:50 -07004557 @Override
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004558 public int getResolvedLayoutDirection(Drawable who) {
4559 if (who == null) return View.LAYOUT_DIRECTION_LTR;
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004560 if (mDrawables != null) {
4561 final Drawables drawables = mDrawables;
4562 if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004563 who == drawables.mDrawableTop || who == drawables.mDrawableBottom ||
4564 who == drawables.mDrawableStart || who == drawables.mDrawableEnd) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004565 return getResolvedLayoutDirection();
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004566 }
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004567 }
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004568 return super.getResolvedLayoutDirection(who);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004569 }
4570
4571 @Override
Romain Guyc4d8eb62010-08-18 20:48:33 -07004572 protected boolean onSetAlpha(int alpha) {
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004573 // Alpha is supported if and only if the drawing can be done in one pass.
4574 // TODO text with spans with a background color currently do not respect this alpha.
4575 if (getBackground() == null) {
Romain Guyc4d8eb62010-08-18 20:48:33 -07004576 mCurrentAlpha = alpha;
4577 final Drawables dr = mDrawables;
4578 if (dr != null) {
Michael Jurka406f0522010-09-15 18:48:48 -07004579 if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4580 if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4581 if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4582 if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004583 if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha);
4584 if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha);
Romain Guyc4d8eb62010-08-18 20:48:33 -07004585 }
4586 return true;
4587 }
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004588
4589 mCurrentAlpha = 255;
Romain Guyc4d8eb62010-08-18 20:48:33 -07004590 return false;
4591 }
4592
Gilles Debunne86b9c782010-11-11 10:43:48 -08004593 /**
4594 * When a TextView is used to display a useful piece of information to the user (such as a
4595 * contact's address), it should be made selectable, so that the user can select and copy this
4596 * content.
4597 *
4598 * Use {@link #setTextIsSelectable(boolean)} or the
4599 * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
Gilles Debunnee12f9992010-12-17 11:04:55 -08004600 * selectable (text is not selectable by default).
Gilles Debunne6f100f32010-12-13 18:04:20 -08004601 *
Gilles Debunnebb588da2011-07-11 18:26:19 -07004602 * Note that this method simply returns the state of this flag. Although this flag has to be set
4603 * in order to select text in non-editable TextView, the content of an {@link EditText} can
4604 * always be selected, independently of the value of this flag.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004605 *
4606 * @return True if the text displayed in this TextView can be selected by the user.
4607 *
4608 * @attr ref android.R.styleable#TextView_textIsSelectable
4609 */
4610 public boolean isTextSelectable() {
4611 return mTextIsSelectable;
4612 }
4613
4614 /**
4615 * Sets whether or not (default) the content of this view is selectable by the user.
Gilles Debunne6f100f32010-12-13 18:04:20 -08004616 *
Gilles Debunnee12f9992010-12-17 11:04:55 -08004617 * Note that this methods affect the {@link #setFocusable(boolean)},
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004618 * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4619 * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4620 * customized.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004621 *
4622 * See {@link #isTextSelectable} for details.
4623 *
4624 * @param selectable Whether or not the content of this TextView should be selectable.
4625 */
4626 public void setTextIsSelectable(boolean selectable) {
4627 if (mTextIsSelectable == selectable) return;
4628
4629 mTextIsSelectable = selectable;
4630
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004631 setFocusableInTouchMode(selectable);
Gilles Debunne86b9c782010-11-11 10:43:48 -08004632 setFocusable(selectable);
4633 setClickable(selectable);
4634 setLongClickable(selectable);
4635
4636 // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4637
4638 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4639 setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4640
4641 // Called by setText above, but safer in case of future code changes
4642 prepareCursorControllers();
4643 }
4644
4645 @Override
4646 protected int[] onCreateDrawableState(int extraSpace) {
Gilles Debunnefb817032011-01-13 13:52:49 -08004647 final int[] drawableState;
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004648
Gilles Debunnefb817032011-01-13 13:52:49 -08004649 if (mSingleLine) {
4650 drawableState = super.onCreateDrawableState(extraSpace);
4651 } else {
4652 drawableState = super.onCreateDrawableState(extraSpace + 1);
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004653 mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4654 }
4655
Gilles Debunne86b9c782010-11-11 10:43:48 -08004656 if (mTextIsSelectable) {
4657 // Disable pressed state, which was introduced when TextView was made clickable.
4658 // Prevents text color change.
4659 // setClickable(false) would have a similar effect, but it also disables focus changes
4660 // and long press actions, which are both needed by text selection.
4661 final int length = drawableState.length;
4662 for (int i = 0; i < length; i++) {
4663 if (drawableState[i] == R.attr.state_pressed) {
4664 final int[] nonPressedState = new int[length - 1];
4665 System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4666 System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4667 return nonPressedState;
4668 }
4669 }
4670 }
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004671
Gilles Debunne86b9c782010-11-11 10:43:48 -08004672 return drawableState;
4673 }
4674
Romain Guyc4d8eb62010-08-18 20:48:33 -07004675 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004676 protected void onDraw(Canvas canvas) {
Michael Jurka2b942d52011-03-01 13:26:11 -08004677 if (mPreDrawState == PREDRAW_DONE) {
4678 final ViewTreeObserver observer = getViewTreeObserver();
4679 observer.removeOnPreDrawListener(this);
4680 mPreDrawState = PREDRAW_NOT_REGISTERED;
4681 }
4682
Romain Guy909cbaf2010-10-13 18:19:48 -07004683 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4684
Romain Guy986003d2009-03-25 17:42:35 -07004685 restartMarqueeIfNeeded();
4686
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004687 // Draw the background for this view
4688 super.onDraw(canvas);
4689
4690 final int compoundPaddingLeft = getCompoundPaddingLeft();
4691 final int compoundPaddingTop = getCompoundPaddingTop();
4692 final int compoundPaddingRight = getCompoundPaddingRight();
4693 final int compoundPaddingBottom = getCompoundPaddingBottom();
4694 final int scrollX = mScrollX;
4695 final int scrollY = mScrollY;
4696 final int right = mRight;
4697 final int left = mLeft;
4698 final int bottom = mBottom;
4699 final int top = mTop;
4700
4701 final Drawables dr = mDrawables;
4702 if (dr != null) {
4703 /*
4704 * Compound, not extended, because the icon is not clipped
4705 * if the text height is smaller.
4706 */
4707
4708 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4709 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4710
Romain Guy3c77d392009-05-20 11:26:50 -07004711 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4712 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004713 if (dr.mDrawableLeft != null) {
4714 canvas.save();
4715 canvas.translate(scrollX + mPaddingLeft,
4716 scrollY + compoundPaddingTop +
4717 (vspace - dr.mDrawableHeightLeft) / 2);
4718 dr.mDrawableLeft.draw(canvas);
4719 canvas.restore();
4720 }
4721
Romain Guy3c77d392009-05-20 11:26:50 -07004722 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4723 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004724 if (dr.mDrawableRight != null) {
4725 canvas.save();
4726 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4727 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4728 dr.mDrawableRight.draw(canvas);
4729 canvas.restore();
4730 }
4731
Romain Guy3c77d392009-05-20 11:26:50 -07004732 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4733 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004734 if (dr.mDrawableTop != null) {
4735 canvas.save();
4736 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4737 scrollY + mPaddingTop);
4738 dr.mDrawableTop.draw(canvas);
4739 canvas.restore();
4740 }
4741
Romain Guy3c77d392009-05-20 11:26:50 -07004742 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4743 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004744 if (dr.mDrawableBottom != null) {
4745 canvas.save();
4746 canvas.translate(scrollX + compoundPaddingLeft +
4747 (hspace - dr.mDrawableWidthBottom) / 2,
4748 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4749 dr.mDrawableBottom.draw(canvas);
4750 canvas.restore();
4751 }
4752 }
4753
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004754 int color = mCurTextColor;
4755
4756 if (mLayout == null) {
4757 assumeLayout();
4758 }
4759
4760 Layout layout = mLayout;
4761 int cursorcolor = color;
4762
4763 if (mHint != null && mText.length() == 0) {
4764 if (mHintTextColor != null) {
4765 color = mCurHintTextColor;
4766 }
4767
4768 layout = mHintLayout;
4769 }
4770
4771 mTextPaint.setColor(color);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004772 if (mCurrentAlpha != 255) {
4773 // If set, the alpha will override the color's alpha. Multiply the alphas.
4774 mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4775 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004776 mTextPaint.drawableState = getDrawableState();
4777
4778 canvas.save();
4779 /* Would be faster if we didn't have to do this. Can we chop the
4780 (displayable) text so that we don't need to do this ever?
4781 */
4782
4783 int extendedPaddingTop = getExtendedPaddingTop();
4784 int extendedPaddingBottom = getExtendedPaddingBottom();
4785
4786 float clipLeft = compoundPaddingLeft + scrollX;
4787 float clipTop = extendedPaddingTop + scrollY;
4788 float clipRight = right - left - compoundPaddingRight + scrollX;
4789 float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4790
4791 if (mShadowRadius != 0) {
4792 clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4793 clipRight += Math.max(0, mShadowDx + mShadowRadius);
4794
4795 clipTop += Math.min(0, mShadowDy - mShadowRadius);
4796 clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4797 }
4798
4799 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4800
4801 int voffsetText = 0;
4802 int voffsetCursor = 0;
4803
4804 // translate in by our padding
4805 {
4806 /* shortcircuit calling getVerticaOffset() */
4807 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4808 voffsetText = getVerticalOffset(false);
4809 voffsetCursor = getVerticalOffset(true);
4810 }
4811 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4812 }
4813
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004814 final int layoutDirection = getResolvedLayoutDirection();
4815 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Adam Powell282e3772011-08-30 16:51:11 -07004816 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
4817 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004818 if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004819 (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004820 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4821 getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4822 }
4823
4824 if (mMarquee != null && mMarquee.isRunning()) {
4825 canvas.translate(-mMarquee.mScroll, 0.0f);
4826 }
4827 }
4828
4829 Path highlight = null;
4830 int selStart = -1, selEnd = -1;
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004831 boolean drawCursor = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004832
4833 // If there is no movement method, then there can be no selection.
4834 // Check that first and attempt to skip everything having to do with
4835 // the cursor.
4836 // XXX This is not strictly true -- a program could set the
4837 // selection manually if it really wanted to.
4838 if (mMovement != null && (isFocused() || isPressed())) {
Gilles Debunne05336272010-07-09 20:13:45 -07004839 selStart = getSelectionStart();
4840 selEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004841
Gilles Debunnebb588da2011-07-11 18:26:19 -07004842 if (selStart >= 0) {
Gilles Debunnefd419b02011-08-25 11:53:26 -07004843 if (mHighlightPath == null) mHighlightPath = new Path();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004844
4845 if (selStart == selEnd) {
Gilles Debunnebb588da2011-07-11 18:26:19 -07004846 if (isCursorVisible() &&
Gilles Debunne86b9c782010-11-11 10:43:48 -08004847 (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004848 if (mHighlightPathBogus) {
4849 mHighlightPath.reset();
4850 mLayout.getCursorPath(selStart, mHighlightPath, mText);
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004851 updateCursorsPositions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004852 mHighlightPathBogus = false;
4853 }
4854
4855 // XXX should pass to skin instead of drawing directly
4856 mHighlightPaint.setColor(cursorcolor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004857 if (mCurrentAlpha != 255) {
4858 mHighlightPaint.setAlpha(
4859 (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4860 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004861 mHighlightPaint.setStyle(Paint.Style.STROKE);
Gilles Debunne46b7d442011-02-17 16:03:10 -08004862 highlight = mHighlightPath;
Gilles Debunneeca97a32011-02-23 17:48:28 -08004863 drawCursor = mCursorCount > 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004864 }
Gilles Debunnebb588da2011-07-11 18:26:19 -07004865 } else if (textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004866 if (mHighlightPathBogus) {
4867 mHighlightPath.reset();
4868 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4869 mHighlightPathBogus = false;
4870 }
4871
4872 // XXX should pass to skin instead of drawing directly
4873 mHighlightPaint.setColor(mHighlightColor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004874 if (mCurrentAlpha != 255) {
4875 mHighlightPaint.setAlpha(
4876 (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4877 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004878 mHighlightPaint.setStyle(Paint.Style.FILL);
4879
4880 highlight = mHighlightPath;
4881 }
4882 }
4883 }
4884
4885 /* Comment out until we decide what to do about animations
4886 boolean isLinearTextOn = false;
4887 if (currentTransformation != null) {
4888 isLinearTextOn = mTextPaint.isLinearTextOn();
4889 Matrix m = currentTransformation.getMatrix();
4890 if (!m.isIdentity()) {
4891 // mTextPaint.setLinearTextOn(true);
4892 }
4893 }
4894 */
4895
4896 final InputMethodState ims = mInputMethodState;
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004897 final int cursorOffsetVertical = voffsetCursor - voffsetText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004898 if (ims != null && ims.mBatchEditNesting == 0) {
4899 InputMethodManager imm = InputMethodManager.peekInstance();
4900 if (imm != null) {
4901 if (imm.isActive(this)) {
4902 boolean reported = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07004903 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004904 // We are in extract mode and the content has changed
4905 // in some way... just report complete new text to the
4906 // input method.
4907 reported = reportExtractedText();
4908 }
4909 if (!reported && highlight != null) {
4910 int candStart = -1;
4911 int candEnd = -1;
4912 if (mText instanceof Spannable) {
4913 Spannable sp = (Spannable)mText;
4914 candStart = EditableInputConnection.getComposingSpanStart(sp);
4915 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4916 }
4917 imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4918 }
4919 }
4920
4921 if (imm.isWatchingCursor(this) && highlight != null) {
4922 highlight.computeBounds(ims.mTmpRectF, true);
4923 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4924
4925 canvas.getMatrix().mapPoints(ims.mTmpOffset);
4926 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4927
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004928 ims.mTmpRectF.offset(0, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004929
4930 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4931 (int)(ims.mTmpRectF.top + 0.5),
4932 (int)(ims.mTmpRectF.right + 0.5),
4933 (int)(ims.mTmpRectF.bottom + 0.5));
4934
4935 imm.updateCursor(this,
4936 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4937 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4938 }
4939 }
4940 }
4941
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004942 if (mCorrectionHighlighter != null) {
4943 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
4944 }
4945
Gilles Debunne46b7d442011-02-17 16:03:10 -08004946 if (drawCursor) {
4947 drawCursor(canvas, cursorOffsetVertical);
4948 // Rely on the drawable entirely, do not draw the cursor line.
4949 // Has to be done after the IMM related code above which relies on the highlight.
4950 highlight = null;
4951 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004952
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004953 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004954
Romain Guyc2303192009-04-03 17:37:18 -07004955 if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4956 canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004957 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
Romain Guyc2303192009-04-03 17:37:18 -07004958 }
4959
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004960 /* Comment out until we decide what to do about animations
4961 if (currentTransformation != null) {
4962 mTextPaint.setLinearTextOn(isLinearTextOn);
4963 }
4964 */
4965
4966 canvas.restore();
Leon Scroggins56426252010-11-01 15:45:37 -04004967 }
4968
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004969 private void updateCursorsPositions() {
Gilles Debunneeca97a32011-02-23 17:48:28 -08004970 if (mCursorDrawableRes == 0) {
4971 mCursorCount = 0;
4972 return;
4973 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004974
4975 final int offset = getSelectionStart();
4976 final int line = mLayout.getLineForOffset(offset);
4977 final int top = mLayout.getLineTop(line);
4978 final int bottom = mLayout.getLineTop(line + 1);
4979
4980 mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
4981
4982 int middle = bottom;
4983 if (mCursorCount == 2) {
4984 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
4985 middle = (top + bottom) >> 1;
4986 }
4987
4988 updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
4989
4990 if (mCursorCount == 2) {
4991 updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
4992 }
4993 }
4994
4995 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
4996 if (mCursorDrawable[cursorIndex] == null)
4997 mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
4998
4999 if (mTempRect == null) mTempRect = new Rect();
5000
5001 mCursorDrawable[cursorIndex].getPadding(mTempRect);
5002 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
5003 horizontal = Math.max(0.5f, horizontal - 0.5f);
5004 final int left = (int) (horizontal) - mTempRect.left;
5005 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
5006 bottom + mTempRect.bottom);
5007 }
5008
5009 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
5010 final boolean translate = cursorOffsetVertical != 0;
5011 if (translate) canvas.translate(0, cursorOffsetVertical);
5012 for (int i = 0; i < mCursorCount; i++) {
5013 mCursorDrawable[i].draw(canvas);
5014 }
5015 if (translate) canvas.translate(0, -cursorOffsetVertical);
5016 }
5017
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005018 @Override
5019 public void getFocusedRect(Rect r) {
5020 if (mLayout == null) {
5021 super.getFocusedRect(r);
5022 return;
5023 }
5024
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005025 int selEnd = getSelectionEnd();
5026 if (selEnd < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005027 super.getFocusedRect(r);
5028 return;
5029 }
5030
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005031 int selStart = getSelectionStart();
5032 if (selStart < 0 || selStart >= selEnd) {
5033 int line = mLayout.getLineForOffset(selEnd);
5034 r.top = mLayout.getLineTop(line);
5035 r.bottom = mLayout.getLineBottom(line);
5036 r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
5037 r.right = r.left + 4;
5038 } else {
5039 int lineStart = mLayout.getLineForOffset(selStart);
5040 int lineEnd = mLayout.getLineForOffset(selEnd);
5041 r.top = mLayout.getLineTop(lineStart);
5042 r.bottom = mLayout.getLineBottom(lineEnd);
5043 if (lineStart == lineEnd) {
5044 r.left = (int) mLayout.getPrimaryHorizontal(selStart);
5045 r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
5046 } else {
5047 // Selection extends across multiple lines -- the focused
5048 // rect covers the entire width.
Gilles Debunnefd419b02011-08-25 11:53:26 -07005049 if (mHighlightPath == null) mHighlightPath = new Path();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005050 if (mHighlightPathBogus) {
5051 mHighlightPath.reset();
5052 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
5053 mHighlightPathBogus = false;
5054 }
5055 synchronized (sTempRect) {
5056 mHighlightPath.computeBounds(sTempRect, true);
5057 r.left = (int)sTempRect.left-1;
5058 r.right = (int)sTempRect.right+1;
5059 }
5060 }
5061 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005062
5063 // Adjust for padding and gravity.
5064 int paddingLeft = getCompoundPaddingLeft();
5065 int paddingTop = getExtendedPaddingTop();
5066 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5067 paddingTop += getVerticalOffset(false);
5068 }
5069 r.offset(paddingLeft, paddingTop);
5070 }
5071
5072 /**
5073 * Return the number of lines of text, or 0 if the internal Layout has not
5074 * been built.
5075 */
5076 public int getLineCount() {
5077 return mLayout != null ? mLayout.getLineCount() : 0;
5078 }
5079
5080 /**
5081 * Return the baseline for the specified line (0...getLineCount() - 1)
5082 * If bounds is not null, return the top, left, right, bottom extents
5083 * of the specified line in it. If the internal Layout has not been built,
5084 * return 0 and set bounds to (0, 0, 0, 0)
5085 * @param line which line to examine (0..getLineCount() - 1)
5086 * @param bounds Optional. If not null, it returns the extent of the line
5087 * @return the Y-coordinate of the baseline
5088 */
5089 public int getLineBounds(int line, Rect bounds) {
5090 if (mLayout == null) {
5091 if (bounds != null) {
5092 bounds.set(0, 0, 0, 0);
5093 }
5094 return 0;
5095 }
5096 else {
5097 int baseline = mLayout.getLineBounds(line, bounds);
5098
5099 int voffset = getExtendedPaddingTop();
5100 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5101 voffset += getVerticalOffset(true);
5102 }
5103 if (bounds != null) {
5104 bounds.offset(getCompoundPaddingLeft(), voffset);
5105 }
5106 return baseline + voffset;
5107 }
5108 }
5109
5110 @Override
5111 public int getBaseline() {
5112 if (mLayout == null) {
5113 return super.getBaseline();
5114 }
5115
5116 int voffset = 0;
5117 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5118 voffset = getVerticalOffset(true);
5119 }
5120
5121 return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
5122 }
5123
Romain Guyf2fc4602011-07-19 15:20:03 -07005124 /**
5125 * @hide
5126 * @param offsetRequired
5127 */
5128 @Override
5129 protected int getFadeTop(boolean offsetRequired) {
Romain Guy59f13c7d2011-07-19 18:35:33 -07005130 if (mLayout == null) return 0;
5131
Romain Guyf2fc4602011-07-19 15:20:03 -07005132 int voffset = 0;
5133 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5134 voffset = getVerticalOffset(true);
5135 }
5136
5137 if (offsetRequired) voffset += getTopPaddingOffset();
5138
5139 return getExtendedPaddingTop() + voffset;
5140 }
5141
5142 /**
5143 * @hide
5144 * @param offsetRequired
5145 */
Gilles Debunne3784a7f2011-07-15 13:49:38 -07005146 @Override
Romain Guyf2fc4602011-07-19 15:20:03 -07005147 protected int getFadeHeight(boolean offsetRequired) {
5148 return mLayout != null ? mLayout.getHeight() : 0;
5149 }
5150
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005151 @Override
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005152 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
5153 if (keyCode == KeyEvent.KEYCODE_BACK) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005154 boolean isInSelectionMode = mSelectionActionMode != null;
5155
Gilles Debunne28294cc2011-08-24 12:02:05 -07005156 if (isInSelectionMode) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005157 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
5158 KeyEvent.DispatcherState state = getKeyDispatcherState();
5159 if (state != null) {
5160 state.startTracking(event, this);
5161 }
5162 return true;
5163 } else if (event.getAction() == KeyEvent.ACTION_UP) {
5164 KeyEvent.DispatcherState state = getKeyDispatcherState();
5165 if (state != null) {
5166 state.handleUpEvent(event);
5167 }
5168 if (event.isTracking() && !event.isCanceled()) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005169 if (isInSelectionMode) {
5170 stopSelectionActionMode();
5171 return true;
5172 }
5173 }
5174 }
5175 }
5176 }
5177 return super.onKeyPreIme(keyCode, event);
5178 }
5179
5180 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005181 public boolean onKeyDown(int keyCode, KeyEvent event) {
5182 int which = doKeyDown(keyCode, event, null);
5183 if (which == 0) {
5184 // Go through default dispatching.
5185 return super.onKeyDown(keyCode, event);
5186 }
5187
5188 return true;
5189 }
5190
5191 @Override
5192 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005193 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005194
5195 int which = doKeyDown(keyCode, down, event);
5196 if (which == 0) {
5197 // Go through default dispatching.
5198 return super.onKeyMultiple(keyCode, repeatCount, event);
5199 }
5200 if (which == -1) {
5201 // Consumed the whole thing.
5202 return true;
5203 }
5204
5205 repeatCount--;
5206
5207 // We are going to dispatch the remaining events to either the input
5208 // or movement method. To do this, we will just send a repeated stream
5209 // of down and up events until we have done the complete repeatCount.
5210 // It would be nice if those interfaces had an onKeyMultiple() method,
5211 // but adding that is a more complicated change.
The Android Open Source Project10592532009-03-18 17:39:46 -07005212 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005213 if (which == 1) {
5214 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5215 while (--repeatCount > 0) {
5216 mInput.onKeyDown(this, (Editable)mText, keyCode, down);
5217 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5218 }
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005219 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005220
5221 } else if (which == 2) {
5222 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5223 while (--repeatCount > 0) {
5224 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
5225 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5226 }
5227 }
5228
5229 return true;
5230 }
5231
5232 /**
5233 * Returns true if pressing ENTER in this field advances focus instead
5234 * of inserting the character. This is true mostly in single-line fields,
5235 * but also in mail addresses and subjects which will display on multiple
5236 * lines but where it doesn't make sense to insert newlines.
5237 */
The Android Open Source Project4df24232009-03-05 14:34:35 -08005238 private boolean shouldAdvanceFocusOnEnter() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005239 if (mInput == null) {
5240 return false;
5241 }
5242
5243 if (mSingleLine) {
5244 return true;
5245 }
5246
5247 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5248 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005249 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
5250 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005251 return true;
5252 }
5253 }
5254
5255 return false;
5256 }
5257
Jeff Brown4e6319b2010-12-13 10:36:51 -08005258 /**
5259 * Returns true if pressing TAB in this field advances focus instead
5260 * of inserting the character. Insert tabs only in multi-line editors.
5261 */
5262 private boolean shouldAdvanceFocusOnTab() {
5263 if (mInput != null && !mSingleLine) {
5264 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5265 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
5266 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
5267 || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
5268 return false;
5269 }
5270 }
5271 }
5272 return true;
5273 }
5274
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005275 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
5276 if (!isEnabled()) {
5277 return 0;
5278 }
5279
5280 switch (keyCode) {
5281 case KeyEvent.KEYCODE_ENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07005282 mEnterKeyIsDown = true;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005283 if (event.hasNoModifiers()) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005284 // When mInputContentType is set, we know that we are
5285 // running in a "modern" cupcake environment, so don't need
5286 // to worry about the application trying to capture
5287 // enter key events.
5288 if (mInputContentType != null) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005289 // If there is an action listener, given them a
5290 // chance to consume the event.
5291 if (mInputContentType.onEditorActionListener != null &&
5292 mInputContentType.onEditorActionListener.onEditorAction(
5293 this, EditorInfo.IME_NULL, event)) {
5294 mInputContentType.enterDown = true;
5295 // We are consuming the enter key for them.
5296 return -1;
5297 }
5298 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005299
The Android Open Source Project10592532009-03-18 17:39:46 -07005300 // If our editor should move focus when enter is pressed, or
5301 // this is a generated event from an IME action button, then
5302 // don't let it be inserted into the text.
Jeff Brown4e6319b2010-12-13 10:36:51 -08005303 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
The Android Open Source Project10592532009-03-18 17:39:46 -07005304 || shouldAdvanceFocusOnEnter()) {
Leon Scroggins7014b122011-01-11 15:17:34 -05005305 if (mOnClickListener != null) {
5306 return 0;
5307 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005308 return -1;
5309 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005310 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005311 break;
5312
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005313 case KeyEvent.KEYCODE_DPAD_CENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07005314 mDPadCenterIsDown = true;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005315 if (event.hasNoModifiers()) {
5316 if (shouldAdvanceFocusOnEnter()) {
5317 return 0;
5318 }
5319 }
5320 break;
5321
5322 case KeyEvent.KEYCODE_TAB:
5323 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
5324 if (shouldAdvanceFocusOnTab()) {
5325 return 0;
5326 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005327 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005328 break;
5329
5330 // Has to be done on key down (and not on key up) to correctly be intercepted.
5331 case KeyEvent.KEYCODE_BACK:
5332 if (mSelectionActionMode != null) {
5333 stopSelectionActionMode();
5334 return -1;
5335 }
5336 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005337 }
5338
5339 if (mInput != null) {
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005340 resetErrorChangedFlag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005341
5342 boolean doDown = true;
5343 if (otherEvent != null) {
5344 try {
5345 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005346 final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005347 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005348 doDown = false;
5349 if (handled) {
5350 return -1;
5351 }
5352 } catch (AbstractMethodError e) {
5353 // onKeyOther was added after 1.0, so if it isn't
5354 // implemented we need to try to dispatch as a regular down.
5355 } finally {
5356 endBatchEdit();
5357 }
5358 }
5359
5360 if (doDown) {
5361 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005362 final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005363 endBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005364 hideErrorIfUnchanged();
5365 if (handled) return 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005366 }
5367 }
5368
5369 // bug 650865: sometimes we get a key event before a layout.
5370 // don't try to move around if we don't know the layout.
5371
5372 if (mMovement != null && mLayout != null) {
5373 boolean doDown = true;
5374 if (otherEvent != null) {
5375 try {
5376 boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
5377 otherEvent);
5378 doDown = false;
5379 if (handled) {
5380 return -1;
5381 }
5382 } catch (AbstractMethodError e) {
5383 // onKeyOther was added after 1.0, so if it isn't
5384 // implemented we need to try to dispatch as a regular down.
5385 }
5386 }
5387 if (doDown) {
5388 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
5389 return 2;
5390 }
5391 }
5392
5393 return 0;
5394 }
5395
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005396 /**
5397 * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
5398 * can be recorded.
5399 * @hide
5400 */
5401 public void resetErrorChangedFlag() {
5402 /*
5403 * Keep track of what the error was before doing the input
5404 * so that if an input filter changed the error, we leave
5405 * that error showing. Otherwise, we take down whatever
5406 * error was showing when the user types something.
5407 */
5408 mErrorWasChanged = false;
5409 }
5410
5411 /**
5412 * @hide
5413 */
5414 public void hideErrorIfUnchanged() {
5415 if (mError != null && !mErrorWasChanged) {
5416 setError(null, null);
5417 }
5418 }
5419
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005420 @Override
5421 public boolean onKeyUp(int keyCode, KeyEvent event) {
5422 if (!isEnabled()) {
5423 return super.onKeyUp(keyCode, event);
5424 }
5425
5426 switch (keyCode) {
5427 case KeyEvent.KEYCODE_DPAD_CENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07005428 mDPadCenterIsDown = false;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005429 if (event.hasNoModifiers()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005430 /*
5431 * If there is a click listener, just call through to
5432 * super, which will invoke it.
5433 *
Jeff Brown4e6319b2010-12-13 10:36:51 -08005434 * If there isn't a click listener, try to show the soft
5435 * input method. (It will also
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005436 * call performClick(), but that won't do anything in
5437 * this case.)
5438 */
5439 if (mOnClickListener == null) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005440 if (mMovement != null && mText instanceof Editable
5441 && mLayout != null && onCheckIsTextEditor()) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08005442 InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09005443 viewClicked(imm);
satok863fcd62011-06-21 17:38:02 +09005444 if (imm != null) {
satok863fcd62011-06-21 17:38:02 +09005445 imm.showSoftInput(this, 0);
5446 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005447 }
5448 }
5449 }
5450 return super.onKeyUp(keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005451
Jeff Brown4e6319b2010-12-13 10:36:51 -08005452 case KeyEvent.KEYCODE_ENTER:
5453 mEnterKeyIsDown = false;
5454 if (event.hasNoModifiers()) {
5455 if (mInputContentType != null
5456 && mInputContentType.onEditorActionListener != null
5457 && mInputContentType.enterDown) {
5458 mInputContentType.enterDown = false;
5459 if (mInputContentType.onEditorActionListener.onEditorAction(
5460 this, EditorInfo.IME_NULL, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005461 return true;
5462 }
5463 }
5464
Jeff Brown4e6319b2010-12-13 10:36:51 -08005465 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
5466 || shouldAdvanceFocusOnEnter()) {
5467 /*
5468 * If there is a click listener, just call through to
5469 * super, which will invoke it.
5470 *
5471 * If there isn't a click listener, try to advance focus,
5472 * but still call through to super, which will reset the
5473 * pressed state and longpress state. (It will also
5474 * call performClick(), but that won't do anything in
5475 * this case.)
5476 */
5477 if (mOnClickListener == null) {
5478 View v = focusSearch(FOCUS_DOWN);
5479
5480 if (v != null) {
5481 if (!v.requestFocus(FOCUS_DOWN)) {
5482 throw new IllegalStateException(
5483 "focus search returned a view " +
5484 "that wasn't able to take focus!");
5485 }
5486
5487 /*
5488 * Return true because we handled the key; super
5489 * will return false because there was no click
5490 * listener.
5491 */
5492 super.onKeyUp(keyCode, event);
5493 return true;
5494 } else if ((event.getFlags()
5495 & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
5496 // No target for next focus, but make sure the IME
5497 // if this came from it.
5498 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08005499 if (imm != null && imm.isActive(this)) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005500 imm.hideSoftInputFromWindow(getWindowToken(), 0);
5501 }
5502 }
5503 }
5504 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005505 return super.onKeyUp(keyCode, event);
5506 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005507 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005508 }
5509
5510 if (mInput != null)
5511 if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
5512 return true;
5513
5514 if (mMovement != null && mLayout != null)
5515 if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
5516 return true;
5517
5518 return super.onKeyUp(keyCode, event);
5519 }
5520
5521 @Override public boolean onCheckIsTextEditor() {
5522 return mInputType != EditorInfo.TYPE_NULL;
5523 }
5524
5525 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
Janos Levai042856c2010-10-15 02:53:58 +03005526 if (onCheckIsTextEditor() && isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005527 if (mInputMethodState == null) {
5528 mInputMethodState = new InputMethodState();
5529 }
5530 outAttrs.inputType = mInputType;
5531 if (mInputContentType != null) {
5532 outAttrs.imeOptions = mInputContentType.imeOptions;
5533 outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5534 outAttrs.actionLabel = mInputContentType.imeActionLabel;
5535 outAttrs.actionId = mInputContentType.imeActionId;
5536 outAttrs.extras = mInputContentType.extras;
5537 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005538 outAttrs.imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005539 }
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005540 if (focusSearch(FOCUS_DOWN) != null) {
5541 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5542 }
5543 if (focusSearch(FOCUS_UP) != null) {
5544 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5545 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005546 if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5547 == EditorInfo.IME_ACTION_UNSPECIFIED) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005548 if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005549 // An action has not been set, but the enter key will move to
5550 // the next focus, so set the action to that.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005551 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005552 } else {
5553 // An action has not been set, and there is no focus to move
5554 // to, so let's just supply a "done" action.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005555 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005556 }
5557 if (!shouldAdvanceFocusOnEnter()) {
5558 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005559 }
5560 }
Gilles Debunne91a08cf2010-11-08 17:34:49 -08005561 if (isMultilineInputType(outAttrs.inputType)) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005562 // Multi-line text editors should always show an enter key.
5563 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5564 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005565 outAttrs.hintText = mHint;
5566 if (mText instanceof Editable) {
5567 InputConnection ic = new EditableInputConnection(this);
Gilles Debunne05336272010-07-09 20:13:45 -07005568 outAttrs.initialSelStart = getSelectionStart();
5569 outAttrs.initialSelEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005570 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5571 return ic;
5572 }
5573 }
5574 return null;
5575 }
5576
5577 /**
5578 * If this TextView contains editable content, extract a portion of it
5579 * based on the information in <var>request</var> in to <var>outText</var>.
5580 * @return Returns true if the text was successfully extracted, else false.
5581 */
5582 public boolean extractText(ExtractedTextRequest request,
5583 ExtractedText outText) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005584 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5585 EXTRACT_UNKNOWN, outText);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005586 }
5587
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005588 static final int EXTRACT_NOTHING = -2;
5589 static final int EXTRACT_UNKNOWN = -1;
5590
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005591 boolean extractTextInternal(ExtractedTextRequest request,
5592 int partialStartOffset, int partialEndOffset, int delta,
5593 ExtractedText outText) {
5594 final CharSequence content = mText;
5595 if (content != null) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005596 if (partialStartOffset != EXTRACT_NOTHING) {
5597 final int N = content.length();
5598 if (partialStartOffset < 0) {
5599 outText.partialStartOffset = outText.partialEndOffset = -1;
5600 partialStartOffset = 0;
5601 partialEndOffset = N;
5602 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01005603 // Now use the delta to determine the actual amount of text
5604 // we need.
5605 partialEndOffset += delta;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005606 // Adjust offsets to ensure we contain full spans.
5607 if (content instanceof Spanned) {
5608 Spanned spanned = (Spanned)content;
5609 Object[] spans = spanned.getSpans(partialStartOffset,
5610 partialEndOffset, ParcelableSpan.class);
5611 int i = spans.length;
5612 while (i > 0) {
5613 i--;
5614 int j = spanned.getSpanStart(spans[i]);
5615 if (j < partialStartOffset) partialStartOffset = j;
5616 j = spanned.getSpanEnd(spans[i]);
5617 if (j > partialEndOffset) partialEndOffset = j;
5618 }
5619 }
5620 outText.partialStartOffset = partialStartOffset;
Viktor Yakovel964be412010-02-17 08:35:57 +01005621 outText.partialEndOffset = partialEndOffset - delta;
5622
Eric Fischer32929412009-12-14 17:33:11 -08005623 if (partialStartOffset > N) {
5624 partialStartOffset = N;
5625 } else if (partialStartOffset < 0) {
5626 partialStartOffset = 0;
5627 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005628 if (partialEndOffset > N) {
5629 partialEndOffset = N;
5630 } else if (partialEndOffset < 0) {
5631 partialEndOffset = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005632 }
5633 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005634 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5635 outText.text = content.subSequence(partialStartOffset,
5636 partialEndOffset);
5637 } else {
5638 outText.text = TextUtils.substring(content, partialStartOffset,
5639 partialEndOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005640 }
Viktor Yakovel970a138c92010-02-12 16:00:41 +01005641 } else {
5642 outText.partialStartOffset = 0;
5643 outText.partialEndOffset = 0;
5644 outText.text = "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005645 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005646 outText.flags = 0;
5647 if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5648 outText.flags |= ExtractedText.FLAG_SELECTING;
5649 }
5650 if (mSingleLine) {
5651 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005652 }
5653 outText.startOffset = 0;
Gilles Debunne05336272010-07-09 20:13:45 -07005654 outText.selectionStart = getSelectionStart();
5655 outText.selectionEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005656 return true;
5657 }
5658 return false;
5659 }
5660
5661 boolean reportExtractedText() {
5662 final InputMethodState ims = mInputMethodState;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005663 if (ims != null) {
5664 final boolean contentChanged = ims.mContentChanged;
5665 if (contentChanged || ims.mSelectionModeChanged) {
5666 ims.mContentChanged = false;
5667 ims.mSelectionModeChanged = false;
5668 final ExtractedTextRequest req = mInputMethodState.mExtracting;
5669 if (req != null) {
5670 InputMethodManager imm = InputMethodManager.peekInstance();
5671 if (imm != null) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005672 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005673 + ims.mChangedStart + " end=" + ims.mChangedEnd
5674 + " delta=" + ims.mChangedDelta);
5675 if (ims.mChangedStart < 0 && !contentChanged) {
5676 ims.mChangedStart = EXTRACT_NOTHING;
5677 }
5678 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5679 ims.mChangedDelta, ims.mTmpExtracted)) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005680 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005681 + ims.mTmpExtracted.partialStartOffset
5682 + " end=" + ims.mTmpExtracted.partialEndOffset
5683 + ": " + ims.mTmpExtracted.text);
5684 imm.updateExtractedText(this, req.token,
5685 mInputMethodState.mTmpExtracted);
Viktor Yakovel964be412010-02-17 08:35:57 +01005686 ims.mChangedStart = EXTRACT_UNKNOWN;
5687 ims.mChangedEnd = EXTRACT_UNKNOWN;
5688 ims.mChangedDelta = 0;
5689 ims.mContentChanged = false;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005690 return true;
5691 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005692 }
5693 }
5694 }
5695 }
5696 return false;
5697 }
5698
5699 /**
5700 * This is used to remove all style-impacting spans from text before new
5701 * extracted text is being replaced into it, so that we don't have any
5702 * lingering spans applied during the replace.
5703 */
5704 static void removeParcelableSpans(Spannable spannable, int start, int end) {
5705 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5706 int i = spans.length;
5707 while (i > 0) {
5708 i--;
5709 spannable.removeSpan(spans[i]);
5710 }
5711 }
5712
5713 /**
5714 * Apply to this text view the given extracted text, as previously
5715 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5716 */
5717 public void setExtractedText(ExtractedText text) {
5718 Editable content = getEditableText();
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005719 if (text.text != null) {
5720 if (content == null) {
5721 setText(text.text, TextView.BufferType.EDITABLE);
5722 } else if (text.partialStartOffset < 0) {
5723 removeParcelableSpans(content, 0, content.length());
5724 content.replace(0, content.length(), text.text);
5725 } else {
5726 final int N = content.length();
5727 int start = text.partialStartOffset;
5728 if (start > N) start = N;
5729 int end = text.partialEndOffset;
5730 if (end > N) end = N;
5731 removeParcelableSpans(content, start, end);
5732 content.replace(start, end, text.text);
5733 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005734 }
5735
5736 // Now set the selection position... make sure it is in range, to
5737 // avoid crashes. If this is a partial update, it is possible that
5738 // the underlying text may have changed, causing us problems here.
5739 // Also we just don't want to trust clients to do the right thing.
5740 Spannable sp = (Spannable)getText();
5741 final int N = sp.length();
5742 int start = text.selectionStart;
5743 if (start < 0) start = 0;
5744 else if (start > N) start = N;
5745 int end = text.selectionEnd;
5746 if (end < 0) end = 0;
5747 else if (end > N) end = N;
5748 Selection.setSelection(sp, start, end);
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005749
5750 // Finally, update the selection mode.
5751 if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5752 MetaKeyKeyListener.startSelecting(this, sp);
5753 } else {
5754 MetaKeyKeyListener.stopSelecting(this, sp);
5755 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005756 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005757
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005758 /**
5759 * @hide
5760 */
5761 public void setExtracting(ExtractedTextRequest req) {
5762 if (mInputMethodState != null) {
5763 mInputMethodState.mExtracting = req;
5764 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005765 // This would stop a possible selection mode, but no such mode is started in case
5766 // extracted mode will start. Some text is selected though, and will trigger an action mode
5767 // in the extracted view.
Adam Powellba0a2c32010-09-28 17:41:23 -07005768 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005769 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005770
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005771 /**
5772 * Called by the framework in response to a text completion from
5773 * the current input method, provided by it calling
5774 * {@link InputConnection#commitCompletion
5775 * InputConnection.commitCompletion()}. The default implementation does
5776 * nothing; text views that are supporting auto-completion should override
5777 * this to do their desired behavior.
5778 *
5779 * @param text The auto complete text the user has selected.
5780 */
5781 public void onCommitCompletion(CompletionInfo text) {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08005782 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005783 }
5784
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005785 /**
5786 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5787 * a dictionnary) from the current input method, provided by it calling
5788 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5789 * implementation flashes the background of the corrected word to provide feedback to the user.
5790 *
5791 * @param info The auto correct info about the text that was corrected.
5792 */
5793 public void onCommitCorrection(CorrectionInfo info) {
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005794 if (mCorrectionHighlighter == null) {
5795 mCorrectionHighlighter = new CorrectionHighlighter();
5796 } else {
5797 mCorrectionHighlighter.invalidate(false);
5798 }
5799
5800 mCorrectionHighlighter.highlight(info);
5801 }
5802
5803 private class CorrectionHighlighter {
5804 private final Path mPath = new Path();
5805 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5806 private int mStart, mEnd;
5807 private long mFadingStartTime;
5808 private final static int FADE_OUT_DURATION = 400;
5809
5810 public CorrectionHighlighter() {
5811 mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5812 mPaint.setStyle(Paint.Style.FILL);
5813 }
5814
5815 public void highlight(CorrectionInfo info) {
5816 mStart = info.getOffset();
5817 mEnd = mStart + info.getNewText().length();
5818 mFadingStartTime = SystemClock.uptimeMillis();
5819
5820 if (mStart < 0 || mEnd < 0) {
5821 stopAnimation();
5822 }
5823 }
5824
5825 public void draw(Canvas canvas, int cursorOffsetVertical) {
5826 if (updatePath() && updatePaint()) {
5827 if (cursorOffsetVertical != 0) {
5828 canvas.translate(0, cursorOffsetVertical);
5829 }
5830
5831 canvas.drawPath(mPath, mPaint);
5832
5833 if (cursorOffsetVertical != 0) {
5834 canvas.translate(0, -cursorOffsetVertical);
5835 }
5836 invalidate(true);
5837 } else {
5838 stopAnimation();
5839 invalidate(false);
5840 }
5841 }
5842
5843 private boolean updatePaint() {
5844 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5845 if (duration > FADE_OUT_DURATION) return false;
5846
5847 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5848 final int highlightColorAlpha = Color.alpha(mHighlightColor);
5849 final int color = (mHighlightColor & 0x00FFFFFF) +
5850 ((int) (highlightColorAlpha * coef) << 24);
5851 mPaint.setColor(color);
5852 return true;
5853 }
5854
5855 private boolean updatePath() {
5856 final Layout layout = TextView.this.mLayout;
5857 if (layout == null) return false;
5858
5859 // Update in case text is edited while the animation is run
5860 final int length = mText.length();
5861 int start = Math.min(length, mStart);
5862 int end = Math.min(length, mEnd);
5863
5864 mPath.reset();
5865 TextView.this.mLayout.getSelectionPath(start, end, mPath);
5866 return true;
5867 }
5868
5869 private void invalidate(boolean delayed) {
5870 if (TextView.this.mLayout == null) return;
5871
5872 synchronized (sTempRect) {
5873 mPath.computeBounds(sTempRect, false);
5874
5875 int left = getCompoundPaddingLeft();
5876 int top = getExtendedPaddingTop() + getVerticalOffset(true);
5877
5878 if (delayed) {
5879 TextView.this.postInvalidateDelayed(16, // 60 Hz update
5880 left + (int) sTempRect.left, top + (int) sTempRect.top,
5881 left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5882 } else {
5883 TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5884 (int) sTempRect.right, (int) sTempRect.bottom);
5885 }
5886 }
5887 }
5888
5889 private void stopAnimation() {
5890 TextView.this.mCorrectionHighlighter = null;
5891 }
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005892 }
5893
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005894 public void beginBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005895 mInBatchEditControllers = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005896 final InputMethodState ims = mInputMethodState;
5897 if (ims != null) {
5898 int nesting = ++ims.mBatchEditNesting;
5899 if (nesting == 1) {
5900 ims.mCursorChanged = false;
5901 ims.mChangedDelta = 0;
5902 if (ims.mContentChanged) {
5903 // We already have a pending change from somewhere else,
5904 // so turn this into a full update.
5905 ims.mChangedStart = 0;
5906 ims.mChangedEnd = mText.length();
5907 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005908 ims.mChangedStart = EXTRACT_UNKNOWN;
5909 ims.mChangedEnd = EXTRACT_UNKNOWN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005910 ims.mContentChanged = false;
5911 }
5912 onBeginBatchEdit();
5913 }
5914 }
5915 }
5916
5917 public void endBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005918 mInBatchEditControllers = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005919 final InputMethodState ims = mInputMethodState;
5920 if (ims != null) {
5921 int nesting = --ims.mBatchEditNesting;
5922 if (nesting == 0) {
5923 finishBatchEdit(ims);
5924 }
5925 }
5926 }
5927
5928 void ensureEndedBatchEdit() {
5929 final InputMethodState ims = mInputMethodState;
5930 if (ims != null && ims.mBatchEditNesting != 0) {
5931 ims.mBatchEditNesting = 0;
5932 finishBatchEdit(ims);
5933 }
5934 }
5935
5936 void finishBatchEdit(final InputMethodState ims) {
5937 onEndBatchEdit();
5938
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005939 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005940 updateAfterEdit();
5941 reportExtractedText();
5942 } else if (ims.mCursorChanged) {
5943 // Cheezy way to get us to report the current cursor location.
5944 invalidateCursor();
5945 }
5946 }
5947
5948 void updateAfterEdit() {
5949 invalidate();
Gilles Debunne05336272010-07-09 20:13:45 -07005950 int curs = getSelectionStart();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005951
Gilles Debunne3d010062011-02-18 14:16:41 -08005952 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005953 registerForPreDraw();
5954 }
5955
5956 if (curs >= 0) {
5957 mHighlightPathBogus = true;
Gilles Debunne3d010062011-02-18 14:16:41 -08005958 makeBlink();
Gilles Debunne8202cd32011-07-15 09:56:30 -07005959 bringPointIntoView(curs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005960 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005961
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005962 checkForResize();
5963 }
5964
5965 /**
5966 * Called by the framework in response to a request to begin a batch
5967 * of edit operations through a call to link {@link #beginBatchEdit()}.
5968 */
5969 public void onBeginBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08005970 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005971 }
5972
5973 /**
5974 * Called by the framework in response to a request to end a batch
5975 * of edit operations through a call to link {@link #endBatchEdit}.
5976 */
5977 public void onEndBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08005978 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005979 }
5980
5981 /**
5982 * Called by the framework in response to a private command from the
5983 * current method, provided by it calling
5984 * {@link InputConnection#performPrivateCommand
5985 * InputConnection.performPrivateCommand()}.
5986 *
5987 * @param action The action name of the command.
5988 * @param data Any additional data for the command. This may be null.
5989 * @return Return true if you handled the command, else false.
5990 */
5991 public boolean onPrivateIMECommand(String action, Bundle data) {
5992 return false;
5993 }
5994
5995 private void nullLayouts() {
5996 if (mLayout instanceof BoringLayout && mSavedLayout == null) {
5997 mSavedLayout = (BoringLayout) mLayout;
5998 }
5999 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
6000 mSavedHintLayout = (BoringLayout) mHintLayout;
6001 }
6002
Adam Powell282e3772011-08-30 16:51:11 -07006003 mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
Gilles Debunne77f18b02010-10-22 14:28:25 -07006004
6005 // Since it depends on the value of mLayout
6006 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006007 }
6008
6009 /**
6010 * Make a new Layout based on the already-measured size of the view,
6011 * on the assumption that it was measured correctly at some point.
6012 */
6013 private void assumeLayout() {
6014 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6015
6016 if (width < 1) {
6017 width = 0;
6018 }
6019
6020 int physicalWidth = width;
6021
6022 if (mHorizontallyScrolling) {
6023 width = VERY_WIDE;
6024 }
6025
6026 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
6027 physicalWidth, false);
6028 }
6029
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006030 @Override
Fabrice Di Meglio7f86c802011-07-01 15:09:24 -07006031 protected void resetResolvedLayoutDirection() {
6032 super.resetResolvedLayoutDirection();
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006033
6034 if (mLayoutAlignment != null &&
6035 (mTextAlign == TextAlign.VIEW_START ||
6036 mTextAlign == TextAlign.VIEW_END)) {
6037 mLayoutAlignment = null;
6038 }
6039 }
6040
6041 private Layout.Alignment getLayoutAlignment() {
6042 if (mLayoutAlignment == null) {
6043 Layout.Alignment alignment;
6044 TextAlign textAlign = mTextAlign;
6045 switch (textAlign) {
6046 case INHERIT:
6047 // fall through to gravity temporarily
6048 // intention is to inherit value through view hierarchy.
6049 case GRAVITY:
6050 switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
6051 case Gravity.START:
6052 alignment = Layout.Alignment.ALIGN_NORMAL;
6053 break;
6054 case Gravity.END:
6055 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6056 break;
6057 case Gravity.LEFT:
6058 alignment = Layout.Alignment.ALIGN_LEFT;
6059 break;
6060 case Gravity.RIGHT:
6061 alignment = Layout.Alignment.ALIGN_RIGHT;
6062 break;
6063 case Gravity.CENTER_HORIZONTAL:
6064 alignment = Layout.Alignment.ALIGN_CENTER;
6065 break;
6066 default:
6067 alignment = Layout.Alignment.ALIGN_NORMAL;
6068 break;
6069 }
6070 break;
6071 case TEXT_START:
6072 alignment = Layout.Alignment.ALIGN_NORMAL;
6073 break;
6074 case TEXT_END:
6075 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6076 break;
6077 case CENTER:
6078 alignment = Layout.Alignment.ALIGN_CENTER;
6079 break;
6080 case VIEW_START:
6081 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6082 Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
6083 break;
6084 case VIEW_END:
6085 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6086 Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
6087 break;
6088 default:
6089 alignment = Layout.Alignment.ALIGN_NORMAL;
6090 break;
6091 }
6092 mLayoutAlignment = alignment;
6093 }
6094 return mLayoutAlignment;
6095 }
6096
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006097 /**
6098 * The width passed in is now the desired layout width,
6099 * not the full view width with padding.
6100 * {@hide}
6101 */
6102 protected void makeNewLayout(int w, int hintWidth,
6103 BoringLayout.Metrics boring,
6104 BoringLayout.Metrics hintBoring,
6105 int ellipsisWidth, boolean bringIntoView) {
6106 stopMarquee();
6107
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006108 // Update "old" cached values
6109 mOldMaximum = mMaximum;
6110 mOldMaxMode = mMaxMode;
6111
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006112 mHighlightPathBogus = true;
6113
6114 if (w < 0) {
6115 w = 0;
6116 }
6117 if (hintWidth < 0) {
6118 hintWidth = 0;
6119 }
6120
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006121 Layout.Alignment alignment = getLayoutAlignment();
Romain Guy4dc4f732009-06-19 15:16:40 -07006122 boolean shouldEllipsize = mEllipsize != null && mInput == null;
Adam Powell282e3772011-08-30 16:51:11 -07006123 final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
6124 mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
6125 TruncateAt effectiveEllipsize = mEllipsize;
6126 if (mEllipsize == TruncateAt.MARQUEE &&
6127 mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
6128 effectiveEllipsize = TruncateAt.END;
6129 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006130
Doug Feltcb3791202011-07-07 11:57:48 -07006131 if (mTextDir == null) {
6132 resolveTextDirection();
6133 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006134
Adam Powell282e3772011-08-30 16:51:11 -07006135 mLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment, shouldEllipsize,
6136 effectiveEllipsize, effectiveEllipsize == mEllipsize);
6137 if (switchEllipsize) {
6138 TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
6139 TruncateAt.END : TruncateAt.MARQUEE;
6140 mSavedMarqueeModeLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment,
6141 shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006142 }
6143
Romain Guy4dc4f732009-06-19 15:16:40 -07006144 shouldEllipsize = mEllipsize != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006145 mHintLayout = null;
6146
6147 if (mHint != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006148 if (shouldEllipsize) hintWidth = w;
6149
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006150 if (hintBoring == UNKNOWN_BORING) {
Doug Feltcb3791202011-07-07 11:57:48 -07006151 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006152 mHintBoring);
6153 if (hintBoring != null) {
6154 mHintBoring = hintBoring;
6155 }
6156 }
6157
6158 if (hintBoring != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006159 if (hintBoring.width <= hintWidth &&
6160 (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006161 if (mSavedHintLayout != null) {
6162 mHintLayout = mSavedHintLayout.
6163 replaceOrMake(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006164 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6165 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006166 } else {
6167 mHintLayout = BoringLayout.make(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006168 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6169 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006170 }
6171
6172 mSavedHintLayout = (BoringLayout) mHintLayout;
Romain Guy4dc4f732009-06-19 15:16:40 -07006173 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
6174 if (mSavedHintLayout != null) {
6175 mHintLayout = mSavedHintLayout.
6176 replaceOrMake(mHint, mTextPaint,
6177 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6178 hintBoring, mIncludePad, mEllipsize,
6179 ellipsisWidth);
6180 } else {
6181 mHintLayout = BoringLayout.make(mHint, mTextPaint,
6182 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6183 hintBoring, mIncludePad, mEllipsize,
6184 ellipsisWidth);
6185 }
6186 } else if (shouldEllipsize) {
6187 mHintLayout = new StaticLayout(mHint,
6188 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006189 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006190 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006191 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006192 } else {
6193 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006194 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006195 mIncludePad);
6196 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006197 } else if (shouldEllipsize) {
6198 mHintLayout = new StaticLayout(mHint,
6199 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006200 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006201 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006202 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006203 } else {
6204 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006205 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006206 mIncludePad);
6207 }
6208 }
6209
6210 if (bringIntoView) {
6211 registerForPreDraw();
6212 }
6213
6214 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
Romain Guy939151f2009-04-08 14:22:40 -07006215 if (!compressText(ellipsisWidth)) {
6216 final int height = mLayoutParams.height;
6217 // If the size of the view does not depend on the size of the text, try to
6218 // start the marquee immediately
Romain Guy980a9382010-01-08 15:06:28 -08006219 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
Romain Guy939151f2009-04-08 14:22:40 -07006220 startMarquee();
6221 } else {
6222 // Defer the start of the marquee until we know our width (see setFrame())
6223 mRestartMarquee = true;
6224 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006225 }
6226 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07006227
6228 // CursorControllers need a non-null mLayout
6229 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006230 }
6231
Adam Powell282e3772011-08-30 16:51:11 -07006232 private Layout makeSingleLayout(int w, BoringLayout.Metrics boring, int ellipsisWidth,
6233 Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
6234 boolean useSaved) {
6235 Layout result = null;
6236 if (mText instanceof Spannable) {
6237 result = new DynamicLayout(mText, mTransformed, mTextPaint, w,
6238 alignment, mTextDir, mSpacingMult,
6239 mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
6240 ellipsisWidth);
6241 } else {
6242 if (boring == UNKNOWN_BORING) {
6243 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
6244 if (boring != null) {
6245 mBoring = boring;
6246 }
6247 }
6248
6249 if (boring != null) {
6250 if (boring.width <= w &&
6251 (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
6252 if (useSaved && mSavedLayout != null) {
6253 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
6254 w, alignment, mSpacingMult, mSpacingAdd,
6255 boring, mIncludePad);
6256 } else {
6257 result = BoringLayout.make(mTransformed, mTextPaint,
6258 w, alignment, mSpacingMult, mSpacingAdd,
6259 boring, mIncludePad);
6260 }
6261
6262 if (useSaved) {
6263 mSavedLayout = (BoringLayout) result;
6264 }
6265 } else if (shouldEllipsize && boring.width <= w) {
6266 if (useSaved && mSavedLayout != null) {
6267 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
6268 w, alignment, mSpacingMult, mSpacingAdd,
6269 boring, mIncludePad, effectiveEllipsize,
6270 ellipsisWidth);
6271 } else {
6272 result = BoringLayout.make(mTransformed, mTextPaint,
6273 w, alignment, mSpacingMult, mSpacingAdd,
6274 boring, mIncludePad, effectiveEllipsize,
6275 ellipsisWidth);
6276 }
6277 } else if (shouldEllipsize) {
6278 result = new StaticLayout(mTransformed,
6279 0, mTransformed.length(),
6280 mTextPaint, w, alignment, mTextDir, mSpacingMult,
6281 mSpacingAdd, mIncludePad, effectiveEllipsize,
6282 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6283 } else {
6284 result = new StaticLayout(mTransformed, mTextPaint,
6285 w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6286 mIncludePad);
6287 }
6288 } else if (shouldEllipsize) {
6289 result = new StaticLayout(mTransformed,
6290 0, mTransformed.length(),
6291 mTextPaint, w, alignment, mTextDir, mSpacingMult,
6292 mSpacingAdd, mIncludePad, effectiveEllipsize,
6293 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6294 } else {
6295 result = new StaticLayout(mTransformed, mTextPaint,
6296 w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
6297 mIncludePad);
6298 }
6299 }
6300 return result;
6301 }
6302
Romain Guy939151f2009-04-08 14:22:40 -07006303 private boolean compressText(float width) {
Romain Guy2bffd262010-09-12 17:40:02 -07006304 if (isHardwareAccelerated()) return false;
6305
Romain Guy3373ed62009-05-04 14:13:32 -07006306 // Only compress the text if it hasn't been compressed by the previous pass
6307 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
6308 mTextPaint.getTextScaleX() == 1.0f) {
Romain Guy939151f2009-04-08 14:22:40 -07006309 final float textWidth = mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07006310 final float overflow = (textWidth + 1.0f - width) / width;
Romain Guy939151f2009-04-08 14:22:40 -07006311 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
6312 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
6313 post(new Runnable() {
6314 public void run() {
6315 requestLayout();
6316 }
6317 });
6318 return true;
6319 }
6320 }
6321
6322 return false;
6323 }
6324
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006325 private static int desired(Layout layout) {
6326 int n = layout.getLineCount();
6327 CharSequence text = layout.getText();
6328 float max = 0;
6329
6330 // if any line was wrapped, we can't use it.
6331 // but it's ok for the last line not to have a newline
6332
6333 for (int i = 0; i < n - 1; i++) {
6334 if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
6335 return -1;
6336 }
6337
6338 for (int i = 0; i < n; i++) {
6339 max = Math.max(max, layout.getLineWidth(i));
6340 }
6341
6342 return (int) FloatMath.ceil(max);
6343 }
6344
6345 /**
6346 * Set whether the TextView includes extra top and bottom padding to make
6347 * room for accents that go above the normal ascent and descent.
6348 * The default is true.
6349 *
6350 * @attr ref android.R.styleable#TextView_includeFontPadding
6351 */
6352 public void setIncludeFontPadding(boolean includepad) {
Gilles Debunne22378292011-08-12 10:38:52 -07006353 if (mIncludePad != includepad) {
6354 mIncludePad = includepad;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006355
Gilles Debunne22378292011-08-12 10:38:52 -07006356 if (mLayout != null) {
6357 nullLayouts();
6358 requestLayout();
6359 invalidate();
6360 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006361 }
6362 }
6363
Romain Guy4dc4f732009-06-19 15:16:40 -07006364 private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006365
6366 @Override
6367 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
6368 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
6369 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
6370 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
6371 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
6372
6373 int width;
6374 int height;
6375
6376 BoringLayout.Metrics boring = UNKNOWN_BORING;
6377 BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
6378
Doug Feltcb3791202011-07-07 11:57:48 -07006379 if (mTextDir == null) {
6380 resolveTextDirection();
6381 }
6382
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006383 int des = -1;
6384 boolean fromexisting = false;
6385
6386 if (widthMode == MeasureSpec.EXACTLY) {
6387 // Parent has told us how big to be. So be it.
6388 width = widthSize;
6389 } else {
6390 if (mLayout != null && mEllipsize == null) {
6391 des = desired(mLayout);
6392 }
6393
6394 if (des < 0) {
Doug Feltcb3791202011-07-07 11:57:48 -07006395 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006396 if (boring != null) {
6397 mBoring = boring;
6398 }
6399 } else {
6400 fromexisting = true;
6401 }
6402
6403 if (boring == null || boring == UNKNOWN_BORING) {
6404 if (des < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006405 des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006406 }
6407
6408 width = des;
6409 } else {
6410 width = boring.width;
6411 }
6412
6413 final Drawables dr = mDrawables;
6414 if (dr != null) {
6415 width = Math.max(width, dr.mDrawableWidthTop);
6416 width = Math.max(width, dr.mDrawableWidthBottom);
6417 }
6418
6419 if (mHint != null) {
6420 int hintDes = -1;
6421 int hintWidth;
6422
Romain Guy4dc4f732009-06-19 15:16:40 -07006423 if (mHintLayout != null && mEllipsize == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006424 hintDes = desired(mHintLayout);
6425 }
6426
6427 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006428 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006429 if (hintBoring != null) {
6430 mHintBoring = hintBoring;
6431 }
6432 }
6433
6434 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
6435 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006436 hintDes = (int) FloatMath.ceil(
6437 Layout.getDesiredWidth(mHint, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006438 }
6439
6440 hintWidth = hintDes;
6441 } else {
6442 hintWidth = hintBoring.width;
6443 }
6444
6445 if (hintWidth > width) {
6446 width = hintWidth;
6447 }
6448 }
6449
6450 width += getCompoundPaddingLeft() + getCompoundPaddingRight();
6451
6452 if (mMaxWidthMode == EMS) {
6453 width = Math.min(width, mMaxWidth * getLineHeight());
6454 } else {
6455 width = Math.min(width, mMaxWidth);
6456 }
6457
6458 if (mMinWidthMode == EMS) {
6459 width = Math.max(width, mMinWidth * getLineHeight());
6460 } else {
6461 width = Math.max(width, mMinWidth);
6462 }
6463
6464 // Check against our minimum width
6465 width = Math.max(width, getSuggestedMinimumWidth());
6466
6467 if (widthMode == MeasureSpec.AT_MOST) {
6468 width = Math.min(widthSize, width);
6469 }
6470 }
6471
6472 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
6473 int unpaddedWidth = want;
Gilles Debunne9a80a652011-01-31 12:56:07 -08006474
6475 if (mHorizontallyScrolling) want = VERY_WIDE;
6476
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006477 int hintWant = want;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006478 int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006479
6480 if (mLayout == null) {
6481 makeNewLayout(want, hintWant, boring, hintBoring,
Romain Guy4dc4f732009-06-19 15:16:40 -07006482 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006483 } else {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006484 final boolean layoutChanged = (mLayout.getWidth() != want) ||
6485 (hintWidth != hintWant) ||
6486 (mLayout.getEllipsizedWidth() !=
6487 width - getCompoundPaddingLeft() - getCompoundPaddingRight());
6488
6489 final boolean widthChanged = (mHint == null) &&
6490 (mEllipsize == null) &&
6491 (want > mLayout.getWidth()) &&
6492 (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
6493
6494 final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
6495
6496 if (layoutChanged || maximumChanged) {
6497 if (!maximumChanged && widthChanged) {
6498 mLayout.increaseWidthTo(want);
6499 } else {
6500 makeNewLayout(want, hintWant, boring, hintBoring,
6501 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
6502 }
6503 } else {
6504 // Nothing has changed
6505 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006506 }
6507
6508 if (heightMode == MeasureSpec.EXACTLY) {
6509 // Parent has told us how big to be. So be it.
6510 height = heightSize;
6511 mDesiredHeightAtMeasure = -1;
6512 } else {
6513 int desired = getDesiredHeight();
6514
6515 height = desired;
6516 mDesiredHeightAtMeasure = desired;
6517
6518 if (heightMode == MeasureSpec.AT_MOST) {
Christoffer Gurell1d05c7c2009-10-12 15:53:39 +02006519 height = Math.min(desired, heightSize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006520 }
6521 }
6522
Romain Guy4dc4f732009-06-19 15:16:40 -07006523 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006524 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006525 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006526 }
6527
6528 /*
6529 * We didn't let makeNewLayout() register to bring the cursor into view,
6530 * so do it here if there is any possibility that it is needed.
6531 */
6532 if (mMovement != null ||
6533 mLayout.getWidth() > unpaddedWidth ||
6534 mLayout.getHeight() > unpaddedHeight) {
6535 registerForPreDraw();
6536 } else {
6537 scrollTo(0, 0);
6538 }
6539
6540 setMeasuredDimension(width, height);
6541 }
6542
6543 private int getDesiredHeight() {
Romain Guy4dc4f732009-06-19 15:16:40 -07006544 return Math.max(
6545 getDesiredHeight(mLayout, true),
6546 getDesiredHeight(mHintLayout, mEllipsize != null));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006547 }
6548
6549 private int getDesiredHeight(Layout layout, boolean cap) {
6550 if (layout == null) {
6551 return 0;
6552 }
6553
6554 int linecount = layout.getLineCount();
6555 int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
6556 int desired = layout.getLineTop(linecount);
6557
6558 final Drawables dr = mDrawables;
6559 if (dr != null) {
6560 desired = Math.max(desired, dr.mDrawableHeightLeft);
6561 desired = Math.max(desired, dr.mDrawableHeightRight);
6562 }
6563
6564 desired += pad;
6565
6566 if (mMaxMode == LINES) {
6567 /*
6568 * Don't cap the hint to a certain number of lines.
6569 * (Do cap it, though, if we have a maximum pixel height.)
6570 */
6571 if (cap) {
6572 if (linecount > mMaximum) {
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08006573 desired = layout.getLineTop(mMaximum);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006574
6575 if (dr != null) {
6576 desired = Math.max(desired, dr.mDrawableHeightLeft);
6577 desired = Math.max(desired, dr.mDrawableHeightRight);
6578 }
6579
6580 desired += pad;
6581 linecount = mMaximum;
6582 }
6583 }
6584 } else {
6585 desired = Math.min(desired, mMaximum);
6586 }
6587
6588 if (mMinMode == LINES) {
6589 if (linecount < mMinimum) {
6590 desired += getLineHeight() * (mMinimum - linecount);
6591 }
6592 } else {
6593 desired = Math.max(desired, mMinimum);
6594 }
6595
6596 // Check against our minimum height
6597 desired = Math.max(desired, getSuggestedMinimumHeight());
6598
6599 return desired;
6600 }
6601
6602 /**
6603 * Check whether a change to the existing text layout requires a
6604 * new view layout.
6605 */
6606 private void checkForResize() {
6607 boolean sizeChanged = false;
6608
6609 if (mLayout != null) {
6610 // Check if our width changed
6611 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
6612 sizeChanged = true;
6613 invalidate();
6614 }
6615
6616 // Check if our height changed
6617 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6618 int desiredHeight = getDesiredHeight();
6619
6620 if (desiredHeight != this.getHeight()) {
6621 sizeChanged = true;
6622 }
Romain Guy980a9382010-01-08 15:06:28 -08006623 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006624 if (mDesiredHeightAtMeasure >= 0) {
6625 int desiredHeight = getDesiredHeight();
6626
6627 if (desiredHeight != mDesiredHeightAtMeasure) {
6628 sizeChanged = true;
6629 }
6630 }
6631 }
6632 }
6633
6634 if (sizeChanged) {
6635 requestLayout();
6636 // caller will have already invalidated
6637 }
6638 }
6639
6640 /**
6641 * Check whether entirely new text requires a new view layout
6642 * or merely a new text layout.
6643 */
6644 private void checkForRelayout() {
6645 // If we have a fixed width, we can just swap in a new text layout
6646 // if the text height stays the same or if the view height is fixed.
6647
6648 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6649 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6650 (mHint == null || mHintLayout != null) &&
6651 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6652 // Static width, so try making a new text layout.
6653
6654 int oldht = mLayout.getHeight();
6655 int want = mLayout.getWidth();
6656 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6657
6658 /*
6659 * No need to bring the text into view, since the size is not
6660 * changing (unless we do the requestLayout(), in which case it
6661 * will happen at measure).
6662 */
6663 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
Romain Guye1e0dc82009-11-03 17:21:04 -08006664 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6665 false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006666
Romain Guye1e0dc82009-11-03 17:21:04 -08006667 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6668 // In a fixed-height view, so use our new text layout.
6669 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
Romain Guy980a9382010-01-08 15:06:28 -08006670 mLayoutParams.height != LayoutParams.MATCH_PARENT) {
Romain Guye1e0dc82009-11-03 17:21:04 -08006671 invalidate();
6672 return;
6673 }
6674
6675 // Dynamic height, but height has stayed the same,
6676 // so use our new text layout.
6677 if (mLayout.getHeight() == oldht &&
6678 (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6679 invalidate();
6680 return;
6681 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006682 }
6683
6684 // We lose: the height has changed and we have a dynamic height.
6685 // Request a new view layout using our new text layout.
6686 requestLayout();
6687 invalidate();
6688 } else {
6689 // Dynamic width, so we have no choice but to request a new
6690 // view layout with a new text layout.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006691 nullLayouts();
6692 requestLayout();
6693 invalidate();
6694 }
6695 }
6696
6697 /**
6698 * Returns true if anything changed.
6699 */
6700 private boolean bringTextIntoView() {
6701 int line = 0;
6702 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6703 line = mLayout.getLineCount() - 1;
6704 }
6705
6706 Layout.Alignment a = mLayout.getParagraphAlignment(line);
6707 int dir = mLayout.getParagraphDirection(line);
6708 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6709 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6710 int ht = mLayout.getHeight();
6711
6712 int scrollx, scrolly;
6713
Doug Felt25b9f422011-07-11 13:48:37 -07006714 // Convert to left, center, or right alignment.
6715 if (a == Layout.Alignment.ALIGN_NORMAL) {
6716 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT :
6717 Layout.Alignment.ALIGN_RIGHT;
6718 } else if (a == Layout.Alignment.ALIGN_OPPOSITE){
6719 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT :
6720 Layout.Alignment.ALIGN_LEFT;
6721 }
6722
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006723 if (a == Layout.Alignment.ALIGN_CENTER) {
6724 /*
6725 * Keep centered if possible, or, if it is too wide to fit,
6726 * keep leading edge in view.
6727 */
6728
6729 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6730 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6731
6732 if (right - left < hspace) {
6733 scrollx = (right + left) / 2 - hspace / 2;
6734 } else {
6735 if (dir < 0) {
6736 scrollx = right - hspace;
6737 } else {
6738 scrollx = left;
6739 }
6740 }
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006741 } else if (a == Layout.Alignment.ALIGN_RIGHT) {
Doug Felt25b9f422011-07-11 13:48:37 -07006742 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6743 scrollx = right - hspace;
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006744 } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
6745 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006746 }
6747
6748 if (ht < vspace) {
6749 scrolly = 0;
6750 } else {
6751 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6752 scrolly = ht - vspace;
6753 } else {
6754 scrolly = 0;
6755 }
6756 }
6757
6758 if (scrollx != mScrollX || scrolly != mScrollY) {
6759 scrollTo(scrollx, scrolly);
6760 return true;
6761 } else {
6762 return false;
6763 }
6764 }
6765
6766 /**
6767 * Move the point, specified by the offset, into the view if it is needed.
6768 * This has to be called after layout. Returns true if anything changed.
6769 */
6770 public boolean bringPointIntoView(int offset) {
6771 boolean changed = false;
6772
Gilles Debunne176ee3d2011-07-16 13:28:41 -07006773 if (mLayout == null) return changed;
6774
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006775 int line = mLayout.getLineForOffset(offset);
6776
6777 // FIXME: Is it okay to truncate this, or should we round?
6778 final int x = (int)mLayout.getPrimaryHorizontal(offset);
6779 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006780 final int bottom = mLayout.getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006781
6782 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6783 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6784 int ht = mLayout.getHeight();
6785
6786 int grav;
6787
6788 switch (mLayout.getParagraphAlignment(line)) {
Doug Felt25b9f422011-07-11 13:48:37 -07006789 case ALIGN_LEFT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006790 grav = 1;
6791 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006792 case ALIGN_RIGHT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006793 grav = -1;
6794 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006795 case ALIGN_NORMAL:
6796 grav = mLayout.getParagraphDirection(line);
6797 break;
6798 case ALIGN_OPPOSITE:
6799 grav = -mLayout.getParagraphDirection(line);
6800 break;
6801 case ALIGN_CENTER:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006802 default:
6803 grav = 0;
Doug Felt25b9f422011-07-11 13:48:37 -07006804 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006805 }
6806
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006807 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6808 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6809
6810 int hslack = (bottom - top) / 2;
6811 int vslack = hslack;
6812
6813 if (vslack > vspace / 4)
6814 vslack = vspace / 4;
6815 if (hslack > hspace / 4)
6816 hslack = hspace / 4;
6817
6818 int hs = mScrollX;
6819 int vs = mScrollY;
6820
6821 if (top - vs < vslack)
6822 vs = top - vslack;
6823 if (bottom - vs > vspace - vslack)
6824 vs = bottom - (vspace - vslack);
6825 if (ht - vs < vspace)
6826 vs = ht - vspace;
6827 if (0 - vs > 0)
6828 vs = 0;
6829
6830 if (grav != 0) {
6831 if (x - hs < hslack) {
6832 hs = x - hslack;
6833 }
6834 if (x - hs > hspace - hslack) {
6835 hs = x - (hspace - hslack);
6836 }
6837 }
6838
6839 if (grav < 0) {
6840 if (left - hs > 0)
6841 hs = left;
6842 if (right - hs < hspace)
6843 hs = right - hspace;
6844 } else if (grav > 0) {
6845 if (right - hs < hspace)
6846 hs = right - hspace;
6847 if (left - hs > 0)
6848 hs = left;
6849 } else /* grav == 0 */ {
6850 if (right - left <= hspace) {
6851 /*
6852 * If the entire text fits, center it exactly.
6853 */
6854 hs = left - (hspace - (right - left)) / 2;
6855 } else if (x > right - hslack) {
6856 /*
6857 * If we are near the right edge, keep the right edge
6858 * at the edge of the view.
6859 */
6860 hs = right - hspace;
6861 } else if (x < left + hslack) {
6862 /*
6863 * If we are near the left edge, keep the left edge
6864 * at the edge of the view.
6865 */
6866 hs = left;
6867 } else if (left > hs) {
6868 /*
6869 * Is there whitespace visible at the left? Fix it if so.
6870 */
6871 hs = left;
6872 } else if (right < hs + hspace) {
6873 /*
6874 * Is there whitespace visible at the right? Fix it if so.
6875 */
6876 hs = right - hspace;
6877 } else {
6878 /*
6879 * Otherwise, float as needed.
6880 */
6881 if (x - hs < hslack) {
6882 hs = x - hslack;
6883 }
6884 if (x - hs > hspace - hslack) {
6885 hs = x - (hspace - hslack);
6886 }
6887 }
6888 }
6889
6890 if (hs != mScrollX || vs != mScrollY) {
6891 if (mScroller == null) {
6892 scrollTo(hs, vs);
6893 } else {
6894 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6895 int dx = hs - mScrollX;
6896 int dy = vs - mScrollY;
6897
6898 if (duration > ANIMATED_SCROLL_GAP) {
6899 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
Mike Cleronf116bf82009-09-27 19:14:12 -07006900 awakenScrollBars(mScroller.getDuration());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006901 invalidate();
6902 } else {
6903 if (!mScroller.isFinished()) {
6904 mScroller.abortAnimation();
6905 }
6906
6907 scrollBy(dx, dy);
6908 }
6909
6910 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6911 }
6912
6913 changed = true;
6914 }
6915
6916 if (isFocused()) {
Gilles Debunne716dbf62011-03-07 18:12:10 -08006917 // This offsets because getInterestingRect() is in terms of viewport coordinates, but
6918 // requestRectangleOnScreen() is in terms of content coordinates.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006919
Gilles Debunne716dbf62011-03-07 18:12:10 -08006920 if (mTempRect == null) mTempRect = new Rect();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07006921 // The offsets here are to ensure the rectangle we are using is
6922 // within our view bounds, in case the cursor is on the far left
6923 // or right. If it isn't withing the bounds, then this request
6924 // will be ignored.
6925 mTempRect.set(x - 2, top, x + 2, bottom);
Gilles Debunne716dbf62011-03-07 18:12:10 -08006926 getInterestingRect(mTempRect, line);
6927 mTempRect.offset(mScrollX, mScrollY);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006928
Gilles Debunne716dbf62011-03-07 18:12:10 -08006929 if (requestRectangleOnScreen(mTempRect)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006930 changed = true;
6931 }
6932 }
6933
6934 return changed;
6935 }
6936
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006937 /**
6938 * Move the cursor, if needed, so that it is at an offset that is visible
6939 * to the user. This will not move the cursor if it represents more than
6940 * one character (a selection range). This will only work if the
6941 * TextView contains spannable text; otherwise it will do nothing.
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07006942 *
Gilles Debunne57f4e5b2010-06-21 16:21:51 -07006943 * @return True if the cursor was actually moved, false otherwise.
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006944 */
6945 public boolean moveCursorToVisibleOffset() {
6946 if (!(mText instanceof Spannable)) {
6947 return false;
6948 }
Gilles Debunne05336272010-07-09 20:13:45 -07006949 int start = getSelectionStart();
6950 int end = getSelectionEnd();
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006951 if (start != end) {
6952 return false;
6953 }
6954
6955 // First: make sure the line is visible on screen:
6956
6957 int line = mLayout.getLineForOffset(start);
6958
6959 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006960 final int bottom = mLayout.getLineTop(line + 1);
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006961 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6962 int vslack = (bottom - top) / 2;
6963 if (vslack > vspace / 4)
6964 vslack = vspace / 4;
6965 final int vs = mScrollY;
6966
6967 if (top < (vs+vslack)) {
6968 line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
6969 } else if (bottom > (vspace+vs-vslack)) {
6970 line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
6971 }
6972
6973 // Next: make sure the character is visible on screen:
6974
6975 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6976 final int hs = mScrollX;
6977 final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
6978 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
6979
Doug Feltc982f602010-05-25 11:51:40 -07006980 // line might contain bidirectional text
6981 final int lowChar = leftChar < rightChar ? leftChar : rightChar;
6982 final int highChar = leftChar > rightChar ? leftChar : rightChar;
6983
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006984 int newStart = start;
Doug Feltc982f602010-05-25 11:51:40 -07006985 if (newStart < lowChar) {
6986 newStart = lowChar;
6987 } else if (newStart > highChar) {
6988 newStart = highChar;
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006989 }
6990
6991 if (newStart != start) {
6992 Selection.setSelection((Spannable)mText, newStart);
6993 return true;
6994 }
6995
6996 return false;
6997 }
6998
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006999 @Override
7000 public void computeScroll() {
7001 if (mScroller != null) {
7002 if (mScroller.computeScrollOffset()) {
7003 mScrollX = mScroller.getCurrX();
7004 mScrollY = mScroller.getCurrY();
Romain Guy0fd89bf2011-01-26 15:41:30 -08007005 invalidateParentCaches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007006 postInvalidate(); // So we draw again
7007 }
7008 }
7009 }
7010
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007011 private void getInterestingRect(Rect r, int line) {
7012 convertFromViewportToContentCoordinates(r);
7013
7014 // Rectangle can can be expanded on first and last line to take
7015 // padding into account.
7016 // TODO Take left/right padding into account too?
7017 if (line == 0) r.top -= getExtendedPaddingTop();
7018 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
7019 }
7020
7021 private void convertFromViewportToContentCoordinates(Rect r) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007022 final int horizontalOffset = viewportToContentHorizontalOffset();
7023 r.left += horizontalOffset;
7024 r.right += horizontalOffset;
7025
7026 final int verticalOffset = viewportToContentVerticalOffset();
7027 r.top += verticalOffset;
7028 r.bottom += verticalOffset;
7029 }
7030
7031 private int viewportToContentHorizontalOffset() {
7032 return getCompoundPaddingLeft() - mScrollX;
7033 }
7034
7035 private int viewportToContentVerticalOffset() {
7036 int offset = getExtendedPaddingTop() - mScrollY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007037 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007038 offset += getVerticalOffset(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007039 }
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007040 return offset;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007041 }
7042
7043 @Override
7044 public void debug(int depth) {
7045 super.debug(depth);
7046
7047 String output = debugIndent(depth);
7048 output += "frame={" + mLeft + ", " + mTop + ", " + mRight
7049 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
7050 + "} ";
7051
7052 if (mText != null) {
7053
7054 output += "mText=\"" + mText + "\" ";
7055 if (mLayout != null) {
7056 output += "mLayout width=" + mLayout.getWidth()
7057 + " height=" + mLayout.getHeight();
7058 }
7059 } else {
7060 output += "mText=NULL";
7061 }
7062 Log.d(VIEW_LOG_TAG, output);
7063 }
7064
7065 /**
7066 * Convenience for {@link Selection#getSelectionStart}.
7067 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007068 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007069 public int getSelectionStart() {
7070 return Selection.getSelectionStart(getText());
7071 }
7072
7073 /**
7074 * Convenience for {@link Selection#getSelectionEnd}.
7075 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007076 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007077 public int getSelectionEnd() {
7078 return Selection.getSelectionEnd(getText());
7079 }
7080
7081 /**
7082 * Return true iff there is a selection inside this text view.
7083 */
7084 public boolean hasSelection() {
Gilles Debunne03789e82010-09-07 19:07:17 -07007085 final int selectionStart = getSelectionStart();
7086 final int selectionEnd = getSelectionEnd();
7087
7088 return selectionStart >= 0 && selectionStart != selectionEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007089 }
7090
7091 /**
7092 * Sets the properties of this field (lines, horizontally scrolling,
7093 * transformation method) to be for a single-line input.
7094 *
7095 * @attr ref android.R.styleable#TextView_singleLine
7096 */
7097 public void setSingleLine() {
7098 setSingleLine(true);
7099 }
7100
7101 /**
Adam Powell7f8f79a2011-07-07 18:35:54 -07007102 * Sets the properties of this field to transform input to ALL CAPS
7103 * display. This may use a "small caps" formatting if available.
7104 * This setting will be ignored if this field is editable or selectable.
7105 *
7106 * This call replaces the current transformation method. Disabling this
7107 * will not necessarily restore the previous behavior from before this
7108 * was enabled.
7109 *
7110 * @see #setTransformationMethod(TransformationMethod)
7111 * @attr ref android.R.styleable#TextView_textAllCaps
7112 */
7113 public void setAllCaps(boolean allCaps) {
7114 if (allCaps) {
7115 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
7116 } else {
7117 setTransformationMethod(null);
7118 }
7119 }
7120
7121 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007122 * If true, sets the properties of this field (number of lines, horizontally scrolling,
7123 * transformation method) to be for a single-line input; if false, restores these to the default
7124 * conditions.
7125 *
7126 * Note that the default conditions are not necessarily those that were in effect prior this
7127 * method, and you may want to reset these properties to your custom values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007128 *
7129 * @attr ref android.R.styleable#TextView_singleLine
7130 */
7131 @android.view.RemotableViewMethod
7132 public void setSingleLine(boolean singleLine) {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007133 // Could be used, but may break backward compatibility.
7134 // if (mSingleLine == singleLine) return;
Gilles Debunned7483bf2010-11-10 10:47:45 -08007135 setInputTypeSingleLine(singleLine);
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007136 applySingleLine(singleLine, true, true);
Gilles Debunned7483bf2010-11-10 10:47:45 -08007137 }
7138
7139 /**
7140 * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
7141 * @param singleLine
7142 */
7143 private void setInputTypeSingleLine(boolean singleLine) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007144 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007145 if (singleLine) {
7146 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7147 } else {
7148 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7149 }
7150 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007151 }
7152
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007153 private void applySingleLine(boolean singleLine, boolean applyTransformation,
7154 boolean changeMaxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007155 mSingleLine = singleLine;
7156 if (singleLine) {
7157 setLines(1);
7158 setHorizontallyScrolling(true);
7159 if (applyTransformation) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007160 setTransformationMethod(SingleLineTransformationMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007161 }
7162 } else {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007163 if (changeMaxLines) {
7164 setMaxLines(Integer.MAX_VALUE);
7165 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007166 setHorizontallyScrolling(false);
7167 if (applyTransformation) {
7168 setTransformationMethod(null);
7169 }
7170 }
7171 }
Gilles Debunneb2316962010-12-21 17:32:43 -08007172
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007173 /**
7174 * Causes words in the text that are longer than the view is wide
7175 * to be ellipsized instead of broken in the middle. You may also
7176 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
Kenny Roote855d132009-06-11 11:00:42 -05007177 * to constrain the text to a single line. Use <code>null</code>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007178 * to turn off ellipsizing.
7179 *
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007180 * If {@link #setMaxLines} has been used to set two or more lines,
Gilles Debunne6435a562011-08-04 21:22:30 -07007181 * {@link android.text.TextUtils.TruncateAt#END} and
7182 * {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported
7183 * (other ellipsizing types will not do anything).
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007184 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007185 * @attr ref android.R.styleable#TextView_ellipsize
7186 */
7187 public void setEllipsize(TextUtils.TruncateAt where) {
Gilles Debunne22378292011-08-12 10:38:52 -07007188 // TruncateAt is an enum. != comparison is ok between these singleton objects.
7189 if (mEllipsize != where) {
7190 mEllipsize = where;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007191
Gilles Debunne22378292011-08-12 10:38:52 -07007192 if (mLayout != null) {
7193 nullLayouts();
7194 requestLayout();
7195 invalidate();
7196 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007197 }
7198 }
7199
7200 /**
7201 * Sets how many times to repeat the marquee animation. Only applied if the
7202 * TextView has marquee enabled. Set to -1 to repeat indefinitely.
7203 *
7204 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
7205 */
7206 public void setMarqueeRepeatLimit(int marqueeLimit) {
7207 mMarqueeRepeatLimit = marqueeLimit;
7208 }
7209
7210 /**
7211 * Returns where, if anywhere, words that are longer than the view
7212 * is wide should be ellipsized.
7213 */
7214 @ViewDebug.ExportedProperty
7215 public TextUtils.TruncateAt getEllipsize() {
7216 return mEllipsize;
7217 }
7218
7219 /**
7220 * Set the TextView so that when it takes focus, all the text is
7221 * selected.
7222 *
7223 * @attr ref android.R.styleable#TextView_selectAllOnFocus
7224 */
7225 @android.view.RemotableViewMethod
7226 public void setSelectAllOnFocus(boolean selectAllOnFocus) {
7227 mSelectAllOnFocus = selectAllOnFocus;
7228
7229 if (selectAllOnFocus && !(mText instanceof Spannable)) {
7230 setText(mText, BufferType.SPANNABLE);
7231 }
7232 }
7233
7234 /**
7235 * Set whether the cursor is visible. The default is true.
7236 *
7237 * @attr ref android.R.styleable#TextView_cursorVisible
7238 */
7239 @android.view.RemotableViewMethod
7240 public void setCursorVisible(boolean visible) {
Gilles Debunne3d010062011-02-18 14:16:41 -08007241 if (mCursorVisible != visible) {
7242 mCursorVisible = visible;
7243 invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007244
Gilles Debunne3d010062011-02-18 14:16:41 -08007245 makeBlink();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007246
Gilles Debunne3d010062011-02-18 14:16:41 -08007247 // InsertionPointCursorController depends on mCursorVisible
7248 prepareCursorControllers();
7249 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007250 }
7251
Gilles Debunne98dbfd42011-01-24 12:54:10 -08007252 private boolean isCursorVisible() {
7253 return mCursorVisible && isTextEditable();
7254 }
7255
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007256 private boolean canMarquee() {
7257 int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
Adam Powell282e3772011-08-30 16:51:11 -07007258 return width > 0 && (mLayout.getLineWidth(0) > width ||
7259 (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null &&
7260 mSavedMarqueeModeLayout.getLineWidth(0) > width));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007261 }
7262
7263 private void startMarquee() {
Romain Guy4dc4f732009-06-19 15:16:40 -07007264 // Do not ellipsize EditText
7265 if (mInput != null) return;
7266
Romain Guy939151f2009-04-08 14:22:40 -07007267 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
7268 return;
7269 }
7270
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007271 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
7272 getLineCount() == 1 && canMarquee()) {
Romain Guy939151f2009-04-08 14:22:40 -07007273
Adam Powell282e3772011-08-30 16:51:11 -07007274 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
7275 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
7276 final Layout tmp = mLayout;
7277 mLayout = mSavedMarqueeModeLayout;
7278 mSavedMarqueeModeLayout = tmp;
7279 setHorizontalFadingEdgeEnabled(true);
7280 requestLayout();
7281 invalidate();
7282 }
7283
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007284 if (mMarquee == null) mMarquee = new Marquee(this);
7285 mMarquee.start(mMarqueeRepeatLimit);
7286 }
7287 }
7288
7289 private void stopMarquee() {
7290 if (mMarquee != null && !mMarquee.isStopped()) {
7291 mMarquee.stop();
7292 }
Adam Powell282e3772011-08-30 16:51:11 -07007293
7294 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
7295 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
7296 final Layout tmp = mSavedMarqueeModeLayout;
7297 mSavedMarqueeModeLayout = mLayout;
7298 mLayout = tmp;
7299 setHorizontalFadingEdgeEnabled(false);
7300 requestLayout();
7301 invalidate();
7302 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007303 }
7304
7305 private void startStopMarquee(boolean start) {
7306 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7307 if (start) {
7308 startMarquee();
7309 } else {
7310 stopMarquee();
7311 }
7312 }
7313 }
7314
7315 private static final class Marquee extends Handler {
7316 // TODO: Add an option to configure this
Romain Guy939151f2009-04-08 14:22:40 -07007317 private static final float MARQUEE_DELTA_MAX = 0.07f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007318 private static final int MARQUEE_DELAY = 1200;
7319 private static final int MARQUEE_RESTART_DELAY = 1200;
7320 private static final int MARQUEE_RESOLUTION = 1000 / 30;
7321 private static final int MARQUEE_PIXELS_PER_SECOND = 30;
7322
7323 private static final byte MARQUEE_STOPPED = 0x0;
7324 private static final byte MARQUEE_STARTING = 0x1;
7325 private static final byte MARQUEE_RUNNING = 0x2;
7326
7327 private static final int MESSAGE_START = 0x1;
7328 private static final int MESSAGE_TICK = 0x2;
7329 private static final int MESSAGE_RESTART = 0x3;
7330
7331 private final WeakReference<TextView> mView;
7332
7333 private byte mStatus = MARQUEE_STOPPED;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007334 private final float mScrollUnit;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007335 private float mMaxScroll;
Romain Guyc2303192009-04-03 17:37:18 -07007336 float mMaxFadeScroll;
7337 private float mGhostStart;
7338 private float mGhostOffset;
7339 private float mFadeStop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007340 private int mRepeatLimit;
7341
7342 float mScroll;
7343
7344 Marquee(TextView v) {
7345 final float density = v.getContext().getResources().getDisplayMetrics().density;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007346 mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007347 mView = new WeakReference<TextView>(v);
7348 }
7349
7350 @Override
7351 public void handleMessage(Message msg) {
7352 switch (msg.what) {
7353 case MESSAGE_START:
7354 mStatus = MARQUEE_RUNNING;
7355 tick();
7356 break;
7357 case MESSAGE_TICK:
7358 tick();
7359 break;
7360 case MESSAGE_RESTART:
7361 if (mStatus == MARQUEE_RUNNING) {
7362 if (mRepeatLimit >= 0) {
7363 mRepeatLimit--;
7364 }
7365 start(mRepeatLimit);
7366 }
7367 break;
7368 }
7369 }
7370
7371 void tick() {
7372 if (mStatus != MARQUEE_RUNNING) {
7373 return;
7374 }
7375
7376 removeMessages(MESSAGE_TICK);
7377
7378 final TextView textView = mView.get();
7379 if (textView != null && (textView.isFocused() || textView.isSelected())) {
7380 mScroll += mScrollUnit;
7381 if (mScroll > mMaxScroll) {
7382 mScroll = mMaxScroll;
7383 sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
7384 } else {
7385 sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
7386 }
7387 textView.invalidate();
7388 }
7389 }
7390
7391 void stop() {
7392 mStatus = MARQUEE_STOPPED;
7393 removeMessages(MESSAGE_START);
7394 removeMessages(MESSAGE_RESTART);
7395 removeMessages(MESSAGE_TICK);
7396 resetScroll();
7397 }
7398
7399 private void resetScroll() {
7400 mScroll = 0.0f;
7401 final TextView textView = mView.get();
7402 if (textView != null) textView.invalidate();
7403 }
7404
7405 void start(int repeatLimit) {
7406 if (repeatLimit == 0) {
7407 stop();
7408 return;
7409 }
7410 mRepeatLimit = repeatLimit;
7411 final TextView textView = mView.get();
7412 if (textView != null && textView.mLayout != null) {
7413 mStatus = MARQUEE_STARTING;
7414 mScroll = 0.0f;
Romain Guyc2303192009-04-03 17:37:18 -07007415 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
7416 textView.getCompoundPaddingRight();
7417 final float lineWidth = textView.mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07007418 final float gap = textWidth / 3.0f;
7419 mGhostStart = lineWidth - textWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007420 mMaxScroll = mGhostStart + textWidth;
Romain Guy3373ed62009-05-04 14:13:32 -07007421 mGhostOffset = lineWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007422 mFadeStop = lineWidth + textWidth / 6.0f;
7423 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
7424
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007425 textView.invalidate();
7426 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
7427 }
7428 }
7429
Romain Guyc2303192009-04-03 17:37:18 -07007430 float getGhostOffset() {
7431 return mGhostOffset;
7432 }
7433
7434 boolean shouldDrawLeftFade() {
7435 return mScroll <= mFadeStop;
7436 }
7437
7438 boolean shouldDrawGhost() {
7439 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
7440 }
7441
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007442 boolean isRunning() {
7443 return mStatus == MARQUEE_RUNNING;
7444 }
7445
7446 boolean isStopped() {
7447 return mStatus == MARQUEE_STOPPED;
7448 }
7449 }
7450
7451 /**
Gilles Debunne4469e602011-03-09 14:38:04 -08007452 * This method is called when the text is changed, in case any subclasses
7453 * would like to know.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007454 *
Gilles Debunne4469e602011-03-09 14:38:04 -08007455 * Within <code>text</code>, the <code>lengthAfter</code> characters
7456 * beginning at <code>start</code> have just replaced old text that had
7457 * length <code>lengthBefore</code>. It is an error to attempt to make
7458 * changes to <code>text</code> from this callback.
7459 *
7460 * @param text The text the TextView is displaying
7461 * @param start The offset of the start of the range of the text that was
7462 * modified
7463 * @param lengthBefore The length of the former text that has been replaced
7464 * @param lengthAfter The length of the replacement modified text
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007465 */
Gilles Debunne4469e602011-03-09 14:38:04 -08007466 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
Gilles Debunne6435a562011-08-04 21:22:30 -07007467 // intentionally empty, template pattern method can be overridden by subclasses
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007468 }
7469
7470 /**
7471 * This method is called when the selection has changed, in case any
7472 * subclasses would like to know.
7473 *
7474 * @param selStart The new selection start location.
7475 * @param selEnd The new selection end location.
7476 */
7477 protected void onSelectionChanged(int selStart, int selEnd) {
Svetoslav Ganova0156172011-06-26 17:55:44 -07007478 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
Gilles Debunne6435a562011-08-04 21:22:30 -07007479 if (mSpellChecker != null) {
7480 mSpellChecker.onSelectionChanged();
7481 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007482 }
Svetoslav Ganova0156172011-06-26 17:55:44 -07007483
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007484 /**
7485 * Adds a TextWatcher to the list of those whose methods are called
7486 * whenever this TextView's text changes.
7487 * <p>
7488 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
7489 * not called after {@link #setText} calls. Now, doing {@link #setText}
7490 * if there are any text changed listeners forces the buffer type to
7491 * Editable if it would not otherwise be and does call this method.
7492 */
7493 public void addTextChangedListener(TextWatcher watcher) {
7494 if (mListeners == null) {
7495 mListeners = new ArrayList<TextWatcher>();
7496 }
7497
7498 mListeners.add(watcher);
7499 }
7500
7501 /**
7502 * Removes the specified TextWatcher from the list of those whose
7503 * methods are called
7504 * whenever this TextView's text changes.
7505 */
7506 public void removeTextChangedListener(TextWatcher watcher) {
7507 if (mListeners != null) {
7508 int i = mListeners.indexOf(watcher);
7509
7510 if (i >= 0) {
7511 mListeners.remove(i);
7512 }
7513 }
7514 }
7515
Gilles Debunne6435a562011-08-04 21:22:30 -07007516 private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007517 if (mListeners != null) {
7518 final ArrayList<TextWatcher> list = mListeners;
7519 final int count = list.size();
7520 for (int i = 0; i < count; i++) {
7521 list.get(i).beforeTextChanged(text, start, before, after);
7522 }
7523 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007524
7525 // The spans that are inside or intersect the modified region no longer make sense
7526 removeIntersectingSpans(start, start + before, SpellCheckSpan.class);
7527 removeIntersectingSpans(start, start + before, SuggestionSpan.class);
7528 }
7529
7530 // Removes all spans that are inside or actually overlap the start..end range
7531 private <T> void removeIntersectingSpans(int start, int end, Class<T> type) {
7532 if (!(mText instanceof Editable)) return;
7533 Editable text = (Editable) mText;
7534
7535 T[] spans = text.getSpans(start, end, type);
7536 final int length = spans.length;
7537 for (int i = 0; i < length; i++) {
7538 final int s = text.getSpanStart(spans[i]);
7539 final int e = text.getSpanEnd(spans[i]);
7540 if (e == start || s == end) break;
7541 text.removeSpan(spans[i]);
7542 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007543 }
7544
7545 /**
7546 * Not private so it can be called from an inner class without going
7547 * through a thunk.
7548 */
Gilles Debunne6435a562011-08-04 21:22:30 -07007549 void sendOnTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007550 if (mListeners != null) {
7551 final ArrayList<TextWatcher> list = mListeners;
7552 final int count = list.size();
7553 for (int i = 0; i < count; i++) {
7554 list.get(i).onTextChanged(text, start, before, after);
7555 }
7556 }
7557 }
7558
7559 /**
7560 * Not private so it can be called from an inner class without going
7561 * through a thunk.
7562 */
7563 void sendAfterTextChanged(Editable text) {
7564 if (mListeners != null) {
7565 final ArrayList<TextWatcher> list = mListeners;
7566 final int count = list.size();
7567 for (int i = 0; i < count; i++) {
7568 list.get(i).afterTextChanged(text);
7569 }
7570 }
7571 }
7572
7573 /**
7574 * Not private so it can be called from an inner class without going
7575 * through a thunk.
7576 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007577 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007578 final InputMethodState ims = mInputMethodState;
7579 if (ims == null || ims.mBatchEditNesting == 0) {
7580 updateAfterEdit();
7581 }
7582 if (ims != null) {
7583 ims.mContentChanged = true;
7584 if (ims.mChangedStart < 0) {
7585 ims.mChangedStart = start;
7586 ims.mChangedEnd = start+before;
7587 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01007588 ims.mChangedStart = Math.min(ims.mChangedStart, start);
7589 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007590 }
7591 ims.mChangedDelta += after-before;
7592 }
7593
7594 sendOnTextChanged(buffer, start, before, after);
7595 onTextChanged(buffer, start, before, after);
Adam Powellba0a2c32010-09-28 17:41:23 -07007596
Gilles Debunne6435a562011-08-04 21:22:30 -07007597 // The WordIterator text change listener may be called after this one.
7598 // Make sure this changed text is rescanned before the iterator is used on it.
7599 getWordIterator().forceUpdate();
7600 updateSpellCheckSpans(start, start + after);
7601
Gilles Debunned94f8c52011-01-10 11:29:15 -08007602 // Hide the controllers if the amount of content changed
Adam Powellba0a2c32010-09-28 17:41:23 -07007603 if (before != after) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007604 // We do not hide the span controllers, as they can be added when a new text is
7605 // inserted into the text view
7606 hideCursorControllers();
Adam Powellba0a2c32010-09-28 17:41:23 -07007607 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007608 }
7609
7610 /**
7611 * Not private so it can be called from an inner class without going
7612 * through a thunk.
7613 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007614 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007615 // XXX Make the start and end move together if this ends up
7616 // spending too much time invalidating.
7617
7618 boolean selChanged = false;
7619 int newSelStart=-1, newSelEnd=-1;
7620
7621 final InputMethodState ims = mInputMethodState;
7622
7623 if (what == Selection.SELECTION_END) {
7624 mHighlightPathBogus = true;
7625 selChanged = true;
7626 newSelEnd = newStart;
7627
7628 if (!isFocused()) {
7629 mSelectionMoved = true;
7630 }
7631
7632 if (oldStart >= 0 || newStart >= 0) {
7633 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
7634 registerForPreDraw();
Gilles Debunne3d010062011-02-18 14:16:41 -08007635 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007636 }
7637 }
7638
7639 if (what == Selection.SELECTION_START) {
7640 mHighlightPathBogus = true;
7641 selChanged = true;
7642 newSelStart = newStart;
7643
7644 if (!isFocused()) {
7645 mSelectionMoved = true;
7646 }
7647
7648 if (oldStart >= 0 || newStart >= 0) {
7649 int end = Selection.getSelectionEnd(buf);
7650 invalidateCursor(end, oldStart, newStart);
7651 }
7652 }
7653
7654 if (selChanged) {
7655 if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
7656 if (newSelStart < 0) {
7657 newSelStart = Selection.getSelectionStart(buf);
7658 }
7659 if (newSelEnd < 0) {
7660 newSelEnd = Selection.getSelectionEnd(buf);
7661 }
7662 onSelectionChanged(newSelStart, newSelEnd);
7663 }
7664 }
7665
7666 if (what instanceof UpdateAppearance ||
7667 what instanceof ParagraphStyle) {
7668 if (ims == null || ims.mBatchEditNesting == 0) {
7669 invalidate();
7670 mHighlightPathBogus = true;
7671 checkForResize();
7672 } else {
7673 ims.mContentChanged = true;
7674 }
7675 }
7676
7677 if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7678 mHighlightPathBogus = true;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007679 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7680 ims.mSelectionModeChanged = true;
7681 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007682
7683 if (Selection.getSelectionStart(buf) >= 0) {
7684 if (ims == null || ims.mBatchEditNesting == 0) {
7685 invalidateCursor();
7686 } else {
7687 ims.mCursorChanged = true;
7688 }
7689 }
7690 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007691
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007692 if (what instanceof ParcelableSpan) {
7693 // If this is a span that can be sent to a remote process,
7694 // the current extract editor would be interested in it.
7695 if (ims != null && ims.mExtracting != null) {
7696 if (ims.mBatchEditNesting != 0) {
7697 if (oldStart >= 0) {
7698 if (ims.mChangedStart > oldStart) {
7699 ims.mChangedStart = oldStart;
7700 }
7701 if (ims.mChangedStart > oldEnd) {
7702 ims.mChangedStart = oldEnd;
7703 }
7704 }
7705 if (newStart >= 0) {
7706 if (ims.mChangedStart > newStart) {
7707 ims.mChangedStart = newStart;
7708 }
7709 if (ims.mChangedStart > newEnd) {
7710 ims.mChangedStart = newEnd;
7711 }
7712 }
7713 } else {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007714 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007715 + oldStart + "-" + oldEnd + ","
7716 + newStart + "-" + newEnd + what);
7717 ims.mContentChanged = true;
7718 }
7719 }
7720 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007721
7722 if (what instanceof SpellCheckSpan) {
7723 if (newStart < 0) {
7724 getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
7725 } else if (oldStart < 0) {
7726 getSpellChecker().addSpellCheckSpan((SpellCheckSpan) what);
7727 }
7728 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007729 }
7730
Gilles Debunne6435a562011-08-04 21:22:30 -07007731 /**
7732 * Create new SpellCheckSpans on the modified region.
7733 */
7734 private void updateSpellCheckSpans(int start, int end) {
7735 if (!(mText instanceof Editable) || !isSuggestionsEnabled()) return;
7736 Editable text = (Editable) mText;
7737
7738 WordIterator wordIterator = getWordIterator();
7739 wordIterator.setCharSequence(text);
7740
7741 // Move back to the beginning of the current word, if any
7742 int wordStart = wordIterator.preceding(start);
7743 int wordEnd;
7744 if (wordStart == BreakIterator.DONE) {
7745 wordEnd = wordIterator.following(start);
7746 if (wordEnd != BreakIterator.DONE) {
7747 wordStart = wordIterator.getBeginning(wordEnd);
7748 }
7749 } else {
7750 wordEnd = wordIterator.getEnd(wordStart);
7751 }
7752 if (wordEnd == BreakIterator.DONE) {
7753 return;
7754 }
7755
7756 // Iterate over the newly added text and schedule new SpellCheckSpans
7757 while (wordStart <= end) {
7758 if (wordEnd >= start) {
7759 // A word across the interval boundaries must remove boundary edition spans
7760 if (wordStart < start && wordEnd > start) {
7761 removeEditionSpansAt(start, text);
7762 }
7763
7764 if (wordStart < end && wordEnd > end) {
7765 removeEditionSpansAt(end, text);
7766 }
7767
7768 // Do not create new boundary spans if they already exist
7769 boolean createSpellCheckSpan = true;
7770 if (wordEnd == start) {
7771 SpellCheckSpan[] spellCheckSpans = text.getSpans(start, start,
7772 SpellCheckSpan.class);
7773 if (spellCheckSpans.length > 0) createSpellCheckSpan = false;
7774 }
7775
7776 if (wordStart == end) {
7777 SpellCheckSpan[] spellCheckSpans = text.getSpans(end, end,
7778 SpellCheckSpan.class);
7779 if (spellCheckSpans.length > 0) createSpellCheckSpan = false;
7780 }
7781
7782 if (createSpellCheckSpan) {
7783 text.setSpan(new SpellCheckSpan(), wordStart, wordEnd,
7784 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
7785 }
7786 }
7787
7788 // iterate word by word
7789 wordEnd = wordIterator.following(wordEnd);
7790 if (wordEnd == BreakIterator.DONE) return;
7791 wordStart = wordIterator.getBeginning(wordEnd);
7792 if (wordStart == BreakIterator.DONE) {
7793 Log.e(LOG_TAG, "Unable to find word beginning from " + wordEnd + "in " + mText);
7794 return;
7795 }
7796 }
7797 }
7798
7799 private static void removeEditionSpansAt(int offset, Editable text) {
7800 SuggestionSpan[] suggestionSpans = text.getSpans(offset, offset, SuggestionSpan.class);
7801 for (int i = 0; i < suggestionSpans.length; i++) {
7802 text.removeSpan(suggestionSpans[i]);
7803 }
7804 SpellCheckSpan[] spellCheckSpans = text.getSpans(offset, offset, SpellCheckSpan.class);
7805 for (int i = 0; i < spellCheckSpans.length; i++) {
7806 text.removeSpan(spellCheckSpans[i]);
7807 }
7808 }
7809
Luca Zanoline6d36822011-08-30 18:04:34 +01007810 /**
7811 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
7812 * pop-up should be displayed.
7813 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007814 private class EasyEditSpanController {
Luca Zanoline6d36822011-08-30 18:04:34 +01007815
Luca Zanolin1564fc72011-09-07 00:01:28 +01007816 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
Luca Zanoline6d36822011-08-30 18:04:34 +01007817
Luca Zanolin1564fc72011-09-07 00:01:28 +01007818 private EasyEditPopupWindow mPopupWindow;
7819
7820 private EasyEditSpan mEasyEditSpan;
7821
7822 private Runnable mHidePopup;
Luca Zanoline6d36822011-08-30 18:04:34 +01007823
7824 private void hide() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007825 if (mPopupWindow != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007826 mPopupWindow.hide();
Luca Zanolin1564fc72011-09-07 00:01:28 +01007827 TextView.this.removeCallbacks(mHidePopup);
Luca Zanoline6d36822011-08-30 18:04:34 +01007828 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007829 removeSpans(mText);
7830 mEasyEditSpan = null;
Luca Zanoline6d36822011-08-30 18:04:34 +01007831 }
7832
7833 /**
7834 * Monitors the changes in the text.
7835 *
7836 * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
7837 * as the notifications are not sent when a spannable (with spans) is inserted.
7838 */
7839 public void onTextChange(CharSequence buffer) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007840 adjustSpans(mText);
7841
7842 if (getWindowVisibility() != View.VISIBLE) {
7843 // The window is not visible yet, ignore the text change.
7844 return;
Luca Zanoline6d36822011-08-30 18:04:34 +01007845 }
7846
Luca Zanolin1564fc72011-09-07 00:01:28 +01007847 if (mLayout == null) {
7848 // The view has not been layout yet, ignore the text change
7849 return;
7850 }
7851
7852 InputMethodManager imm = InputMethodManager.peekInstance();
7853 if (!(TextView.this instanceof ExtractEditText)
7854 && imm != null && imm.isFullscreenMode()) {
7855 // The input is in extract mode. We do not have to handle the easy edit in the
7856 // original TextView, as the ExtractEditText will do
7857 return;
7858 }
7859
7860 // Remove the current easy edit span, as the text changed, and remove the pop-up
7861 // (if any)
7862 if (mEasyEditSpan != null) {
7863 if (mText instanceof Spannable) {
7864 ((Spannable) mText).removeSpan(mEasyEditSpan);
7865 }
7866 mEasyEditSpan = null;
7867 }
7868 if (mPopupWindow != null && mPopupWindow.isShowing()) {
7869 mPopupWindow.hide();
7870 }
7871
7872 // Display the new easy edit span (if any).
Luca Zanoline6d36822011-08-30 18:04:34 +01007873 if (buffer instanceof Spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007874 mEasyEditSpan = getSpan((Spanned) buffer);
7875 if (mEasyEditSpan != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007876 if (mPopupWindow == null) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007877 mPopupWindow = new EasyEditPopupWindow();
7878 mHidePopup = new Runnable() {
7879 @Override
7880 public void run() {
7881 hide();
7882 }
7883 };
Luca Zanoline6d36822011-08-30 18:04:34 +01007884 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007885 mPopupWindow.show(mEasyEditSpan);
7886 TextView.this.removeCallbacks(mHidePopup);
7887 TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
7888 }
7889 }
7890 }
7891
7892 /**
7893 * Adjusts the spans by removing all of them except the last one.
7894 */
7895 private void adjustSpans(CharSequence buffer) {
7896 // This method enforces that only one easy edit span is attached to the text.
7897 // A better way to enforce this would be to listen for onSpanAdded, but this method
7898 // cannot be used in this scenario as no notification is triggered when a text with
7899 // spans is inserted into a text.
7900 if (buffer instanceof Spannable) {
7901 Spannable spannable = (Spannable) buffer;
7902 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7903 EasyEditSpan.class);
7904 for (int i = 0; i < spans.length - 1; i++) {
7905 spannable.removeSpan(spans[i]);
7906 }
7907 }
7908 }
7909
7910 /**
7911 * Removes all the {@link EasyEditSpan} currently attached.
7912 */
7913 private void removeSpans(CharSequence buffer) {
7914 if (buffer instanceof Spannable) {
7915 Spannable spannable = (Spannable) buffer;
7916 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7917 EasyEditSpan.class);
7918 for (int i = 0; i < spans.length; i++) {
7919 spannable.removeSpan(spans[i]);
Luca Zanoline6d36822011-08-30 18:04:34 +01007920 }
7921 }
7922 }
7923
7924 private EasyEditSpan getSpan(Spanned spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007925 EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
Luca Zanoline6d36822011-08-30 18:04:34 +01007926 EasyEditSpan.class);
Luca Zanolin1564fc72011-09-07 00:01:28 +01007927 if (easyEditSpans.length == 0) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007928 return null;
7929 } else {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007930 return easyEditSpans[0];
Luca Zanoline6d36822011-08-30 18:04:34 +01007931 }
7932 }
7933 }
7934
7935 /**
7936 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Luca Zanolin1564fc72011-09-07 00:01:28 +01007937 * by {@link EasyEditSpanController}.
Luca Zanoline6d36822011-08-30 18:04:34 +01007938 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007939 private class EasyEditPopupWindow extends PinnedPopupWindow
Luca Zanoline6d36822011-08-30 18:04:34 +01007940 implements OnClickListener {
7941 private static final int POPUP_TEXT_LAYOUT =
7942 com.android.internal.R.layout.text_edit_action_popup_text;
7943 private TextView mDeleteTextView;
Luca Zanolin1564fc72011-09-07 00:01:28 +01007944 private EasyEditSpan mEasyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007945
7946 @Override
7947 protected void createPopupWindow() {
7948 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
7949 com.android.internal.R.attr.textSelectHandleWindowStyle);
7950 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
7951 mPopupWindow.setClippingEnabled(true);
7952 }
7953
7954 @Override
7955 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07007956 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
7957 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
7958 mContentView = linearLayout;
Luca Zanoline6d36822011-08-30 18:04:34 +01007959 mContentView.setBackgroundResource(
7960 com.android.internal.R.drawable.text_edit_side_paste_window);
7961
7962 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
7963 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
7964
7965 LayoutParams wrapContent = new LayoutParams(
7966 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
7967
7968 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
7969 mDeleteTextView.setLayoutParams(wrapContent);
7970 mDeleteTextView.setText(com.android.internal.R.string.delete);
7971 mDeleteTextView.setOnClickListener(this);
7972 mContentView.addView(mDeleteTextView);
7973 }
7974
Luca Zanolin1564fc72011-09-07 00:01:28 +01007975 public void show(EasyEditSpan easyEditSpan) {
7976 mEasyEditSpan = easyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007977 super.show();
7978 }
7979
7980 @Override
7981 public void onClick(View view) {
7982 if (view == mDeleteTextView) {
7983 deleteText();
7984 }
7985 }
7986
7987 private void deleteText() {
7988 Editable editable = (Editable) mText;
Luca Zanolin1564fc72011-09-07 00:01:28 +01007989 int start = editable.getSpanStart(mEasyEditSpan);
7990 int end = editable.getSpanEnd(mEasyEditSpan);
Luca Zanoline6d36822011-08-30 18:04:34 +01007991 if (start >= 0 && end >= 0) {
7992 editable.delete(start, end);
7993 }
7994 }
7995
7996 @Override
7997 protected int getTextOffset() {
7998 // Place the pop-up at the end of the span
7999 Editable editable = (Editable) mText;
Luca Zanolin1564fc72011-09-07 00:01:28 +01008000 return editable.getSpanEnd(mEasyEditSpan);
Luca Zanoline6d36822011-08-30 18:04:34 +01008001 }
8002
8003 @Override
8004 protected int getVerticalLocalPosition(int line) {
8005 return mLayout.getLineBottom(line);
8006 }
8007
8008 @Override
8009 protected int clipVertically(int positionY) {
8010 // As we display the pop-up below the span, no vertical clipping is required.
8011 return positionY;
8012 }
8013 }
8014
Gilles Debunne6435a562011-08-04 21:22:30 -07008015 private class ChangeWatcher implements TextWatcher, SpanWatcher {
svetoslavganov75986cf2009-05-14 22:28:01 -07008016
8017 private CharSequence mBeforeText;
8018
Luca Zanolin1564fc72011-09-07 00:01:28 +01008019 private EasyEditSpanController mEasyEditSpanController;
Luca Zanoline6d36822011-08-30 18:04:34 +01008020
8021 private ChangeWatcher() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008022 mEasyEditSpanController = new EasyEditSpanController();
Luca Zanoline6d36822011-08-30 18:04:34 +01008023 }
8024
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008025 public void beforeTextChanged(CharSequence buffer, int start,
8026 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008027 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008028 + " before=" + before + " after=" + after + ": " + buffer);
svetoslavganov75986cf2009-05-14 22:28:01 -07008029
Amith Yamasani91ccdb52010-01-14 18:56:02 -08008030 if (AccessibilityManager.getInstance(mContext).isEnabled()
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008031 && !isPasswordInputType(mInputType)
8032 && !hasPasswordTransformationMethod()) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008033 mBeforeText = buffer.toString();
8034 }
8035
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008036 TextView.this.sendBeforeTextChanged(buffer, start, before, after);
8037 }
8038
8039 public void onTextChanged(CharSequence buffer, int start,
8040 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008041 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008042 + " before=" + before + " after=" + after + ": " + buffer);
8043 TextView.this.handleTextChanged(buffer, start, before, after);
svetoslavganov75986cf2009-05-14 22:28:01 -07008044
Luca Zanolin1564fc72011-09-07 00:01:28 +01008045 mEasyEditSpanController.onTextChange(buffer);
Luca Zanoline6d36822011-08-30 18:04:34 +01008046
svetoslavganov75986cf2009-05-14 22:28:01 -07008047 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
Gilles Debunne6435a562011-08-04 21:22:30 -07008048 (isFocused() || isSelected() && isShown())) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008049 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
8050 mBeforeText = null;
8051 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008052 }
8053
8054 public void afterTextChanged(Editable buffer) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008055 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008056 TextView.this.sendAfterTextChanged(buffer);
8057
Gilles Debunne6435a562011-08-04 21:22:30 -07008058 if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008059 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
8060 }
8061 }
8062
8063 public void onSpanChanged(Spannable buf,
8064 Object what, int s, int e, int st, int en) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008065 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008066 + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
8067 TextView.this.spanChange(buf, what, s, st, e, en);
8068 }
8069
8070 public void onSpanAdded(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008071 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008072 + " what=" + what + ": " + buf);
8073 TextView.this.spanChange(buf, what, -1, s, -1, e);
8074 }
8075
8076 public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008077 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008078 + " what=" + what + ": " + buf);
8079 TextView.this.spanChange(buf, what, s, -1, e, -1);
8080 }
Luca Zanoline6d36822011-08-30 18:04:34 +01008081
8082 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008083 mEasyEditSpanController.hide();
Luca Zanoline6d36822011-08-30 18:04:34 +01008084 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008085 }
8086
Romain Guydcc490f2010-02-24 17:59:35 -08008087 /**
8088 * @hide
8089 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008090 @Override
Romain Guya440b002010-02-24 15:57:54 -08008091 public void dispatchFinishTemporaryDetach() {
8092 mDispatchTemporaryDetach = true;
8093 super.dispatchFinishTemporaryDetach();
8094 mDispatchTemporaryDetach = false;
8095 }
8096
8097 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008098 public void onStartTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008099 super.onStartTemporaryDetach();
8100 // Only track when onStartTemporaryDetach() is called directly,
8101 // usually because this instance is an editable field in a list
8102 if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
Gilles Debunne4b2274f2011-02-25 15:18:03 -08008103
8104 // Because of View recycling in ListView, there is no easy way to know when a TextView with
8105 // selection becomes visible again. Until a better solution is found, stop text selection
8106 // mode (if any) as soon as this TextView is recycled.
Gilles Debunne4a7199a2011-07-11 14:58:27 -07008107 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008108 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008109
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008110 @Override
8111 public void onFinishTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008112 super.onFinishTemporaryDetach();
8113 // Only track when onStartTemporaryDetach() is called directly,
8114 // usually because this instance is an editable field in a list
8115 if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008116 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008117
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008118 @Override
8119 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
8120 if (mTemporaryDetach) {
8121 // If we are temporarily in the detach state, then do nothing.
8122 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8123 return;
8124 }
8125
8126 mShowCursor = SystemClock.uptimeMillis();
8127
8128 ensureEndedBatchEdit();
Gilles Debunne03789e82010-09-07 19:07:17 -07008129
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008130 if (focused) {
8131 int selStart = getSelectionStart();
8132 int selEnd = getSelectionEnd();
8133
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08008134 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
8135 // mode for these, unless there was a specific selection already started.
8136 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
8137 selEnd == mText.length();
8138 mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
8139
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008140 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008141 // If a tap was used to give focus to that view, move cursor at tap position.
Gilles Debunne64e54a62010-09-07 19:07:17 -07008142 // Has to be done before onTakeFocus, which can be overloaded.
Gilles Debunne380b6042010-10-08 16:12:11 -07008143 final int lastTapPosition = getLastTapPosition();
8144 if (lastTapPosition >= 0) {
8145 Selection.setSelection((Spannable) mText, lastTapPosition);
8146 }
Gilles Debunne2703a422010-08-23 15:14:03 -07008147
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008148 if (mMovement != null) {
8149 mMovement.onTakeFocus(this, (Spannable) mText, direction);
8150 }
8151
Gilles Debunne64e54a62010-09-07 19:07:17 -07008152 // The DecorView does not have focus when the 'Done' ExtractEditText button is
Joe Onoratoc6cc0f82011-04-12 11:53:13 -07008153 // pressed. Since it is the ViewAncestor's mView, it requests focus before
Gilles Debunne64e54a62010-09-07 19:07:17 -07008154 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
8155 // This special case ensure that we keep current selection in that case.
8156 // It would be better to know why the DecorView does not have focus at that time.
Gilles Debunne47fa8e82010-09-07 18:50:07 -07008157 if (((this instanceof ExtractEditText) || mSelectionMoved) &&
8158 selStart >= 0 && selEnd >= 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008159 /*
8160 * Someone intentionally set the selection, so let them
8161 * do whatever it is that they wanted to do instead of
8162 * the default on-focus behavior. We reset the selection
8163 * here instead of just skipping the onTakeFocus() call
8164 * because some movement methods do something other than
8165 * just setting the selection in theirs and we still
8166 * need to go through that path.
8167 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008168 Selection.setSelection((Spannable) mText, selStart, selEnd);
8169 }
Gilles Debunnef170a342010-11-11 11:08:59 -08008170
8171 if (mSelectAllOnFocus) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008172 selectAll();
Gilles Debunnef170a342010-11-11 11:08:59 -08008173 }
8174
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008175 mTouchFocusSelected = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008176 }
8177
8178 mFrozenWithFocus = false;
8179 mSelectionMoved = false;
8180
8181 if (mText instanceof Spannable) {
8182 Spannable sp = (Spannable) mText;
8183 MetaKeyKeyListener.resetMetaState(sp);
8184 }
8185
8186 makeBlink();
8187
8188 if (mError != null) {
8189 showError();
8190 }
8191 } else {
8192 if (mError != null) {
8193 hideError();
8194 }
8195 // Don't leave us in the middle of a batch edit.
8196 onEndBatchEdit();
Gilles Debunne05336272010-07-09 20:13:45 -07008197
Gilles Debunne64e54a62010-09-07 19:07:17 -07008198 if (this instanceof ExtractEditText) {
8199 // terminateTextSelectionMode removes selection, which we want to keep when
8200 // ExtractEditText goes out of focus.
8201 final int selStart = getSelectionStart();
8202 final int selEnd = getSelectionEnd();
Gilles Debunneb7012e842011-02-24 15:40:38 -08008203 hideControllers();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008204 Selection.setSelection((Spannable) mText, selStart, selEnd);
8205 } else {
Gilles Debunneb7012e842011-02-24 15:40:38 -08008206 hideControllers();
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008207 downgradeEasyCorrectionSpans();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008208 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008209
Gilles Debunnee587d832010-11-23 20:20:11 -08008210 // No need to create the controller
Gilles Debunne380b6042010-10-08 16:12:11 -07008211 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008212 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunne380b6042010-10-08 16:12:11 -07008213 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008214 }
8215
8216 startStopMarquee(focused);
8217
8218 if (mTransformation != null) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008219 mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008220 }
8221
8222 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8223 }
8224
Gilles Debunne380b6042010-10-08 16:12:11 -07008225 private int getLastTapPosition() {
Gilles Debunnee587d832010-11-23 20:20:11 -08008226 // No need to create the controller at that point, no last tap position saved
Gilles Debunne528c64882010-10-08 11:56:13 -07008227 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008228 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
Gilles Debunne380b6042010-10-08 16:12:11 -07008229 if (lastTapPosition >= 0) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008230 // Safety check, should not be possible.
Gilles Debunne380b6042010-10-08 16:12:11 -07008231 if (lastTapPosition > mText.length()) {
8232 Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
Gilles Debunne528c64882010-10-08 11:56:13 -07008233 + mText.length() + ")");
Gilles Debunne380b6042010-10-08 16:12:11 -07008234 lastTapPosition = mText.length();
Gilles Debunne528c64882010-10-08 11:56:13 -07008235 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008236 return lastTapPosition;
Gilles Debunne528c64882010-10-08 11:56:13 -07008237 }
8238 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008239
8240 return -1;
Gilles Debunne528c64882010-10-08 11:56:13 -07008241 }
8242
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008243 @Override
8244 public void onWindowFocusChanged(boolean hasWindowFocus) {
8245 super.onWindowFocusChanged(hasWindowFocus);
8246
8247 if (hasWindowFocus) {
8248 if (mBlink != null) {
8249 mBlink.uncancel();
Gilles Debunne3d010062011-02-18 14:16:41 -08008250 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008251 }
8252 } else {
8253 if (mBlink != null) {
8254 mBlink.cancel();
8255 }
8256 // Don't leave us in the middle of a batch edit.
8257 onEndBatchEdit();
8258 if (mInputContentType != null) {
8259 mInputContentType.enterDown = false;
8260 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008261
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008262 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008263 }
8264
8265 startStopMarquee(hasWindowFocus);
8266 }
8267
Adam Powellba0a2c32010-09-28 17:41:23 -07008268 @Override
8269 protected void onVisibilityChanged(View changedView, int visibility) {
8270 super.onVisibilityChanged(changedView, visibility);
8271 if (visibility != VISIBLE) {
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008272 hideControllers();
Adam Powellba0a2c32010-09-28 17:41:23 -07008273 }
8274 }
8275
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008276 /**
8277 * Use {@link BaseInputConnection#removeComposingSpans
8278 * BaseInputConnection.removeComposingSpans()} to remove any IME composing
8279 * state from this text view.
8280 */
8281 public void clearComposingText() {
8282 if (mText instanceof Spannable) {
8283 BaseInputConnection.removeComposingSpans((Spannable)mText);
8284 }
8285 }
8286
8287 @Override
8288 public void setSelected(boolean selected) {
8289 boolean wasSelected = isSelected();
8290
8291 super.setSelected(selected);
8292
8293 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8294 if (selected) {
8295 startMarquee();
8296 } else {
8297 stopMarquee();
8298 }
8299 }
8300 }
8301
8302 @Override
8303 public boolean onTouchEvent(MotionEvent event) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008304 final int action = event.getActionMasked();
Adam Powell965b9692010-10-21 18:44:32 -07008305
Adam Powell965b9692010-10-21 18:44:32 -07008306 if (hasSelectionController()) {
8307 getSelectionController().onTouchEvent(event);
Gilles Debunne5347c582010-10-27 14:22:35 -07008308 }
Adam Powellb08013c2010-09-16 16:28:11 -07008309
Gilles Debunne5347c582010-10-27 14:22:35 -07008310 if (action == MotionEvent.ACTION_DOWN) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07008311 mLastDownPositionX = event.getX();
8312 mLastDownPositionY = event.getY();
Gilles Debunne9948ad72010-11-24 14:00:46 -08008313
The Android Open Source Project4df24232009-03-05 14:34:35 -08008314 // Reset this state; it will be re-set if super.onTouchEvent
8315 // causes focus to move to the view.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008316 mTouchFocusSelected = false;
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008317 mIgnoreActionUpEvent = false;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008318 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008319
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008320 final boolean superResult = super.onTouchEvent(event);
8321
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008322 /*
8323 * Don't handle the release after a long press, because it will
8324 * move the selection away from whatever the menu action was
8325 * trying to affect.
8326 */
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008327 if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
8328 mDiscardNextActionUp = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008329 return superResult;
8330 }
8331
Gilles Debunne70a63122011-09-01 13:27:33 -07008332 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
8333 !shouldIgnoreActionUpEvent() && isFocused();
Gilles Debunnec3e85a72011-01-21 08:46:06 -08008334
Gilles Debunne70a63122011-09-01 13:27:33 -07008335 if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
Janos Levai042856c2010-10-15 02:53:58 +03008336 && mText instanceof Spannable && mLayout != null) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008337 boolean handled = false;
8338
The Android Open Source Projectba87e3e2009-03-13 13:04:22 -07008339 if (mMovement != null) {
8340 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
8341 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008342
Gilles Debunne70a63122011-09-01 13:27:33 -07008343 if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable) {
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008344 // The LinkMovementMethod which should handle taps on links has not been installed
Gilles Debunne70a63122011-09-01 13:27:33 -07008345 // on non editable text that support text selection.
8346 // We reproduce its behavior here to open links for these.
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008347 ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
8348 getSelectionEnd(), ClickableSpan.class);
8349
8350 if (links.length != 0) {
8351 links[0].onClick(this);
8352 handled = true;
8353 }
8354 }
8355
Gilles Debunne70a63122011-09-01 13:27:33 -07008356 if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008357 // Show the IME, except when selecting in read-only text.
satok863fcd62011-06-21 17:38:02 +09008358 final InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09008359 viewClicked(imm);
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008360 if (!mTextIsSelectable) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008361 handled |= imm != null && imm.showSoftInput(this, 0);
Adam Powell879fb6b2010-09-20 11:23:56 -07008362 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008363
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008364 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
Gilles Debunne70a63122011-09-01 13:27:33 -07008365 hideControllers();
8366 if (!selectAllGotFocus && mText.length() > 0) {
8367 if (isCursorInsideEasyCorrectionSpan()) {
8368 showSuggestions();
8369 } else if (hasInsertionController()) {
8370 getInsertionController().show();
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08008371 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008372 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008373
8374 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008375 }
8376
The Android Open Source Project4df24232009-03-05 14:34:35 -08008377 if (handled) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008378 return true;
8379 }
8380 }
8381
8382 return superResult;
8383 }
8384
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008385 /**
Gilles Debunne6435a562011-08-04 21:22:30 -07008386 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
8387 */
8388 private boolean isCursorInsideSuggestionSpan() {
8389 if (!(mText instanceof Spannable)) return false;
8390
8391 SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
8392 getSelectionEnd(), SuggestionSpan.class);
8393 return (suggestionSpans.length > 0);
8394 }
8395
8396 /**
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008397 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
8398 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
8399 */
8400 private boolean isCursorInsideEasyCorrectionSpan() {
Gilles Debunne6435a562011-08-04 21:22:30 -07008401 Spannable spannable = (Spannable) mText;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008402 SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
8403 getSelectionEnd(), SuggestionSpan.class);
8404 for (int i = 0; i < suggestionSpans.length; i++) {
8405 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
8406 return true;
8407 }
8408 }
8409 return false;
8410 }
8411
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008412 /**
8413 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
8414 * span.
8415 */
8416 private void downgradeEasyCorrectionSpans() {
8417 if (mText instanceof Spannable) {
8418 Spannable spannable = (Spannable) mText;
8419 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8420 spannable.length(), SuggestionSpan.class);
8421 for (int i = 0; i < suggestionSpans.length; i++) {
8422 int flags = suggestionSpans[i].getFlags();
8423 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8424 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
8425 flags = flags & ~SuggestionSpan.FLAG_EASY_CORRECT;
8426 suggestionSpans[i].setFlags(flags);
8427 }
8428 }
8429 }
8430 }
8431
Jeff Brown8f345672011-02-26 13:29:53 -08008432 @Override
8433 public boolean onGenericMotionEvent(MotionEvent event) {
8434 if (mMovement != null && mText instanceof Spannable && mLayout != null) {
8435 try {
8436 if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
8437 return true;
8438 }
8439 } catch (AbstractMethodError ex) {
8440 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
8441 // Ignore its absence in case third party applications implemented the
8442 // interface directly.
8443 }
8444 }
8445 return super.onGenericMotionEvent(event);
8446 }
8447
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008448 private void prepareCursorControllers() {
Adam Powell8c8293b2010-10-12 14:45:12 -07008449 boolean windowSupportsHandles = false;
8450
8451 ViewGroup.LayoutParams params = getRootView().getLayoutParams();
8452 if (params instanceof WindowManager.LayoutParams) {
8453 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
8454 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
8455 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
8456 }
8457
Gilles Debunne98dbfd42011-01-24 12:54:10 -08008458 mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
Adam Powell965b9692010-10-21 18:44:32 -07008459 mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
8460 mLayout != null;
8461
8462 if (!mInsertionControllerEnabled) {
Gilles Debunnef48e83b2010-12-06 18:36:08 -08008463 hideInsertionPointCursorController();
Adam Powell65a1de92011-01-30 15:47:29 -08008464 if (mInsertionPointCursorController != null) {
8465 mInsertionPointCursorController.onDetached();
8466 mInsertionPointCursorController = null;
8467 }
Gilles Debunne05336272010-07-09 20:13:45 -07008468 }
8469
Adam Powell965b9692010-10-21 18:44:32 -07008470 if (!mSelectionControllerEnabled) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008471 stopSelectionActionMode();
Adam Powell65a1de92011-01-30 15:47:29 -08008472 if (mSelectionModifierCursorController != null) {
8473 mSelectionModifierCursorController.onDetached();
8474 mSelectionModifierCursorController = null;
8475 }
Gilles Debunne05336272010-07-09 20:13:45 -07008476 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008477 }
8478
8479 /**
Gilles Debunne86b9c782010-11-11 10:43:48 -08008480 * @return True iff this TextView contains a text that can be edited, or if this is
8481 * a selectable TextView.
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008482 */
8483 private boolean isTextEditable() {
Gilles Debunnef076eeb2010-11-29 11:32:53 -08008484 return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008485 }
8486
The Android Open Source Project4df24232009-03-05 14:34:35 -08008487 /**
8488 * Returns true, only while processing a touch gesture, if the initial
8489 * 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 -07008490 * its selection changed. Only valid while processing the touch gesture
8491 * of interest.
The Android Open Source Project4df24232009-03-05 14:34:35 -08008492 */
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008493 public boolean didTouchFocusSelect() {
8494 return mTouchFocusSelected;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008495 }
8496
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008497 @Override
8498 public void cancelLongPress() {
8499 super.cancelLongPress();
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008500 mIgnoreActionUpEvent = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008501 }
Gilles Debunne70a63122011-09-01 13:27:33 -07008502
8503 /**
8504 * This method is only valid during a touch event.
8505 *
8506 * @return true when the ACTION_UP event should be ignored, false otherwise.
8507 *
8508 * @hide
8509 */
8510 public boolean shouldIgnoreActionUpEvent() {
8511 return mIgnoreActionUpEvent;
8512 }
8513
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008514 @Override
8515 public boolean onTrackballEvent(MotionEvent event) {
8516 if (mMovement != null && mText instanceof Spannable &&
8517 mLayout != null) {
8518 if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
8519 return true;
8520 }
8521 }
8522
8523 return super.onTrackballEvent(event);
8524 }
8525
8526 public void setScroller(Scroller s) {
8527 mScroller = s;
8528 }
8529
8530 private static class Blink extends Handler implements Runnable {
Gilles Debunnee15b3582010-06-16 15:17:21 -07008531 private final WeakReference<TextView> mView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008532 private boolean mCancelled;
8533
8534 public Blink(TextView v) {
8535 mView = new WeakReference<TextView>(v);
8536 }
8537
8538 public void run() {
8539 if (mCancelled) {
8540 return;
8541 }
8542
8543 removeCallbacks(Blink.this);
8544
8545 TextView tv = mView.get();
8546
Gilles Debunne3d010062011-02-18 14:16:41 -08008547 if (tv != null && tv.shouldBlink()) {
8548 if (tv.mLayout != null) {
8549 tv.invalidateCursorPath();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008550 }
Gilles Debunne3d010062011-02-18 14:16:41 -08008551
8552 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008553 }
8554 }
8555
8556 void cancel() {
8557 if (!mCancelled) {
8558 removeCallbacks(Blink.this);
8559 mCancelled = true;
8560 }
8561 }
8562
8563 void uncancel() {
8564 mCancelled = false;
8565 }
8566 }
8567
Gilles Debunne3d010062011-02-18 14:16:41 -08008568 /**
8569 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
8570 */
8571 private boolean shouldBlink() {
8572 if (!isFocused()) return false;
8573
8574 final int start = getSelectionStart();
8575 if (start < 0) return false;
8576
8577 final int end = getSelectionEnd();
8578 if (end < 0) return false;
8579
8580 return start == end;
8581 }
8582
8583 private void makeBlink() {
8584 if (isCursorVisible()) {
8585 if (shouldBlink()) {
8586 mShowCursor = SystemClock.uptimeMillis();
8587 if (mBlink == null) mBlink = new Blink(this);
8588 mBlink.removeCallbacks(mBlink);
8589 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
8590 }
8591 } else {
8592 if (mBlink != null) mBlink.removeCallbacks(mBlink);
8593 }
8594 }
8595
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008596 @Override
8597 protected float getLeftFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008598 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008599 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8600 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008601 if (mMarquee != null && !mMarquee.isStopped()) {
8602 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008603 if (marquee.shouldDrawLeftFade()) {
8604 return marquee.mScroll / getHorizontalFadingEdgeLength();
8605 } else {
8606 return 0.0f;
8607 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008608 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008609 final int layoutDirection = getResolvedLayoutDirection();
8610 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008611 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008612 case Gravity.LEFT:
8613 return 0.0f;
8614 case Gravity.RIGHT:
8615 return (mLayout.getLineRight(0) - (mRight - mLeft) -
8616 getCompoundPaddingLeft() - getCompoundPaddingRight() -
8617 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
8618 case Gravity.CENTER_HORIZONTAL:
8619 return 0.0f;
8620 }
8621 }
8622 }
8623 return super.getLeftFadingEdgeStrength();
8624 }
8625
8626 @Override
8627 protected float getRightFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008628 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008629 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8630 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008631 if (mMarquee != null && !mMarquee.isStopped()) {
8632 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008633 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008634 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008635 final int layoutDirection = getResolvedLayoutDirection();
8636 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008637 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008638 case Gravity.LEFT:
Romain Guy076dc9f2009-06-24 17:17:51 -07008639 final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
8640 getCompoundPaddingRight();
8641 final float lineWidth = mLayout.getLineWidth(0);
8642 return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008643 case Gravity.RIGHT:
8644 return 0.0f;
8645 case Gravity.CENTER_HORIZONTAL:
Gilles Debunne44c14732010-10-19 11:56:59 -07008646 case Gravity.FILL_HORIZONTAL:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008647 return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
8648 getCompoundPaddingLeft() - getCompoundPaddingRight())) /
8649 getHorizontalFadingEdgeLength();
8650 }
8651 }
8652 }
8653 return super.getRightFadingEdgeStrength();
8654 }
8655
8656 @Override
8657 protected int computeHorizontalScrollRange() {
Romain Guydac5f9f2010-07-08 11:40:54 -07008658 if (mLayout != null) {
8659 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
8660 (int) mLayout.getLineWidth(0) : mLayout.getWidth();
8661 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008662
8663 return super.computeHorizontalScrollRange();
8664 }
8665
8666 @Override
8667 protected int computeVerticalScrollRange() {
8668 if (mLayout != null)
8669 return mLayout.getHeight();
8670
8671 return super.computeVerticalScrollRange();
8672 }
8673
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008674 @Override
8675 protected int computeVerticalScrollExtent() {
8676 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
8677 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008678
8679 @Override
Svetoslav Ganovea515ae2011-09-14 18:15:32 -07008680 public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
8681 super.findViewsWithText(outViews, searched, flags);
8682 if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
8683 && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
8684 String searchedLowerCase = searched.toString().toLowerCase();
8685 String textLowerCase = mText.toString().toLowerCase();
8686 if (textLowerCase.contains(searchedLowerCase)) {
8687 outViews.add(this);
8688 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008689 }
8690 }
8691
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008692 public enum BufferType {
8693 NORMAL, SPANNABLE, EDITABLE,
8694 }
8695
8696 /**
8697 * Returns the TextView_textColor attribute from the
8698 * Resources.StyledAttributes, if set, or the TextAppearance_textColor
8699 * from the TextView_textAppearance attribute, if TextView_textColor
8700 * was not set directly.
8701 */
8702 public static ColorStateList getTextColors(Context context, TypedArray attrs) {
8703 ColorStateList colors;
8704 colors = attrs.getColorStateList(com.android.internal.R.styleable.
8705 TextView_textColor);
8706
8707 if (colors == null) {
8708 int ap = attrs.getResourceId(com.android.internal.R.styleable.
8709 TextView_textAppearance, -1);
8710 if (ap != -1) {
8711 TypedArray appearance;
8712 appearance = context.obtainStyledAttributes(ap,
8713 com.android.internal.R.styleable.TextAppearance);
8714 colors = appearance.getColorStateList(com.android.internal.R.styleable.
8715 TextAppearance_textColor);
8716 appearance.recycle();
8717 }
8718 }
8719
8720 return colors;
8721 }
8722
8723 /**
8724 * Returns the default color from the TextView_textColor attribute
8725 * from the AttributeSet, if set, or the default color from the
8726 * TextAppearance_textColor from the TextView_textAppearance attribute,
8727 * if TextView_textColor was not set directly.
8728 */
8729 public static int getTextColor(Context context,
8730 TypedArray attrs,
8731 int def) {
8732 ColorStateList colors = getTextColors(context, attrs);
8733
8734 if (colors == null) {
8735 return def;
8736 } else {
8737 return colors.getDefaultColor();
8738 }
8739 }
8740
8741 @Override
8742 public boolean onKeyShortcut(int keyCode, KeyEvent event) {
Jeff Brownc1df9072010-12-21 16:38:50 -08008743 final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
8744 if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
8745 switch (keyCode) {
8746 case KeyEvent.KEYCODE_A:
8747 if (canSelectText()) {
8748 return onTextContextMenuItem(ID_SELECT_ALL);
8749 }
8750 break;
8751 case KeyEvent.KEYCODE_X:
8752 if (canCut()) {
8753 return onTextContextMenuItem(ID_CUT);
8754 }
8755 break;
8756 case KeyEvent.KEYCODE_C:
8757 if (canCopy()) {
8758 return onTextContextMenuItem(ID_COPY);
8759 }
8760 break;
8761 case KeyEvent.KEYCODE_V:
8762 if (canPaste()) {
8763 return onTextContextMenuItem(ID_PASTE);
8764 }
8765 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008766 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008767 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008768 return super.onKeyShortcut(keyCode, event);
8769 }
8770
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008771 /**
8772 * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
8773 * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
8774 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
8775 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008776 private boolean canSelectText() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008777 return hasSelectionController() && mText.length() != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008778 }
8779
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008780 /**
8781 * Test based on the <i>intrinsic</i> charateristics of the TextView.
8782 * The text must be spannable and the movement method must allow for arbitary selection.
8783 *
8784 * See also {@link #canSelectText()}.
8785 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008786 private boolean textCanBeSelected() {
Gilles Debunne05336272010-07-09 20:13:45 -07008787 // prepareCursorController() relies on this method.
8788 // If you change this condition, make sure prepareCursorController is called anywhere
8789 // the value of this condition might be changed.
Gilles Debunnebb588da2011-07-11 18:26:19 -07008790 if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
8791 return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008792 }
8793
8794 private boolean canCut() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008795 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008796 return false;
8797 }
8798
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008799 if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
8800 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008801 }
8802
8803 return false;
8804 }
8805
8806 private boolean canCopy() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008807 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008808 return false;
8809 }
8810
Gilles Debunne03789e82010-09-07 19:07:17 -07008811 if (mText.length() > 0 && hasSelection()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008812 return true;
8813 }
8814
8815 return false;
8816 }
8817
8818 private boolean canPaste() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008819 return (mText instanceof Editable &&
8820 mInput != null &&
8821 getSelectionStart() >= 0 &&
8822 getSelectionEnd() >= 0 &&
8823 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008824 hasPrimaryClip());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008825 }
8826
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008827 private static long packRangeInLong(int start, int end) {
Gilles Debunne05336272010-07-09 20:13:45 -07008828 return (((long) start) << 32) | end;
8829 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008830
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008831 private static int extractRangeStartFromLong(long range) {
8832 return (int) (range >>> 32);
8833 }
8834
8835 private static int extractRangeEndFromLong(long range) {
8836 return (int) (range & 0x00000000FFFFFFFFL);
8837 }
Gilles Debunnecbfbb522010-10-07 16:57:31 -07008838
Gilles Debunnec59269f2011-04-22 11:46:09 -07008839 private boolean selectAll() {
8840 final int length = mText.length();
8841 Selection.setSelection((Spannable) mText, 0, length);
8842 return length > 0;
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008843 }
8844
Gilles Debunnec59269f2011-04-22 11:46:09 -07008845 /**
8846 * Adjusts selection to the word under last touch offset.
8847 * Return true if the operation was successfully performed.
8848 */
8849 private boolean selectCurrentWord() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008850 if (!canSelectText()) {
Gilles Debunnec59269f2011-04-22 11:46:09 -07008851 return false;
Gilles Debunne6da7e932010-12-07 14:28:14 -08008852 }
8853
Gilles Debunne710a9102010-11-23 16:50:28 -08008854 if (hasPasswordTransformationMethod()) {
Gilles Debunne87380bc2011-01-04 13:24:54 -08008855 // Always select all on a password field.
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008856 // Cut/copy menu entries are not available for passwords, but being able to select all
8857 // is however useful to delete or paste to replace the entire content.
Gilles Debunnec59269f2011-04-22 11:46:09 -07008858 return selectAll();
8859 }
8860
8861 int klass = mInputType & InputType.TYPE_MASK_CLASS;
8862 int variation = mInputType & InputType.TYPE_MASK_VARIATION;
8863
8864 // Specific text field types: select the entire text for these
8865 if (klass == InputType.TYPE_CLASS_NUMBER ||
8866 klass == InputType.TYPE_CLASS_PHONE ||
8867 klass == InputType.TYPE_CLASS_DATETIME ||
8868 variation == InputType.TYPE_TEXT_VARIATION_URI ||
8869 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
8870 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
8871 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
8872 return selectAll();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008873 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008874
Gilles Debunne87380bc2011-01-04 13:24:54 -08008875 long lastTouchOffsets = getLastTouchOffsets();
8876 final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
8877 final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008878
Gilles Debunnebb588da2011-07-11 18:26:19 -07008879 // Safety check in case standard touch event handling has been bypassed
8880 if (minOffset < 0 || minOffset >= mText.length()) return false;
8881 if (maxOffset < 0 || maxOffset >= mText.length()) return false;
8882
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008883 int selectionStart, selectionEnd;
8884
8885 // If a URLSpan (web address, email, phone...) is found at that position, select it.
8886 URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
8887 if (urlSpans.length == 1) {
8888 URLSpan url = urlSpans[0];
8889 selectionStart = ((Spanned) mText).getSpanStart(url);
8890 selectionEnd = ((Spanned) mText).getSpanEnd(url);
8891 } else {
Gilles Debunne6435a562011-08-04 21:22:30 -07008892 WordIterator wordIterator = getWordIterator();
8893 // WordIterator handles text changes, this is a no-op if text in unchanged.
8894 wordIterator.setCharSequence(mText);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008895
Gilles Debunne6435a562011-08-04 21:22:30 -07008896 selectionStart = wordIterator.getBeginning(minOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008897 if (selectionStart == BreakIterator.DONE) return false;
8898
Gilles Debunne6435a562011-08-04 21:22:30 -07008899 selectionEnd = wordIterator.getEnd(maxOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008900 if (selectionEnd == BreakIterator.DONE) return false;
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008901 }
8902
8903 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008904 return true;
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008905 }
8906
Gilles Debunne6435a562011-08-04 21:22:30 -07008907 WordIterator getWordIterator() {
8908 if (mWordIterator == null) {
8909 mWordIterator = new WordIterator();
8910 }
8911 return mWordIterator;
8912 }
8913
8914 private SpellChecker getSpellChecker() {
8915 if (mSpellChecker == null) {
8916 mSpellChecker = new SpellChecker(this);
8917 }
8918 return mSpellChecker;
8919 }
8920
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008921 private long getLastTouchOffsets() {
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008922 int minOffset, maxOffset;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008923
Gilles Debunne528c64882010-10-08 11:56:13 -07008924 if (mContextMenuTriggeredByKey) {
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008925 minOffset = getSelectionStart();
8926 maxOffset = getSelectionEnd();
8927 } else {
Gilles Debunnee587d832010-11-23 20:20:11 -08008928 SelectionModifierCursorController selectionController = getSelectionController();
8929 minOffset = selectionController.getMinTouchOffset();
8930 maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne03789e82010-09-07 19:07:17 -07008931 }
8932
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008933 return packRangeInLong(minOffset, maxOffset);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008934 }
Gilles Debunnedf4ee432010-08-25 19:13:48 -07008935
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008936 @Override
Svetoslav Ganov736c2752011-04-22 18:30:36 -07008937 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganov887e1a12011-04-29 15:09:28 -07008938 super.onPopulateAccessibilityEvent(event);
8939
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008940 final boolean isPassword = hasPasswordTransformationMethod();
svetoslavganov75986cf2009-05-14 22:28:01 -07008941 if (!isPassword) {
8942 CharSequence text = getText();
8943 if (TextUtils.isEmpty(text)) {
8944 text = getHint();
8945 }
Svetoslav Ganov76502592011-07-29 10:44:59 -07008946 if (TextUtils.isEmpty(text)) {
8947 text = getContentDescription();
8948 }
svetoslavganov75986cf2009-05-14 22:28:01 -07008949 if (!TextUtils.isEmpty(text)) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008950 event.getText().add(text);
8951 }
svetoslavganov75986cf2009-05-14 22:28:01 -07008952 }
svetoslavganov75986cf2009-05-14 22:28:01 -07008953 }
8954
Svetoslav Ganov30401322011-05-12 18:53:45 -07008955 @Override
8956 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
8957 super.onInitializeAccessibilityEvent(event);
8958
8959 final boolean isPassword = hasPasswordTransformationMethod();
8960 event.setPassword(isPassword);
Svetoslav Ganova0156172011-06-26 17:55:44 -07008961
8962 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
8963 event.setFromIndex(Selection.getSelectionStart(mText));
8964 event.setToIndex(Selection.getSelectionEnd(mText));
8965 event.setItemCount(mText.length());
8966 }
Svetoslav Ganov30401322011-05-12 18:53:45 -07008967 }
8968
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008969 @Override
8970 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
8971 super.onInitializeAccessibilityNodeInfo(info);
8972
8973 final boolean isPassword = hasPasswordTransformationMethod();
8974 if (!isPassword) {
8975 info.setText(getText());
8976 }
8977 info.setPassword(isPassword);
8978 }
8979
Svetoslav Ganov4e03f592011-07-29 22:17:14 -07008980 @Override
8981 public void sendAccessibilityEvent(int eventType) {
8982 // Do not send scroll events since first they are not interesting for
8983 // accessibility and second such events a generated too frequently.
8984 // For details see the implementation of bringTextIntoView().
8985 if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
8986 return;
8987 }
8988 super.sendAccessibilityEvent(eventType);
8989 }
8990
svetoslavganov75986cf2009-05-14 22:28:01 -07008991 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
8992 int fromIndex, int removedCount, int addedCount) {
8993 AccessibilityEvent event =
8994 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
8995 event.setFromIndex(fromIndex);
8996 event.setRemovedCount(removedCount);
8997 event.setAddedCount(addedCount);
8998 event.setBeforeText(beforeText);
8999 sendAccessibilityEventUnchecked(event);
9000 }
9001
9002 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009003 protected void onCreateContextMenu(ContextMenu menu) {
9004 super.onCreateContextMenu(menu);
9005 boolean added = false;
Gilles Debunne528c64882010-10-08 11:56:13 -07009006 mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
9007 // Problem with context menu on long press: the menu appears while the key in down and when
Gilles Debunnecbcb3452010-12-17 15:31:02 -08009008 // the key is released, the view does not receive the key_up event.
9009 // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
9010 // events. We cannot simply clear these flags in onTextContextMenuItem since
Gilles Debunne528c64882010-10-08 11:56:13 -07009011 // it may not be called (if the user/ discards the context menu with the back key).
Gilles Debunnecbcb3452010-12-17 15:31:02 -08009012 // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
9013 // available in onTextContextMenuItem.
Gilles Debunne528c64882010-10-08 11:56:13 -07009014 mDPadCenterIsDown = mEnterKeyIsDown = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009015
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009016 MenuHandler handler = new MenuHandler();
9017
Gilles Debunneb0db5942011-01-04 13:58:54 -08009018 if (mText instanceof Spanned && hasSelectionController()) {
Gilles Debunne4dfe0862010-12-17 15:46:28 -08009019 long lastTouchOffset = getLastTouchOffsets();
9020 final int selStart = extractRangeStartFromLong(lastTouchOffset);
9021 final int selEnd = extractRangeEndFromLong(lastTouchOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009022
Gilles Debunne4dfe0862010-12-17 15:46:28 -08009023 URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
Gilles Debunnecbcb3452010-12-17 15:31:02 -08009024 if (urls.length > 0) {
9025 menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
9026 setOnMenuItemClickListener(handler);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009027
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009028 added = true;
9029 }
9030 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009031
9032 // The context menu is not empty, which will prevent the selection mode from starting.
9033 // Add a entry to start it in the context menu.
9034 // TODO Does not handle the case where a subclass does not call super.thisMethod or
9035 // populates the menu AFTER this call.
9036 if (menu.size() > 0) {
9037 menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
Gilles Debunnecbcb3452010-12-17 15:31:02 -08009038 setOnMenuItemClickListener(handler);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009039 added = true;
9040 }
9041
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009042 if (added) {
9043 menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
9044 }
9045 }
9046
9047 /**
9048 * Returns whether this text view is a current input method target. The
9049 * default implementation just checks with {@link InputMethodManager}.
9050 */
9051 public boolean isInputMethodTarget() {
9052 InputMethodManager imm = InputMethodManager.peekInstance();
9053 return imm != null && imm.isActive(this);
9054 }
9055
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009056 // Selection context mode
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009057 private static final int ID_SELECT_ALL = android.R.id.selectAll;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009058 private static final int ID_CUT = android.R.id.cut;
9059 private static final int ID_COPY = android.R.id.copy;
9060 private static final int ID_PASTE = android.R.id.paste;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009061 // Context menu entries
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009062 private static final int ID_COPY_URL = android.R.id.copyUrl;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009063 private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009064
9065 private class MenuHandler implements MenuItem.OnMenuItemClickListener {
9066 public boolean onMenuItemClick(MenuItem item) {
9067 return onTextContextMenuItem(item.getItemId());
9068 }
9069 }
9070
9071 /**
9072 * Called when a context menu option for the text view is selected. Currently
Jeff Brownc1df9072010-12-21 16:38:50 -08009073 * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
9074 * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
9075 * or {@link android.R.id#copy}.
Gilles Debunnec59269f2011-04-22 11:46:09 -07009076 *
9077 * @return true if the context menu item action was performed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009078 */
9079 public boolean onTextContextMenuItem(int id) {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009080 int min = 0;
9081 int max = mText.length();
Gilles Debunne64e54a62010-09-07 19:07:17 -07009082
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009083 if (isFocused()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07009084 final int selStart = getSelectionStart();
9085 final int selEnd = getSelectionEnd();
9086
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009087 min = Math.max(0, Math.min(selStart, selEnd));
9088 max = Math.max(0, Math.max(selStart, selEnd));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009089 }
9090
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009091 switch (id) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009092 case ID_COPY_URL:
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009093 URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07009094 if (urls.length >= 1) {
Dianne Hackborn1040dc42010-08-26 22:11:06 -07009095 ClipData clip = null;
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07009096 for (int i=0; i<urls.length; i++) {
9097 Uri uri = Uri.parse(urls[0].getURL());
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07009098 if (clip == null) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009099 clip = ClipData.newRawUri(null, uri);
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07009100 } else {
Dianne Hackborn1040dc42010-08-26 22:11:06 -07009101 clip.addItem(new ClipData.Item(uri));
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07009102 }
9103 }
Dianne Hackborn1040dc42010-08-26 22:11:06 -07009104 if (clip != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009105 setPrimaryClip(clip);
Dianne Hackborn1040dc42010-08-26 22:11:06 -07009106 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009107 }
Gilles Debunne4dfe0862010-12-17 15:46:28 -08009108 stopSelectionActionMode();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009109 return true;
9110
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009111 case ID_SELECTION_MODE:
Gilles Debunne4dfe0862010-12-17 15:46:28 -08009112 if (mSelectionActionMode != null) {
9113 // Selection mode is already started, simply change selected part.
Gilles Debunne2037b822011-04-22 13:07:33 -07009114 selectCurrentWord();
Gilles Debunne4dfe0862010-12-17 15:46:28 -08009115 } else {
9116 startSelectionActionMode();
9117 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009118 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009119
Jeff Brownc1df9072010-12-21 16:38:50 -08009120 case ID_SELECT_ALL:
Gilles Debunne299733e2011-02-07 17:11:41 -08009121 // This does not enter text selection mode. Text is highlighted, so that it can be
Gilles Debunnec59269f2011-04-22 11:46:09 -07009122 // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
Jeff Brownc1df9072010-12-21 16:38:50 -08009123 selectAll();
Jeff Brownc1df9072010-12-21 16:38:50 -08009124 return true;
9125
9126 case ID_PASTE:
9127 paste(min, max);
9128 return true;
9129
9130 case ID_CUT:
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009131 setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08009132 ((Editable) mText).delete(min, max);
9133 stopSelectionActionMode();
9134 return true;
9135
9136 case ID_COPY:
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009137 setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08009138 stopSelectionActionMode();
9139 return true;
9140 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009141 return false;
9142 }
9143
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009144 /**
9145 * Prepare text so that there are not zero or two spaces at beginning and end of region defined
9146 * by [min, max] when replacing this region by paste.
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009147 * 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 -08009148 * make sure we do not add an extra one from the paste content.
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009149 */
9150 private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009151 if (paste.length() > 0) {
9152 if (min > 0) {
9153 final char charBefore = mTransformed.charAt(min - 1);
9154 final char charAfter = paste.charAt(0);
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009155
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009156 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9157 // Two spaces at beginning of paste: remove one
9158 final int originalLength = mText.length();
9159 ((Editable) mText).delete(min - 1, min);
9160 // Due to filters, there is no guarantee that exactly one character was
9161 // removed: count instead.
9162 final int delta = mText.length() - originalLength;
9163 min += delta;
9164 max += delta;
9165 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9166 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9167 // No space at beginning of paste: add one
9168 final int originalLength = mText.length();
9169 ((Editable) mText).replace(min, min, " ");
9170 // Taking possible filters into account as above.
9171 final int delta = mText.length() - originalLength;
9172 min += delta;
9173 max += delta;
9174 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009175 }
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009176
9177 if (max < mText.length()) {
9178 final char charBefore = paste.charAt(paste.length() - 1);
9179 final char charAfter = mTransformed.charAt(max);
9180
9181 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9182 // Two spaces at end of paste: remove one
9183 ((Editable) mText).delete(max, max + 1);
9184 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9185 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9186 // No space at end of paste: add one
9187 ((Editable) mText).replace(max, max, " ");
9188 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009189 }
9190 }
Gilles Debunne4ae0f292010-11-29 14:56:39 -08009191
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009192 return packRangeInLong(min, max);
9193 }
9194
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009195 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
9196 TextView shadowView = (TextView) inflate(mContext,
Gilles Debunnef170a342010-11-11 11:08:59 -08009197 com.android.internal.R.layout.text_drag_thumbnail, null);
9198
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009199 if (shadowView == null) {
Gilles Debunnef170a342010-11-11 11:08:59 -08009200 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
9201 }
9202
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009203 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
9204 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
Gilles Debunnef170a342010-11-11 11:08:59 -08009205 }
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009206 shadowView.setText(text);
9207 shadowView.setTextColor(getTextColors());
Gilles Debunnef170a342010-11-11 11:08:59 -08009208
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009209 shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
9210 shadowView.setGravity(Gravity.CENTER);
Gilles Debunnef170a342010-11-11 11:08:59 -08009211
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009212 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
Gilles Debunnef170a342010-11-11 11:08:59 -08009213 ViewGroup.LayoutParams.WRAP_CONTENT));
9214
9215 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009216 shadowView.measure(size, size);
Gilles Debunnef170a342010-11-11 11:08:59 -08009217
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009218 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
9219 shadowView.invalidate();
9220 return new DragShadowBuilder(shadowView);
Gilles Debunnef170a342010-11-11 11:08:59 -08009221 }
9222
Gilles Debunneaaa84792010-12-03 11:10:14 -08009223 private static class DragLocalState {
9224 public TextView sourceTextView;
9225 public int start, end;
9226
9227 public DragLocalState(TextView sourceTextView, int start, int end) {
9228 this.sourceTextView = sourceTextView;
9229 this.start = start;
9230 this.end = end;
9231 }
9232 }
9233
Gilles Debunnee15b3582010-06-16 15:17:21 -07009234 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009235 public boolean performLongClick() {
Gilles Debunnee28454a2011-09-07 18:03:44 -07009236 boolean handled = false;
9237 boolean vibrate = true;
9238
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009239 if (super.performLongClick()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009240 mDiscardNextActionUp = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009241 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009242 }
Gilles Debunnef170a342010-11-11 11:08:59 -08009243
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009244 // Long press in empty space moves cursor and shows the Paste affordance if available.
Gilles Debunnee28454a2011-09-07 18:03:44 -07009245 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009246 mInsertionControllerEnabled) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07009247 final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
Gilles Debunned1dc72a2010-11-30 10:16:35 -08009248 stopSelectionActionMode();
Gilles Debunne75beb332011-04-29 11:40:22 -07009249 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne6435a562011-08-04 21:22:30 -07009250 getInsertionController().showWithActionPopup();
Gilles Debunne299733e2011-02-07 17:11:41 -08009251 handled = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009252 vibrate = false;
Gilles Debunne9948ad72010-11-24 14:00:46 -08009253 }
9254
Gilles Debunne299733e2011-02-07 17:11:41 -08009255 if (!handled && mSelectionActionMode != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009256 if (touchPositionIsInSelection()) {
9257 // Start a drag
9258 final int start = getSelectionStart();
9259 final int end = getSelectionEnd();
9260 CharSequence selectedText = mTransformed.subSequence(start, end);
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009261 ClipData data = ClipData.newPlainText(null, selectedText);
Gilles Debunneaaa84792010-12-03 11:10:14 -08009262 DragLocalState localState = new DragLocalState(this, start, end);
Christopher Tate02d2b3b2011-01-10 20:43:53 -08009263 startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009264 stopSelectionActionMode();
9265 } else {
Gilles Debunnef682a772011-08-31 15:49:10 -07009266 getSelectionController().hide();
Gilles Debunne2037b822011-04-22 13:07:33 -07009267 selectCurrentWord();
Gilles Debunne57324c72011-08-29 14:42:15 -07009268 getSelectionController().show();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009269 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009270 handled = true;
Gilles Debunnef170a342010-11-11 11:08:59 -08009271 }
9272
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009273 // Start a new selection
Gilles Debunnee28454a2011-09-07 18:03:44 -07009274 if (!handled) {
9275 handled = startSelectionActionMode();
9276 }
9277
9278 if (vibrate) {
9279 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
9280 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009281
9282 if (handled) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009283 mDiscardNextActionUp = true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009284 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009285
Gilles Debunne299733e2011-02-07 17:11:41 -08009286 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009287 }
9288
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009289 private boolean touchPositionIsInSelection() {
9290 int selectionStart = getSelectionStart();
9291 int selectionEnd = getSelectionEnd();
Gilles Debunne05336272010-07-09 20:13:45 -07009292
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009293 if (selectionStart == selectionEnd) {
9294 return false;
9295 }
Gilles Debunne05336272010-07-09 20:13:45 -07009296
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009297 if (selectionStart > selectionEnd) {
9298 int tmp = selectionStart;
9299 selectionStart = selectionEnd;
9300 selectionEnd = tmp;
Gilles Debunne05336272010-07-09 20:13:45 -07009301 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009302 }
Gilles Debunne05336272010-07-09 20:13:45 -07009303
Gilles Debunnee587d832010-11-23 20:20:11 -08009304 SelectionModifierCursorController selectionController = getSelectionController();
9305 int minOffset = selectionController.getMinTouchOffset();
9306 int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne05336272010-07-09 20:13:45 -07009307
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009308 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
9309 }
9310
Gilles Debunne21078e42011-08-02 10:22:35 -07009311 private PositionListener getPositionListener() {
9312 if (mPositionListener == null) {
9313 mPositionListener = new PositionListener();
9314 }
9315 return mPositionListener;
9316 }
9317
9318 private interface TextViewPositionListener {
Gilles Debunnef682a772011-08-31 15:49:10 -07009319 public void updatePosition(int parentPositionX, int parentPositionY,
9320 boolean parentPositionChanged, boolean parentScrolled);
Gilles Debunne21078e42011-08-02 10:22:35 -07009321 }
9322
9323 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
Luca Zanolin1564fc72011-09-07 00:01:28 +01009324 // 3 handles
9325 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
9326 private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
Gilles Debunne21078e42011-08-02 10:22:35 -07009327 private TextViewPositionListener[] mPositionListeners =
9328 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
9329 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
9330 private boolean mPositionHasChanged = true;
9331 // Absolute position of the TextView with respect to its parent window
9332 private int mPositionX, mPositionY;
9333 private int mNumberOfListeners;
Gilles Debunnef682a772011-08-31 15:49:10 -07009334 private boolean mScrollHasChanged;
Gilles Debunne21078e42011-08-02 10:22:35 -07009335
9336 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
9337 if (mNumberOfListeners == 0) {
9338 updatePosition();
9339 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9340 vto.addOnPreDrawListener(this);
9341 }
9342
9343 int emptySlotIndex = -1;
9344 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9345 TextViewPositionListener listener = mPositionListeners[i];
9346 if (listener == positionListener) {
9347 return;
9348 } else if (emptySlotIndex < 0 && listener == null) {
9349 emptySlotIndex = i;
9350 }
9351 }
9352
9353 mPositionListeners[emptySlotIndex] = positionListener;
9354 mCanMove[emptySlotIndex] = canMove;
9355 mNumberOfListeners++;
9356 }
9357
9358 public void removeSubscriber(TextViewPositionListener positionListener) {
9359 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9360 if (mPositionListeners[i] == positionListener) {
9361 mPositionListeners[i] = null;
9362 mNumberOfListeners--;
9363 break;
9364 }
9365 }
9366
9367 if (mNumberOfListeners == 0) {
9368 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9369 vto.removeOnPreDrawListener(this);
9370 }
9371 }
9372
9373 public int getPositionX() {
9374 return mPositionX;
9375 }
9376
9377 public int getPositionY() {
9378 return mPositionY;
9379 }
9380
9381 @Override
9382 public boolean onPreDraw() {
9383 updatePosition();
9384
9385 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009386 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
Gilles Debunne21078e42011-08-02 10:22:35 -07009387 TextViewPositionListener positionListener = mPositionListeners[i];
9388 if (positionListener != null) {
9389 positionListener.updatePosition(mPositionX, mPositionY,
Gilles Debunnef682a772011-08-31 15:49:10 -07009390 mPositionHasChanged, mScrollHasChanged);
Gilles Debunne21078e42011-08-02 10:22:35 -07009391 }
9392 }
9393 }
9394
Gilles Debunnef682a772011-08-31 15:49:10 -07009395 mScrollHasChanged = false;
Gilles Debunne21078e42011-08-02 10:22:35 -07009396 return true;
9397 }
9398
9399 private void updatePosition() {
9400 TextView.this.getLocationInWindow(mTempCoords);
9401
9402 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
9403
9404 mPositionX = mTempCoords[0];
9405 mPositionY = mTempCoords[1];
9406 }
9407
9408 public boolean isVisible(int positionX, int positionY) {
9409 final TextView textView = TextView.this;
9410
9411 if (mTempRect == null) mTempRect = new Rect();
9412 final Rect clip = mTempRect;
9413 clip.left = getCompoundPaddingLeft();
9414 clip.top = getExtendedPaddingTop();
9415 clip.right = textView.getWidth() - getCompoundPaddingRight();
9416 clip.bottom = textView.getHeight() - getExtendedPaddingBottom();
9417
9418 final ViewParent parent = textView.getParent();
9419 if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
9420 return false;
9421 }
9422
9423 int posX = mPositionX + positionX;
9424 int posY = mPositionY + positionY;
9425
9426 // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
9427 return posX >= clip.left - 1 && posX <= clip.right + 1 &&
9428 posY >= clip.top && posY <= clip.bottom;
9429 }
9430
9431 public boolean isOffsetVisible(int offset) {
9432 final int line = mLayout.getLineForOffset(offset);
9433 final int lineBottom = mLayout.getLineBottom(line);
9434 final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
9435 return isVisible(primaryHorizontal, lineBottom);
9436 }
Gilles Debunnef682a772011-08-31 15:49:10 -07009437
9438 public void onScrollChanged() {
9439 mScrollHasChanged = true;
9440 }
9441 }
9442
9443 @Override
9444 protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
9445 super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
9446 if (mPositionListener != null) {
9447 mPositionListener.onScrollChanged();
9448 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009449 }
9450
9451 private abstract class PinnedPopupWindow implements TextViewPositionListener {
9452 protected PopupWindow mPopupWindow;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009453 protected ViewGroup mContentView;
Gilles Debunne21078e42011-08-02 10:22:35 -07009454 int mPositionX, mPositionY;
9455
9456 protected abstract void createPopupWindow();
9457 protected abstract void initContentView();
9458 protected abstract int getTextOffset();
9459 protected abstract int getVerticalLocalPosition(int line);
9460 protected abstract int clipVertically(int positionY);
9461
9462 public PinnedPopupWindow() {
9463 createPopupWindow();
9464
9465 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9466 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
9467 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
9468
Gilles Debunne0eea6682011-08-29 13:30:31 -07009469 initContentView();
9470
9471 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9472 ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunne21078e42011-08-02 10:22:35 -07009473 mContentView.setLayoutParams(wrapContent);
9474
Gilles Debunne21078e42011-08-02 10:22:35 -07009475 mPopupWindow.setContentView(mContentView);
9476 }
9477
9478 public void show() {
Gilles Debunnef682a772011-08-31 15:49:10 -07009479 TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
Gilles Debunne21078e42011-08-02 10:22:35 -07009480
9481 computeLocalPosition();
9482
9483 final PositionListener positionListener = TextView.this.getPositionListener();
9484 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
9485 }
Gilles Debunne0eea6682011-08-29 13:30:31 -07009486
9487 protected void measureContent() {
9488 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9489 mContentView.measure(
9490 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
9491 View.MeasureSpec.AT_MOST),
9492 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
9493 View.MeasureSpec.AT_MOST));
9494 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009495
Gilles Debunne0eea6682011-08-29 13:30:31 -07009496 /* The popup window will be horizontally centered on the getTextOffset() and vertically
9497 * positioned according to viewportToContentHorizontalOffset.
9498 *
9499 * This method assumes that mContentView has properly been measured from its content. */
Gilles Debunne21078e42011-08-02 10:22:35 -07009500 private void computeLocalPosition() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009501 measureContent();
Gilles Debunne21078e42011-08-02 10:22:35 -07009502 final int width = mContentView.getMeasuredWidth();
Gilles Debunne0eea6682011-08-29 13:30:31 -07009503 final int offset = getTextOffset();
Gilles Debunne21078e42011-08-02 10:22:35 -07009504 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
9505 mPositionX += viewportToContentHorizontalOffset();
9506
9507 final int line = mLayout.getLineForOffset(offset);
9508 mPositionY = getVerticalLocalPosition(line);
9509 mPositionY += viewportToContentVerticalOffset();
9510 }
9511
9512 private void updatePosition(int parentPositionX, int parentPositionY) {
9513 int positionX = parentPositionX + mPositionX;
9514 int positionY = parentPositionY + mPositionY;
9515
9516 positionY = clipVertically(positionY);
9517
9518 // Horizontal clipping
9519 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9520 final int width = mContentView.getMeasuredWidth();
9521 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
9522 positionX = Math.max(0, positionX);
9523
9524 if (isShowing()) {
9525 mPopupWindow.update(positionX, positionY, -1, -1);
9526 } else {
9527 mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
9528 positionX, positionY);
9529 }
9530 }
9531
9532 public void hide() {
9533 mPopupWindow.dismiss();
9534 TextView.this.getPositionListener().removeSubscriber(this);
9535 }
9536
9537 @Override
Gilles Debunnef682a772011-08-31 15:49:10 -07009538 public void updatePosition(int parentPositionX, int parentPositionY,
9539 boolean parentPositionChanged, boolean parentScrolled) {
9540 // Either parentPositionChanged or parentScrolled is true, check if still visible
Gilles Debunne21078e42011-08-02 10:22:35 -07009541 if (isShowing() && getPositionListener().isOffsetVisible(getTextOffset())) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009542 if (parentScrolled) computeLocalPosition();
Gilles Debunne21078e42011-08-02 10:22:35 -07009543 updatePosition(parentPositionX, parentPositionY);
9544 } else {
9545 hide();
9546 }
9547 }
9548
9549 public boolean isShowing() {
9550 return mPopupWindow.isShowing();
9551 }
9552 }
9553
Gilles Debunne0eea6682011-08-29 13:30:31 -07009554 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunne6435a562011-08-04 21:22:30 -07009555 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009556 private SuggestionInfo[] mSuggestionInfos;
9557 private int mNumberOfSuggestions;
Gilles Debunne28294cc2011-08-24 12:02:05 -07009558 private boolean mCursorWasVisibleBeforeSuggestions;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009559 private SuggestionAdapter mSuggestionsAdapter;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009560 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
9561 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
9562
Gilles Debunne28294cc2011-08-24 12:02:05 -07009563
9564 private class CustomPopupWindow extends PopupWindow {
9565 public CustomPopupWindow(Context context, int defStyle) {
9566 super(context, null, defStyle);
9567 }
9568
9569 @Override
9570 public void dismiss() {
9571 super.dismiss();
9572
Fabrice Di Meglio03e4d642011-09-06 19:06:06 -07009573 TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
9574
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009575 if ((mText instanceof Spannable)) {
9576 ((Spannable) mText).removeSpan(mSuggestionRangeSpan);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009577 }
9578
9579 setCursorVisible(mCursorWasVisibleBeforeSuggestions);
9580 if (hasInsertionController()) {
9581 getInsertionController().show();
9582 }
9583 }
9584 }
9585
9586 public SuggestionsPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009587 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009588 mSuggestionSpanComparator = new SuggestionSpanComparator();
9589 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
Gilles Debunne28294cc2011-08-24 12:02:05 -07009590 }
Gilles Debunne69340442011-03-31 13:37:51 -07009591
Gilles Debunne21078e42011-08-02 10:22:35 -07009592 @Override
9593 protected void createPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009594 mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
Gilles Debunne21078e42011-08-02 10:22:35 -07009595 com.android.internal.R.attr.textSuggestionsWindowStyle);
Gilles Debunne28885242011-07-25 18:56:09 -07009596 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009597 mPopupWindow.setFocusable(true);
Gilles Debunne21078e42011-08-02 10:22:35 -07009598 mPopupWindow.setClippingEnabled(false);
9599 }
Gilles Debunne69340442011-03-31 13:37:51 -07009600
Gilles Debunne21078e42011-08-02 10:22:35 -07009601 @Override
9602 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009603 ListView listView = new ListView(TextView.this.getContext());
9604 mSuggestionsAdapter = new SuggestionAdapter();
9605 listView.setAdapter(mSuggestionsAdapter);
9606 listView.setOnItemClickListener(this);
9607 mContentView = listView;
Gilles Debunne28885242011-07-25 18:56:09 -07009608
Gilles Debunnee90bed12011-08-30 14:28:27 -07009609 // Inflate the suggestion items once and for all. +1 for add to dictionary
9610 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 1];
9611 for (int i = 0; i < MAX_NUMBER_SUGGESTIONS + 1; i++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009612 mSuggestionInfos[i] = new SuggestionInfo();
Gilles Debunne28885242011-07-25 18:56:09 -07009613 }
Gilles Debunne69340442011-03-31 13:37:51 -07009614 }
9615
Gilles Debunne214a8622011-04-26 15:44:37 -07009616 private class SuggestionInfo {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009617 int suggestionStart, suggestionEnd; // range of actual suggestion within text
Gilles Debunneee511cc2011-05-05 14:57:50 -07009618 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009619 int suggestionIndex; // the index of this suggestion inside suggestionSpan
Gilles Debunne0eea6682011-08-29 13:30:31 -07009620 SpannableStringBuilder text = new SpannableStringBuilder();
Luca Zanolin2346e012011-09-06 22:56:47 +01009621 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
9622 android.R.style.TextAppearance_SuggestionHighlight);
Gilles Debunnee90bed12011-08-30 14:28:27 -07009623
9624 void removeMisspelledFlag() {
9625 int suggestionSpanFlags = suggestionSpan.getFlags();
9626 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
9627 suggestionSpanFlags &= ~(SuggestionSpan.FLAG_MISSPELLED);
9628 suggestionSpanFlags &= ~(SuggestionSpan.FLAG_EASY_CORRECT);
9629 suggestionSpan.setFlags(suggestionSpanFlags);
9630 }
9631 }
Gilles Debunne0eea6682011-08-29 13:30:31 -07009632 }
9633
9634 private class SuggestionAdapter extends BaseAdapter {
9635 private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
9636 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
9637
9638 @Override
9639 public int getCount() {
9640 return mNumberOfSuggestions;
9641 }
9642
9643 @Override
9644 public Object getItem(int position) {
9645 return mSuggestionInfos[position];
9646 }
9647
9648 @Override
9649 public long getItemId(int position) {
9650 return position;
9651 }
9652
9653 @Override
9654 public View getView(int position, View convertView, ViewGroup parent) {
9655 TextView textView = (TextView) convertView;
9656
9657 if (textView == null) {
9658 textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
9659 false);
9660 }
9661
9662 textView.setText(mSuggestionInfos[position].text);
9663 return textView;
9664 }
Gilles Debunne214a8622011-04-26 15:44:37 -07009665 }
9666
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009667 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
9668 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
9669 final int flag1 = span1.getFlags();
9670 final int flag2 = span2.getFlags();
9671 if (flag1 != flag2) {
9672 // The order here should match what is used in updateDrawState
9673 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9674 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9675 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9676 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9677 if (easy1 && !misspelled1) return -1;
9678 if (easy2 && !misspelled2) return 1;
9679 if (misspelled1) return -1;
9680 if (misspelled2) return 1;
9681 }
9682
9683 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
9684 }
9685 }
9686
Luca Zanoline3f89c02011-08-01 09:55:17 +01009687 /**
9688 * Returns the suggestion spans that cover the current cursor position. The suggestion
9689 * spans are sorted according to the length of text that they are attached to.
9690 */
9691 private SuggestionSpan[] getSuggestionSpans() {
9692 int pos = TextView.this.getSelectionStart();
9693 Spannable spannable = (Spannable) TextView.this.mText;
9694 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
9695
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009696 mSpansLengths.clear();
Luca Zanoline3f89c02011-08-01 09:55:17 +01009697 for (SuggestionSpan suggestionSpan : suggestionSpans) {
9698 int start = spannable.getSpanStart(suggestionSpan);
9699 int end = spannable.getSpanEnd(suggestionSpan);
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009700 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
Luca Zanoline3f89c02011-08-01 09:55:17 +01009701 }
9702
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009703 // The suggestions are sorted according to their types (easy correction first, then
9704 // misspelled) and to the length of the text that they cover (shorter first).
9705 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
Luca Zanoline3f89c02011-08-01 09:55:17 +01009706 return suggestionSpans;
9707 }
9708
Gilles Debunne21078e42011-08-02 10:22:35 -07009709 @Override
Gilles Debunne69340442011-03-31 13:37:51 -07009710 public void show() {
9711 if (!(mText instanceof Editable)) return;
9712
Gilles Debunne6435a562011-08-04 21:22:30 -07009713 if (updateSuggestions()) {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009714 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9715 setCursorVisible(false);
Gilles Debunne6435a562011-08-04 21:22:30 -07009716 super.show();
9717 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009718 }
9719
9720 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009721 protected void measureContent() {
9722 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9723 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
9724 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
9725 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
9726 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
9727
9728 int width = 0;
9729 View view = null;
9730 for (int i = 0; i < mNumberOfSuggestions; i++) {
9731 view = mSuggestionsAdapter.getView(i, view, mContentView);
9732 view.measure(horizontalMeasure, verticalMeasure);
9733 width = Math.max(width, view.getMeasuredWidth());
9734 }
9735
9736 // Enforce the width based on actual text widths
9737 mContentView.measure(
9738 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
9739 verticalMeasure);
9740
9741 Drawable popupBackground = mPopupWindow.getBackground();
9742 if (popupBackground != null) {
9743 if (mTempRect == null) mTempRect = new Rect();
9744 popupBackground.getPadding(mTempRect);
9745 width += mTempRect.left + mTempRect.right;
9746 }
9747 mPopupWindow.setWidth(width);
9748 }
9749
9750 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -07009751 protected int getTextOffset() {
9752 return getSelectionStart();
9753 }
9754
9755 @Override
9756 protected int getVerticalLocalPosition(int line) {
9757 return mLayout.getLineBottom(line);
9758 }
9759
9760 @Override
9761 protected int clipVertically(int positionY) {
9762 final int height = mContentView.getMeasuredHeight();
9763 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9764 return Math.min(positionY, displayMetrics.heightPixels - height);
9765 }
9766
9767 @Override
9768 public void hide() {
9769 super.hide();
Gilles Debunne21078e42011-08-02 10:22:35 -07009770 }
9771
Gilles Debunne6435a562011-08-04 21:22:30 -07009772 private boolean updateSuggestions() {
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009773 Spannable spannable = (Spannable) TextView.this.mText;
Luca Zanoline3f89c02011-08-01 09:55:17 +01009774 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
9775
satokb3fc1a52011-04-06 18:28:55 +09009776 final int nbSpans = suggestionSpans.length;
Gilles Debunne69340442011-03-31 13:37:51 -07009777
Gilles Debunne0eea6682011-08-29 13:30:31 -07009778 mNumberOfSuggestions = 0;
Gilles Debunne214a8622011-04-26 15:44:37 -07009779 int spanUnionStart = mText.length();
9780 int spanUnionEnd = 0;
9781
Gilles Debunnee90bed12011-08-30 14:28:27 -07009782 SuggestionSpan misspelledSpan = null;
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009783 int underlineColor = 0;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009784
Gilles Debunne69340442011-03-31 13:37:51 -07009785 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
satokb3fc1a52011-04-06 18:28:55 +09009786 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
9787 final int spanStart = spannable.getSpanStart(suggestionSpan);
9788 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009789 spanUnionStart = Math.min(spanStart, spanUnionStart);
9790 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
Gilles Debunne69340442011-03-31 13:37:51 -07009791
Gilles Debunnee90bed12011-08-30 14:28:27 -07009792 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
9793 misspelledSpan = suggestionSpan;
9794 }
9795
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009796 // The first span dictates the background color of the highlighted text
9797 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
9798
satokb3fc1a52011-04-06 18:28:55 +09009799 String[] suggestions = suggestionSpan.getSuggestions();
Gilles Debunne69340442011-03-31 13:37:51 -07009800 int nbSuggestions = suggestions.length;
9801 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009802 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunneee511cc2011-05-05 14:57:50 -07009803 suggestionInfo.suggestionSpan = suggestionSpan;
9804 suggestionInfo.suggestionIndex = suggestionIndex;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009805 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9806 suggestions[suggestionIndex]);
Gilles Debunne69340442011-03-31 13:37:51 -07009807
Gilles Debunne0eea6682011-08-29 13:30:31 -07009808 mNumberOfSuggestions++;
9809 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
Gilles Debunne214a8622011-04-26 15:44:37 -07009810 // Also end outer for loop
Gilles Debunne69340442011-03-31 13:37:51 -07009811 spanIndex = nbSpans;
9812 break;
9813 }
9814 }
9815 }
9816
Gilles Debunnee90bed12011-08-30 14:28:27 -07009817 for (int i = 0; i < mNumberOfSuggestions; i++) {
9818 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
9819 }
9820
9821 if (misspelledSpan != null) {
9822 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
9823 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
9824 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
9825 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunnee90bed12011-08-30 14:28:27 -07009826 suggestionInfo.suggestionSpan = misspelledSpan;
9827 suggestionInfo.suggestionIndex = -1;
9828 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9829 getContext().getString(com.android.internal.R.string.addToDictionary));
9830
9831 mNumberOfSuggestions++;
9832 }
9833 }
9834
Gilles Debunne0eea6682011-08-29 13:30:31 -07009835 if (mNumberOfSuggestions == 0) return false;
Gilles Debunne214a8622011-04-26 15:44:37 -07009836
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009837 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
9838 if (underlineColor == 0) {
9839 // Fallback on the default highlight color when the first span does not provide one
9840 mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
9841 } else {
9842 final float BACKGROUND_TRANSPARENCY = 0.3f;
9843 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
9844 mSuggestionRangeSpan.setBackgroundColor(
9845 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
9846 }
9847 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
Gilles Debunne6435a562011-08-04 21:22:30 -07009848 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9849
Gilles Debunne0eea6682011-08-29 13:30:31 -07009850 mSuggestionsAdapter.notifyDataSetChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -07009851
9852 return true;
9853 }
9854
Luca Zanolin2346e012011-09-06 22:56:47 +01009855 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
9856 int unionEnd) {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009857 final Spannable text = (Spannable) mText;
9858 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
9859 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009860
Luca Zanolin2346e012011-09-06 22:56:47 +01009861 // Adjust the start/end of the suggestion span
9862 suggestionInfo.suggestionStart = spanStart - unionStart;
9863 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
9864 + suggestionInfo.text.length();
9865
9866 suggestionInfo.text.clearSpans();
9867 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
9868 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne214a8622011-04-26 15:44:37 -07009869
Luca Zanolin2346e012011-09-06 22:56:47 +01009870 // Add the text before and after the span.
9871 suggestionInfo.text.insert(0, mText.subSequence(unionStart, spanStart).toString());
9872 suggestionInfo.text.append(mText.subSequence(spanEnd, unionEnd).toString());
Gilles Debunne214a8622011-04-26 15:44:37 -07009873 }
9874
Gilles Debunne69340442011-03-31 13:37:51 -07009875 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009876 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunne69340442011-03-31 13:37:51 -07009877 if (view instanceof TextView) {
9878 TextView textView = (TextView) view;
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009879 Editable editable = (Editable) mText;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009880
Gilles Debunne0eea6682011-08-29 13:30:31 -07009881 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009882 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
9883 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunnee90bed12011-08-30 14:28:27 -07009884 final String originalText = mText.subSequence(spanStart, spanEnd).toString();
9885
9886 if (suggestionInfo.suggestionIndex < 0) {
9887 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
9888 intent.putExtra("word", originalText);
9889 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
9890 getContext().startActivity(intent);
9891 suggestionInfo.removeMisspelledFlag();
9892 } else {
Gilles Debunneee511cc2011-05-05 14:57:50 -07009893 // SuggestionSpans are removed by replace: save them before
Gilles Debunneee511cc2011-05-05 14:57:50 -07009894 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
9895 SuggestionSpan.class);
9896 final int length = suggestionSpans.length;
9897 int[] suggestionSpansStarts = new int[length];
9898 int[] suggestionSpansEnds = new int[length];
9899 int[] suggestionSpansFlags = new int[length];
9900 for (int i = 0; i < length; i++) {
9901 final SuggestionSpan suggestionSpan = suggestionSpans[i];
9902 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
9903 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
9904 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
9905 }
9906
Gilles Debunne214a8622011-04-26 15:44:37 -07009907 final int suggestionStart = suggestionInfo.suggestionStart;
9908 final int suggestionEnd = suggestionInfo.suggestionEnd;
9909 final String suggestion = textView.getText().subSequence(
9910 suggestionStart, suggestionEnd).toString();
Gilles Debunne28294cc2011-08-24 12:02:05 -07009911 editable.replace(spanStart, spanEnd, suggestion);
Gilles Debunneee511cc2011-05-05 14:57:50 -07009912
Gilles Debunnee90bed12011-08-30 14:28:27 -07009913 suggestionInfo.removeMisspelledFlag();
Gilles Debunne6435a562011-08-04 21:22:30 -07009914
Gilles Debunne4d802802011-06-14 10:55:01 -07009915 // Notify source IME of the suggestion pick. Do this before swaping texts.
Gilles Debunneb1619e52011-05-26 14:39:34 -07009916 if (!TextUtils.isEmpty(
9917 suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
9918 InputMethodManager imm = InputMethodManager.peekInstance();
9919 imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
9920 suggestionInfo.suggestionIndex);
9921 }
9922
Gilles Debunne4d802802011-06-14 10:55:01 -07009923 // Swap text content between actual text and Suggestion span
9924 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
9925 suggestions[suggestionInfo.suggestionIndex] = originalText;
9926
Gilles Debunneee511cc2011-05-05 14:57:50 -07009927 // Restore previous SuggestionSpans
9928 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
9929 for (int i = 0; i < length; i++) {
9930 // Only spans that include the modified region make sense after replacement
9931 // Spans partially included in the replaced region are removed, there is no
9932 // way to assign them a valid range after replacement
9933 if (suggestionSpansStarts[i] <= spanStart &&
9934 suggestionSpansEnds[i] >= spanEnd) {
9935 editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i],
9936 suggestionSpansEnds[i] + lengthDifference,
9937 suggestionSpansFlags[i]);
9938 }
9939 }
Gilles Debunne1a87ba42011-09-09 14:49:26 -07009940
9941 // Move cursor at the end of the replacement word
9942 Selection.setSelection(editable, spanEnd + lengthDifference);
Gilles Debunne69340442011-03-31 13:37:51 -07009943 }
9944 }
9945 hide();
9946 }
Gilles Debunne69340442011-03-31 13:37:51 -07009947 }
9948
Luca Zanoline0760452011-09-08 12:03:37 +01009949 /**
9950 * Removes the suggestion spans.
9951 */
9952 CharSequence removeSuggestionSpans(CharSequence text) {
9953 if (text instanceof Spanned) {
9954 Spannable spannable;
9955 if (text instanceof Spannable) {
9956 spannable = (Spannable) text;
9957 } else {
9958 spannable = new SpannableString(text);
9959 text = spannable;
9960 }
Gilles Debunnef3a135b2011-05-23 16:28:47 -07009961
Luca Zanoline0760452011-09-08 12:03:37 +01009962 SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
9963 for (int i = 0; i < spans.length; i++) {
9964 spannable.removeSpan(spans[i]);
9965 }
9966 }
9967 return text;
9968 }
9969
9970 void showSuggestions() {
Gilles Debunne69340442011-03-31 13:37:51 -07009971 if (mSuggestionsPopupWindow == null) {
9972 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
9973 }
9974 hideControllers();
9975 mSuggestionsPopupWindow.show();
9976 }
9977
Gilles Debunne4a7199a2011-07-11 14:58:27 -07009978 boolean areSuggestionsShown() {
9979 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
Gilles Debunne6435a562011-08-04 21:22:30 -07009980 }
9981
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009982 /**
Gilles Debunne6435a562011-08-04 21:22:30 -07009983 * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
9984 * by the IME or by the spell checker as the user types. This is done by adding
9985 * {@link SuggestionSpan}s to the text.
Gilles Debunnef3a135b2011-05-23 16:28:47 -07009986 *
9987 * When suggestions are enabled (default), this list of suggestions will be displayed when the
Gilles Debunne6435a562011-08-04 21:22:30 -07009988 * user asks for them on these parts of the text. This value depends on the inputType of this
9989 * TextView.
Gilles Debunnef3a135b2011-05-23 16:28:47 -07009990 *
Gilles Debunne6435a562011-08-04 21:22:30 -07009991 * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
9992 *
9993 * In addition, the type variation must be one of
Gilles Debunne248b1122011-08-12 13:24:16 -07009994 * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
9995 * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
9996 * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
9997 * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
9998 * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
9999 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010000 * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010001 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010002 * @return true if the suggestions popup window is enabled, based on the inputType.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010003 */
10004 public boolean isSuggestionsEnabled() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010005 if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false;
Gilles Debunne6435a562011-08-04 21:22:30 -070010006 if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
10007
Luca Zanolin15b80162011-08-17 18:47:27 +010010008 final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Gilles Debunne6435a562011-08-04 21:22:30 -070010009 return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL ||
Gilles Debunne248b1122011-08-12 13:24:16 -070010010 variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT ||
10011 variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE ||
10012 variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE ||
Gilles Debunne6435a562011-08-04 21:22:30 -070010013 variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010014 }
10015
10016 /**
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010017 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
10018 * selection is initiated in this View.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010019 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010020 * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
10021 * Paste actions, depending on what this View supports.
10022 *
10023 * A custom implementation can add new entries in the default menu in its
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010024 * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The
10025 * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and
10026 * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
10027 * or {@link android.R.id#paste} ids as parameters.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010028 *
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010029 * Returning false from
10030 * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent
10031 * the action mode from being started.
Gilles Debunneddd6f392011-01-27 09:48:01 -080010032 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010033 * Action click events should be handled by the custom implementation of
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010034 * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010035 *
10036 * Note that text selection mode is not started when a TextView receives focus and the
10037 * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
10038 * that case, to allow for quick replacement.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010039 */
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010040 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
10041 mCustomSelectionActionModeCallback = actionModeCallback;
10042 }
10043
10044 /**
10045 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
10046 *
10047 * @return The current custom selection callback.
10048 */
10049 public ActionMode.Callback getCustomSelectionActionModeCallback() {
10050 return mCustomSelectionActionModeCallback;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010051 }
10052
10053 /**
10054 *
10055 * @return true if the selection mode was actually started.
10056 */
10057 private boolean startSelectionActionMode() {
10058 if (mSelectionActionMode != null) {
10059 // Selection action mode is already started
10060 return false;
10061 }
10062
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010063 if (!canSelectText() || !requestFocus()) {
10064 Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
10065 return false;
10066 }
10067
Gilles Debunneed674182011-06-02 19:52:22 -070010068 if (!hasSelection()) {
10069 // There may already be a selection on device rotation
10070 boolean currentWordSelected = selectCurrentWord();
10071 if (!currentWordSelected) {
10072 // No word found under cursor or text selection not permitted.
10073 return false;
10074 }
Gilles Debunnec01f3fe2010-12-22 17:07:36 -080010075 }
10076
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010077 final InputMethodManager imm = InputMethodManager.peekInstance();
10078 boolean extractedTextModeWillBeStartedFullScreen = !(this instanceof ExtractEditText) &&
10079 imm != null && imm.isFullscreenMode();
Gilles Debunne17d31de2011-01-27 11:02:18 -080010080
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010081 // Do not start the action mode when extracted text will show up full screen, thus
10082 // immediately hiding the newly created action bar, which would be visually distracting.
10083 if (!extractedTextModeWillBeStartedFullScreen) {
10084 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
10085 mSelectionActionMode = startActionMode(actionModeCallback);
10086 }
10087 final boolean selectionStarted = mSelectionActionMode != null ||
10088 extractedTextModeWillBeStartedFullScreen;
10089
10090 if (selectionStarted && !mTextIsSelectable && imm != null) {
Gilles Debunne17d31de2011-01-27 11:02:18 -080010091 // Show the IME to be able to replace text, except when selecting non editable text.
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010092 imm.showSoftInput(this, 0, null);
Gilles Debunne17d31de2011-01-27 11:02:18 -080010093 }
10094
10095 return selectionStarted;
Gilles Debunne05336272010-07-09 20:13:45 -070010096 }
10097
Gilles Debunneed279f82010-08-18 21:24:35 -070010098 private void stopSelectionActionMode() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010099 if (mSelectionActionMode != null) {
Gilles Debunned94f8c52011-01-10 11:29:15 -080010100 // This will hide the mSelectionModifierCursorController
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010101 mSelectionActionMode.finish();
10102 }
10103 }
10104
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010105 /**
10106 * Paste clipboard content between min and max positions.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010107 */
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010108 private void paste(int min, int max) {
10109 ClipboardManager clipboard =
10110 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010111 ClipData clip = clipboard.getPrimaryClip();
10112 if (clip != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010113 boolean didFirst = false;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010114 for (int i=0; i<clip.getItemCount(); i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080010115 CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010116 if (paste != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010117 if (!didFirst) {
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010118 long minMax = prepareSpacesAroundPaste(min, max, paste);
10119 min = extractRangeStartFromLong(minMax);
10120 max = extractRangeEndFromLong(minMax);
10121 Selection.setSelection((Spannable) mText, max);
10122 ((Editable) mText).replace(min, max, paste);
Gilles Debunne75beb332011-04-29 11:40:22 -070010123 didFirst = true;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010124 } else {
10125 ((Editable) mText).insert(getSelectionEnd(), "\n");
10126 ((Editable) mText).insert(getSelectionEnd(), paste);
10127 }
10128 }
10129 }
10130 stopSelectionActionMode();
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010131 sLastCutOrCopyTime = 0;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010132 }
10133 }
10134
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010135 private void setPrimaryClip(ClipData clip) {
10136 ClipboardManager clipboard = (ClipboardManager) getContext().
10137 getSystemService(Context.CLIPBOARD_SERVICE);
10138 clipboard.setPrimaryClip(clip);
10139 sLastCutOrCopyTime = SystemClock.uptimeMillis();
10140 }
10141
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010142 /**
10143 * An ActionMode Callback class that is used to provide actions while in text selection mode.
10144 *
10145 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
10146 * on which of these this TextView supports.
10147 */
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010148 private class SelectionActionModeCallback implements ActionMode.Callback {
10149
10150 @Override
10151 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Gilles Debunne78996c92010-10-12 16:01:47 -070010152 TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
10153
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010154 boolean allowText = getContext().getResources().getBoolean(
Adam Powell35aecd52011-07-01 13:43:49 -070010155 com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010156
Gilles Debunne21078e42011-08-02 10:22:35 -070010157 mode.setTitle(allowText ?
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010158 mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010159 mode.setSubtitle(null);
10160
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010161 int selectAllIconId = 0; // No icon by default
10162 if (!allowText) {
10163 // Provide an icon, text will not be displayed on smaller screens.
10164 selectAllIconId = styledAttributes.getResourceId(
10165 R.styleable.Theme_actionModeSelectAllDrawable, 0);
10166 }
10167
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010168 menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010169 setIcon(selectAllIconId).
Adam Powelld8404b22010-10-13 14:26:41 -070010170 setAlphabeticShortcut('a').
10171 setShowAsAction(
10172 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010173
10174 if (canCut()) {
10175 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010176 setIcon(styledAttributes.getResourceId(
10177 R.styleable.Theme_actionModeCutDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010178 setAlphabeticShortcut('x').
10179 setShowAsAction(
10180 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010181 }
10182
10183 if (canCopy()) {
10184 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010185 setIcon(styledAttributes.getResourceId(
10186 R.styleable.Theme_actionModeCopyDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010187 setAlphabeticShortcut('c').
10188 setShowAsAction(
10189 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010190 }
10191
10192 if (canPaste()) {
10193 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010194 setIcon(styledAttributes.getResourceId(
10195 R.styleable.Theme_actionModePasteDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010196 setAlphabeticShortcut('v').
10197 setShowAsAction(
10198 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010199 }
10200
Gilles Debunne78996c92010-10-12 16:01:47 -070010201 styledAttributes.recycle();
10202
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010203 if (mCustomSelectionActionModeCallback != null) {
Gilles Debunneddd6f392011-01-27 09:48:01 -080010204 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
10205 // The custom mode can choose to cancel the action mode
10206 return false;
10207 }
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010208 }
10209
10210 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -080010211 getSelectionController().show();
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010212 return true;
10213 } else {
10214 return false;
10215 }
Gilles Debunne05336272010-07-09 20:13:45 -070010216 }
10217
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010218 @Override
10219 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010220 if (mCustomSelectionActionModeCallback != null) {
10221 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
10222 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010223 return true;
10224 }
10225
10226 @Override
10227 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010228 if (mCustomSelectionActionModeCallback != null &&
10229 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
10230 return true;
10231 }
Jeff Brownc1df9072010-12-21 16:38:50 -080010232 return onTextContextMenuItem(item.getItemId());
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010233 }
10234
10235 @Override
10236 public void onDestroyActionMode(ActionMode mode) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010237 if (mCustomSelectionActionModeCallback != null) {
10238 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
10239 }
Gilles Debunned94f8c52011-01-10 11:29:15 -080010240 Selection.setSelection((Spannable) mText, getSelectionEnd());
10241
10242 if (mSelectionModifierCursorController != null) {
10243 mSelectionModifierCursorController.hide();
10244 }
10245
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010246 mSelectionActionMode = null;
10247 }
10248 }
Gilles Debunne05336272010-07-09 20:13:45 -070010249
Gilles Debunne21078e42011-08-02 10:22:35 -070010250 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
10251 private static final int POPUP_TEXT_LAYOUT =
Gilles Debunne646f8562011-07-27 17:44:03 -070010252 com.android.internal.R.layout.text_edit_action_popup_text;
Gilles Debunne646f8562011-07-27 17:44:03 -070010253 private TextView mPasteTextView;
10254 private TextView mReplaceTextView;
Gilles Debunne646f8562011-07-27 17:44:03 -070010255
Gilles Debunne21078e42011-08-02 10:22:35 -070010256 @Override
10257 protected void createPopupWindow() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010258 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010259 com.android.internal.R.attr.textSelectHandleWindowStyle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010260 mPopupWindow.setClippingEnabled(true);
Gilles Debunne21078e42011-08-02 10:22:35 -070010261 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010262
Gilles Debunne21078e42011-08-02 10:22:35 -070010263 @Override
10264 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -070010265 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
10266 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
10267 mContentView = linearLayout;
Gilles Debunne646f8562011-07-27 17:44:03 -070010268 mContentView.setBackgroundResource(
Gilles Debunne0eea6682011-08-29 13:30:31 -070010269 com.android.internal.R.drawable.text_edit_paste_window);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010270
Gilles Debunne646f8562011-07-27 17:44:03 -070010271 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010272 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010273
Gilles Debunne21078e42011-08-02 10:22:35 -070010274 LayoutParams wrapContent = new LayoutParams(
10275 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
10276
10277 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010278 mPasteTextView.setLayoutParams(wrapContent);
10279 mContentView.addView(mPasteTextView);
10280 mPasteTextView.setText(com.android.internal.R.string.paste);
10281 mPasteTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010282
Gilles Debunne21078e42011-08-02 10:22:35 -070010283 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010284 mReplaceTextView.setLayoutParams(wrapContent);
10285 mContentView.addView(mReplaceTextView);
10286 mReplaceTextView.setText(com.android.internal.R.string.replace);
10287 mReplaceTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010288 }
10289
Gilles Debunne21078e42011-08-02 10:22:35 -070010290 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010291 public void show() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010292 boolean canPaste = canPaste();
Gilles Debunne6435a562011-08-04 21:22:30 -070010293 boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
Gilles Debunne248b1122011-08-12 13:24:16 -070010294 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
Gilles Debunne6435a562011-08-04 21:22:30 -070010295 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
Gilles Debunne072b50c2011-08-02 20:22:43 -070010296
Gilles Debunne6435a562011-08-04 21:22:30 -070010297 if (!canPaste && !canSuggest) return;
Gilles Debunne072b50c2011-08-02 20:22:43 -070010298
Gilles Debunne21078e42011-08-02 10:22:35 -070010299 super.show();
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010300 }
10301
10302 @Override
Gilles Debunne646f8562011-07-27 17:44:03 -070010303 public void onClick(View view) {
10304 if (view == mPasteTextView && canPaste()) {
Gilles Debunne459ac632011-07-12 16:36:33 -070010305 onTextContextMenuItem(ID_PASTE);
Gilles Debunne646f8562011-07-27 17:44:03 -070010306 hide();
10307 } else if (view == mReplaceTextView) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010308 final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
10309 stopSelectionActionMode();
10310 Selection.setSelection((Spannable) mText, middle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010311 showSuggestions();
Gilles Debunnee1c14e62010-11-03 19:24:29 -070010312 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010313 }
10314
Gilles Debunne21078e42011-08-02 10:22:35 -070010315 @Override
10316 protected int getTextOffset() {
10317 return (getSelectionStart() + getSelectionEnd()) / 2;
10318 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010319
Gilles Debunne21078e42011-08-02 10:22:35 -070010320 @Override
10321 protected int getVerticalLocalPosition(int line) {
10322 return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
10323 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010324
Gilles Debunne21078e42011-08-02 10:22:35 -070010325 @Override
10326 protected int clipVertically(int positionY) {
10327 if (positionY < 0) {
10328 final int offset = getTextOffset();
10329 final int line = mLayout.getLineForOffset(offset);
10330 positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
10331 positionY += mContentView.getMeasuredHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010332
Gilles Debunne646f8562011-07-27 17:44:03 -070010333 // Assumes insertion and selection handles share the same height
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010334 final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
Gilles Debunne21078e42011-08-02 10:22:35 -070010335 positionY += handle.getIntrinsicHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010336 }
10337
Gilles Debunne21078e42011-08-02 10:22:35 -070010338 return positionY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010339 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010340 }
10341
Gilles Debunne21078e42011-08-02 10:22:35 -070010342 private abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010343 protected Drawable mDrawable;
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010344 protected Drawable mDrawableLtr;
10345 protected Drawable mDrawableRtl;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010346 private final PopupWindow mContainer;
10347 // Position with respect to the parent TextView
10348 private int mPositionX, mPositionY;
Adam Powell879fb6b2010-09-20 11:23:56 -070010349 private boolean mIsDragging;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010350 // Offset from touch position to mPosition
10351 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
Gilles Debunne21078e42011-08-02 10:22:35 -070010352 protected int mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010353 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
Adam Powellfbb3b472010-10-06 21:04:35 -070010354 private float mTouchOffsetY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010355 // Where the touch position should be on the handle to ensure a maximum cursor visibility
10356 private float mIdealVerticalOffset;
Gilles Debunne2037b822011-04-22 13:07:33 -070010357 // Parent's (TextView) previous position in window
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010358 private int mLastParentX, mLastParentY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010359 // Transient action popup window for Paste and Replace actions
10360 protected ActionPopupWindow mActionPopupWindow;
Gilles Debunne21078e42011-08-02 10:22:35 -070010361 // Previous text character offset
10362 private int mPreviousOffset = -1;
10363 // Previous text character offset
10364 private boolean mPositionHasChanged = true;
Gilles Debunne646f8562011-07-27 17:44:03 -070010365 // Used to delay the appearance of the action popup window
10366 private Runnable mActionPopupShower;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010367
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010368 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010369 super(TextView.this.mContext);
10370 mContainer = new PopupWindow(TextView.this.mContext, null,
10371 com.android.internal.R.attr.textSelectHandleWindowStyle);
10372 mContainer.setSplitTouchEnabled(true);
10373 mContainer.setClippingEnabled(false);
10374 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
10375 mContainer.setContentView(this);
10376
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010377 mDrawableLtr = drawableLtr;
10378 mDrawableRtl = drawableRtl;
10379
10380 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010381
10382 final int handleHeight = mDrawable.getIntrinsicHeight();
10383 mTouchOffsetY = -0.3f * handleHeight;
10384 mIdealVerticalOffset = 0.7f * handleHeight;
10385 }
10386
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010387 protected void updateDrawable() {
10388 final int offset = getCurrentCursorOffset();
10389 final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
10390 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
10391 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
10392 }
10393
10394 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070010395
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010396 // Touch-up filter: number of previous positions remembered
10397 private static final int HISTORY_SIZE = 5;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010398 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
10399 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010400 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
10401 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
10402 private int mPreviousOffsetIndex = 0;
10403 private int mNumberPreviousOffsets = 0;
10404
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010405 private void startTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010406 mNumberPreviousOffsets = 0;
10407 addPositionToTouchUpFilter(offset);
10408 }
10409
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010410 private void addPositionToTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010411 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
10412 mPreviousOffsets[mPreviousOffsetIndex] = offset;
10413 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
10414 mNumberPreviousOffsets++;
10415 }
10416
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010417 private void filterOnTouchUp() {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010418 final long now = SystemClock.uptimeMillis();
10419 int i = 0;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010420 int index = mPreviousOffsetIndex;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010421 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010422 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010423 i++;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010424 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010425 }
10426
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010427 if (i > 0 && i < iMax &&
10428 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010429 positionAtCursorOffset(mPreviousOffsets[index], false);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010430 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010431 }
10432
Gilles Debunne040023a2011-08-02 18:23:31 -070010433 public boolean offsetHasBeenChanged() {
10434 return mNumberPreviousOffsets > 1;
10435 }
10436
Adam Powell879fb6b2010-09-20 11:23:56 -070010437 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010438 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010439 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
Adam Powell879fb6b2010-09-20 11:23:56 -070010440 }
10441
10442 public void show() {
Gilles Debunne21078e42011-08-02 10:22:35 -070010443 if (isShowing()) return;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010444
Gilles Debunnef682a772011-08-31 15:49:10 -070010445 getPositionListener().addSubscriber(this, true /* local position may change */);
Gilles Debunne21078e42011-08-02 10:22:35 -070010446
10447 // Make sure the offset is always considered new, even when focusing at same position
10448 mPreviousOffset = -1;
Gilles Debunnef682a772011-08-31 15:49:10 -070010449 positionAtCursorOffset(getCurrentCursorOffset(), false);
Gilles Debunne21078e42011-08-02 10:22:35 -070010450
Gilles Debunne646f8562011-07-27 17:44:03 -070010451 hideActionPopupWindow();
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010452 }
10453
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010454 protected void dismiss() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010455 mIsDragging = false;
10456 mContainer.dismiss();
Gilles Debunne646f8562011-07-27 17:44:03 -070010457 onDetached();
Adam Powell879fb6b2010-09-20 11:23:56 -070010458 }
10459
10460 public void hide() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010461 dismiss();
10462
Gilles Debunne21078e42011-08-02 10:22:35 -070010463 TextView.this.getPositionListener().removeSubscriber(this);
Gilles Debunne646f8562011-07-27 17:44:03 -070010464 }
10465
Gilles Debunnecb2516b2011-08-05 17:02:54 -070010466 void showActionPopupWindow(int delay) {
Gilles Debunne646f8562011-07-27 17:44:03 -070010467 if (mActionPopupWindow == null) {
10468 mActionPopupWindow = new ActionPopupWindow();
10469 }
10470 if (mActionPopupShower == null) {
10471 mActionPopupShower = new Runnable() {
10472 public void run() {
10473 mActionPopupWindow.show();
10474 }
10475 };
10476 } else {
10477 TextView.this.removeCallbacks(mActionPopupShower);
10478 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010479 TextView.this.postDelayed(mActionPopupShower, delay);
10480 }
10481
10482 protected void hideActionPopupWindow() {
10483 if (mActionPopupShower != null) {
10484 TextView.this.removeCallbacks(mActionPopupShower);
10485 }
10486 if (mActionPopupWindow != null) {
10487 mActionPopupWindow.hide();
10488 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010489 }
10490
10491 public boolean isShowing() {
10492 return mContainer.isShowing();
10493 }
10494
Gilles Debunne21078e42011-08-02 10:22:35 -070010495 private boolean isVisible() {
Adam Powellabcbb1a2010-10-04 21:12:19 -070010496 // Always show a dragging handle.
10497 if (mIsDragging) {
10498 return true;
10499 }
10500
Adam Powell965b9692010-10-21 18:44:32 -070010501 if (isInBatchEditMode()) {
10502 return false;
10503 }
10504
Gilles Debunne21078e42011-08-02 10:22:35 -070010505 return getPositionListener().isVisible(mPositionX + mHotspotX, mPositionY);
Adam Powell879fb6b2010-09-20 11:23:56 -070010506 }
10507
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010508 public abstract int getCurrentCursorOffset();
10509
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010510 protected void updateSelection(int offset) {
10511 updateDrawable();
10512 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010513
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010514 public abstract void updatePosition(float x, float y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010515
Gilles Debunnef682a772011-08-31 15:49:10 -070010516 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010517 // A HandleView relies on the layout, which may be nulled by external methods
Gilles Debunned4bb0b02011-05-27 15:58:55 -070010518 if (mLayout == null) {
10519 // Will update controllers' state, hiding them and stopping selection mode if needed
10520 prepareCursorControllers();
10521 return;
10522 }
10523
Gilles Debunnef682a772011-08-31 15:49:10 -070010524 if (offset != mPreviousOffset || parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010525 updateSelection(offset);
10526 addPositionToTouchUpFilter(offset);
10527 final int line = mLayout.getLineForOffset(offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010528
Gilles Debunne21078e42011-08-02 10:22:35 -070010529 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
10530 mPositionY = mLayout.getLineBottom(line);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010531
Gilles Debunne21078e42011-08-02 10:22:35 -070010532 // Take TextView's padding into account.
10533 mPositionX += viewportToContentHorizontalOffset();
10534 mPositionY += viewportToContentVerticalOffset();
10535
10536 mPreviousOffset = offset;
10537 mPositionHasChanged = true;
10538 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010539 }
10540
Gilles Debunnef682a772011-08-31 15:49:10 -070010541 public void updatePosition(int parentPositionX, int parentPositionY,
10542 boolean parentPositionChanged, boolean parentScrolled) {
10543 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
10544 if (parentPositionChanged || mPositionHasChanged) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010545 if (mIsDragging) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010546 // Update touchToWindow offset in case of parent scrolling while dragging
10547 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
10548 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
10549 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
10550 mLastParentX = parentPositionX;
10551 mLastParentY = parentPositionY;
Gilles Debunne2037b822011-04-22 13:07:33 -070010552 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010553
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010554 onHandleMoved();
10555 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010556
Gilles Debunne21078e42011-08-02 10:22:35 -070010557 if (isVisible()) {
10558 final int positionX = parentPositionX + mPositionX;
10559 final int positionY = parentPositionY + mPositionY;
10560 if (isShowing()) {
10561 mContainer.update(positionX, positionY, -1, -1);
10562 } else {
10563 mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
10564 positionX, positionY);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010565 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010566 } else {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010567 if (isShowing()) {
10568 dismiss();
10569 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010570 }
Gilles Debunne21078e42011-08-02 10:22:35 -070010571
10572 mPositionHasChanged = false;
Adam Powell879fb6b2010-09-20 11:23:56 -070010573 }
10574 }
10575
10576 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010577 protected void onDraw(Canvas c) {
Adam Powell879fb6b2010-09-20 11:23:56 -070010578 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010579 mDrawable.draw(c);
Adam Powell879fb6b2010-09-20 11:23:56 -070010580 }
10581
10582 @Override
10583 public boolean onTouchEvent(MotionEvent ev) {
10584 switch (ev.getActionMasked()) {
Gilles Debunne874d77c2011-01-25 15:29:13 -080010585 case MotionEvent.ACTION_DOWN: {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010586 startTouchUpFilter(getCurrentCursorOffset());
10587 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
10588 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010589
Gilles Debunne21078e42011-08-02 10:22:35 -070010590 final PositionListener positionListener = getPositionListener();
10591 mLastParentX = positionListener.getPositionX();
10592 mLastParentY = positionListener.getPositionY();
Gilles Debunne874d77c2011-01-25 15:29:13 -080010593 mIsDragging = true;
10594 break;
10595 }
10596
10597 case MotionEvent.ACTION_MOVE: {
10598 final float rawX = ev.getRawX();
10599 final float rawY = ev.getRawY();
Gilles Debunneddf00b82011-02-23 17:25:13 -080010600
10601 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
10602 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
10603 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
10604 float newVerticalOffset;
10605 if (previousVerticalOffset < mIdealVerticalOffset) {
10606 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
10607 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
10608 } else {
10609 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
10610 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
10611 }
10612 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
10613
Gilles Debunne874d77c2011-01-25 15:29:13 -080010614 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010615 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunne874d77c2011-01-25 15:29:13 -080010616
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010617 updatePosition(newPosX, newPosY);
Gilles Debunne874d77c2011-01-25 15:29:13 -080010618 break;
10619 }
10620
10621 case MotionEvent.ACTION_UP:
Gilles Debunne874d77c2011-01-25 15:29:13 -080010622 filterOnTouchUp();
10623 mIsDragging = false;
10624 break;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -080010625
Gilles Debunne874d77c2011-01-25 15:29:13 -080010626 case MotionEvent.ACTION_CANCEL:
10627 mIsDragging = false;
10628 break;
Adam Powell879fb6b2010-09-20 11:23:56 -070010629 }
10630 return true;
10631 }
10632
10633 public boolean isDragging() {
10634 return mIsDragging;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010635 }
Gilles Debunne64e54a62010-09-07 19:07:17 -070010636
Gilles Debunne2037b822011-04-22 13:07:33 -070010637 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010638 hideActionPopupWindow();
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010639 }
Gilles Debunne69340442011-03-31 13:37:51 -070010640
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010641 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010642 hideActionPopupWindow();
Gilles Debunne81f08082011-02-17 14:07:19 -080010643 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010644 }
10645
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010646 private class InsertionHandleView extends HandleView {
Gilles Debunne646f8562011-07-27 17:44:03 -070010647 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010648 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010649
Gilles Debunne646f8562011-07-27 17:44:03 -070010650 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010651 private float mDownPositionX, mDownPositionY;
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010652 private Runnable mHider;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010653
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010654 public InsertionHandleView(Drawable drawable) {
10655 super(drawable, drawable);
10656 }
10657
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010658 @Override
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010659 public void show() {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010660 super.show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010661
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010662 final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
10663 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010664 showActionPopupWindow(0);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010665 }
Gilles Debunne6435a562011-08-04 21:22:30 -070010666
10667 hideAfterDelay();
10668 }
10669
10670 public void showWithActionPopup() {
10671 show();
10672 showActionPopupWindow(0);
Gilles Debunne9948ad72010-11-24 14:00:46 -080010673 }
10674
Gilles Debunne646f8562011-07-27 17:44:03 -070010675 private void hideAfterDelay() {
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010676 removeHiderCallback();
10677 if (mHider == null) {
10678 mHider = new Runnable() {
10679 public void run() {
10680 hide();
10681 }
10682 };
10683 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010684 TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010685 }
10686
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010687 private void removeHiderCallback() {
10688 if (mHider != null) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010689 TextView.this.removeCallbacks(mHider);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010690 }
10691 }
10692
10693 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010694 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10695 return drawable.getIntrinsicWidth() / 2;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010696 }
10697
10698 @Override
10699 public boolean onTouchEvent(MotionEvent ev) {
10700 final boolean result = super.onTouchEvent(ev);
10701
10702 switch (ev.getActionMasked()) {
10703 case MotionEvent.ACTION_DOWN:
10704 mDownPositionX = ev.getRawX();
10705 mDownPositionY = ev.getRawY();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010706 break;
10707
10708 case MotionEvent.ACTION_UP:
Gilles Debunne040023a2011-08-02 18:23:31 -070010709 if (!offsetHasBeenChanged()) {
10710 final float deltaX = mDownPositionX - ev.getRawX();
10711 final float deltaY = mDownPositionY - ev.getRawY();
10712 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10713 if (distanceSquared < mSquaredTouchSlopDistance) {
10714 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
10715 // Tapping on the handle dismisses the displayed action popup
10716 mActionPopupWindow.hide();
10717 } else {
Gilles Debunne6435a562011-08-04 21:22:30 -070010718 showWithActionPopup();
Gilles Debunne040023a2011-08-02 18:23:31 -070010719 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010720 }
10721 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010722 hideAfterDelay();
Gilles Debunne2037b822011-04-22 13:07:33 -070010723 break;
10724
10725 case MotionEvent.ACTION_CANCEL:
Gilles Debunne646f8562011-07-27 17:44:03 -070010726 hideAfterDelay();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010727 break;
10728
10729 default:
10730 break;
10731 }
10732
10733 return result;
10734 }
10735
10736 @Override
10737 public int getCurrentCursorOffset() {
10738 return TextView.this.getSelectionStart();
10739 }
10740
10741 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010742 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010743 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010744 }
10745
10746 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010747 public void updatePosition(float x, float y) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010748 positionAtCursorOffset(getOffsetForPosition(x, y), false);
Gilles Debunne05336272010-07-09 20:13:45 -070010749 }
10750
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010751 @Override
Gilles Debunne2037b822011-04-22 13:07:33 -070010752 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010753 super.onHandleMoved();
Gilles Debunne2037b822011-04-22 13:07:33 -070010754 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010755 }
10756
10757 @Override
10758 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010759 super.onDetached();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010760 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010761 }
10762 }
10763
10764 private class SelectionStartHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010765
10766 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10767 super(drawableLtr, drawableRtl);
10768 }
10769
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010770 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010771 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10772 if (isRtlRun) {
10773 return drawable.getIntrinsicWidth() / 4;
10774 } else {
10775 return (drawable.getIntrinsicWidth() * 3) / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010776 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010777 }
10778
10779 @Override
10780 public int getCurrentCursorOffset() {
10781 return TextView.this.getSelectionStart();
10782 }
10783
10784 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010785 public void updateSelection(int offset) {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010786 super.updateSelection(offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010787 Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010788 }
10789
10790 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010791 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010792 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010793
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010794 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010795 final int selectionEnd = getSelectionEnd();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010796 if (offset >= selectionEnd) offset = selectionEnd - 1;
10797
Gilles Debunnef682a772011-08-31 15:49:10 -070010798 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010799 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010800
10801 public ActionPopupWindow getActionPopupWindow() {
10802 return mActionPopupWindow;
10803 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010804 }
10805
10806 private class SelectionEndHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010807
10808 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10809 super(drawableLtr, drawableRtl);
10810 }
10811
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010812 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010813 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10814 if (isRtlRun) {
10815 return (drawable.getIntrinsicWidth() * 3) / 4;
10816 } else {
10817 return drawable.getIntrinsicWidth() / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010818 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010819 }
10820
10821 @Override
10822 public int getCurrentCursorOffset() {
10823 return TextView.this.getSelectionEnd();
10824 }
10825
10826 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010827 public void updateSelection(int offset) {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010828 super.updateSelection(offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010829 Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010830 }
10831
10832 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010833 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010834 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010835
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010836 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010837 final int selectionStart = getSelectionStart();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010838 if (offset <= selectionStart) offset = selectionStart + 1;
10839
Gilles Debunnef682a772011-08-31 15:49:10 -070010840 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010841 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010842
10843 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
10844 mActionPopupWindow = actionPopupWindow;
10845 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010846 }
10847
10848 /**
10849 * A CursorController instance can be used to control a cursor in the text.
10850 * It is not used outside of {@link TextView}.
10851 * @hide
10852 */
10853 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
10854 /**
10855 * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
10856 * See also {@link #hide()}.
10857 */
10858 public void show();
10859
10860 /**
10861 * Hide the cursor controller from screen.
10862 * See also {@link #show()}.
10863 */
10864 public void hide();
10865
10866 /**
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010867 * Called when the view is detached from window. Perform house keeping task, such as
10868 * stopping Runnable thread that would otherwise keep a reference on the context, thus
10869 * preventing the activity from being recycled.
10870 */
10871 public void onDetached();
10872 }
10873
10874 private class InsertionPointCursorController implements CursorController {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010875 private InsertionHandleView mHandle;
10876
10877 public void show() {
Gilles Debunne6435a562011-08-04 21:22:30 -070010878 getHandle().show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010879 }
10880
Gilles Debunne6435a562011-08-04 21:22:30 -070010881 public void showWithActionPopup() {
10882 getHandle().showWithActionPopup();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010883 }
10884
10885 public void hide() {
10886 if (mHandle != null) {
10887 mHandle.hide();
10888 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010889 }
10890
Adam Powell624380a2010-10-02 18:12:02 -070010891 public void onTouchModeChanged(boolean isInTouchMode) {
10892 if (!isInTouchMode) {
10893 hide();
10894 }
10895 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010896
Gilles Debunne646f8562011-07-27 17:44:03 -070010897 private InsertionHandleView getHandle() {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010898 if (mSelectHandleCenter == null) {
10899 mSelectHandleCenter = mContext.getResources().getDrawable(
10900 mTextSelectHandleRes);
10901 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010902 if (mHandle == null) {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010903 mHandle = new InsertionHandleView(mSelectHandleCenter);
Gilles Debunnec4440f02010-11-24 14:40:48 -080010904 }
10905 return mHandle;
10906 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010907
10908 @Override
10909 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070010910 final ViewTreeObserver observer = getViewTreeObserver();
10911 observer.removeOnTouchModeChangeListener(this);
10912
Gilles Debunne6a855082011-03-11 16:51:24 -080010913 if (mHandle != null) mHandle.onDetached();
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010914 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010915 }
10916
Jeff Brown01ce2e92010-09-26 22:20:12 -070010917 private class SelectionModifierCursorController implements CursorController {
Gilles Debunne6435a562011-08-04 21:22:30 -070010918 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010919 // The cursor controller handles, lazily created when shown.
10920 private SelectionStartHandleView mStartHandle;
10921 private SelectionEndHandleView mEndHandle;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010922 // The offsets of that last touch down event. Remembered to start selection there.
10923 private int mMinTouchOffset, mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070010924
Gilles Debunne5347c582010-10-27 14:22:35 -070010925 // Double tap detection
10926 private long mPreviousTapUpTime = 0;
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010927 private float mPreviousTapPositionX, mPreviousTapPositionY;
Gilles Debunne5347c582010-10-27 14:22:35 -070010928
Gilles Debunne05336272010-07-09 20:13:45 -070010929 SelectionModifierCursorController() {
Gilles Debunne380b6042010-10-08 16:12:11 -070010930 resetTouchOffsets();
Gilles Debunne05336272010-07-09 20:13:45 -070010931 }
10932
10933 public void show() {
Adam Powell965b9692010-10-21 18:44:32 -070010934 if (isInBatchEditMode()) {
10935 return;
10936 }
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010937 initDrawables();
10938 initHandles();
10939 hideInsertionPointCursorController();
10940 }
Adam Powell965b9692010-10-21 18:44:32 -070010941
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010942 private void initDrawables() {
10943 if (mSelectHandleLeft == null) {
10944 mSelectHandleLeft = mContext.getResources().getDrawable(
10945 mTextSelectHandleLeftRes);
10946 }
10947 if (mSelectHandleRight == null) {
10948 mSelectHandleRight = mContext.getResources().getDrawable(
10949 mTextSelectHandleRightRes);
10950 }
10951 }
10952
10953 private void initHandles() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080010954 // Lazy object creation has to be done before updatePosition() is called.
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010955 if (mStartHandle == null) {
10956 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
10957 }
10958 if (mEndHandle == null) {
10959 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
10960 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010961
Adam Powell879fb6b2010-09-20 11:23:56 -070010962 mStartHandle.show();
10963 mEndHandle.show();
Gilles Debunnec4440f02010-11-24 14:40:48 -080010964
Gilles Debunne646f8562011-07-27 17:44:03 -070010965 // Make sure both left and right handles share the same ActionPopupWindow (so that
10966 // moving any of the handles hides the action popup).
Gilles Debunnecb2516b2011-08-05 17:02:54 -070010967 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
Gilles Debunne646f8562011-07-27 17:44:03 -070010968 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
10969
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010970 hideInsertionPointCursorController();
Gilles Debunne05336272010-07-09 20:13:45 -070010971 }
10972
10973 public void hide() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080010974 if (mStartHandle != null) mStartHandle.hide();
10975 if (mEndHandle != null) mEndHandle.hide();
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010976 }
10977
Gilles Debunnebb588da2011-07-11 18:26:19 -070010978 public void onTouchEvent(MotionEvent event) {
Gilles Debunne528c64882010-10-08 11:56:13 -070010979 // This is done even when the View does not have focus, so that long presses can start
10980 // selection and tap can move cursor from this tap position.
Gilles Debunnebb588da2011-07-11 18:26:19 -070010981 switch (event.getActionMasked()) {
10982 case MotionEvent.ACTION_DOWN:
10983 final float x = event.getX();
10984 final float y = event.getY();
Gilles Debunne05336272010-07-09 20:13:45 -070010985
Gilles Debunnebb588da2011-07-11 18:26:19 -070010986 // Remember finger down position, to be able to start selection from there
10987 mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
Gilles Debunne05336272010-07-09 20:13:45 -070010988
Gilles Debunnebb588da2011-07-11 18:26:19 -070010989 // Double tap detection
10990 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
10991 if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
10992 isPositionOnText(x, y)) {
10993 final float deltaX = x - mPreviousTapPositionX;
10994 final float deltaY = y - mPreviousTapPositionY;
10995 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10996 if (distanceSquared < mSquaredTouchSlopDistance) {
Gilles Debunne646f8562011-07-27 17:44:03 -070010997 startSelectionActionMode();
Gilles Debunnebb588da2011-07-11 18:26:19 -070010998 mDiscardNextActionUp = true;
Gilles Debunne5347c582010-10-27 14:22:35 -070010999 }
Gilles Debunnebb588da2011-07-11 18:26:19 -070011000 }
Gilles Debunne5347c582010-10-27 14:22:35 -070011001
Gilles Debunnebb588da2011-07-11 18:26:19 -070011002 mPreviousTapPositionX = x;
11003 mPreviousTapPositionY = y;
Gilles Debunnebb588da2011-07-11 18:26:19 -070011004 break;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011005
Gilles Debunnebb588da2011-07-11 18:26:19 -070011006 case MotionEvent.ACTION_POINTER_DOWN:
11007 case MotionEvent.ACTION_POINTER_UP:
11008 // Handle multi-point gestures. Keep min and max offset positions.
11009 // Only activated for devices that correctly handle multi-touch.
11010 if (mContext.getPackageManager().hasSystemFeature(
11011 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
11012 updateMinAndMaxOffsets(event);
11013 }
11014 break;
Gilles Debunne5347c582010-10-27 14:22:35 -070011015
Gilles Debunnebb588da2011-07-11 18:26:19 -070011016 case MotionEvent.ACTION_UP:
11017 mPreviousTapUpTime = SystemClock.uptimeMillis();
11018 break;
Gilles Debunne05336272010-07-09 20:13:45 -070011019 }
11020 }
11021
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011022 /**
11023 * @param event
11024 */
11025 private void updateMinAndMaxOffsets(MotionEvent event) {
11026 int pointerCount = event.getPointerCount();
11027 for (int index = 0; index < pointerCount; index++) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011028 int offset = getOffsetForPosition(event.getX(index), event.getY(index));
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011029 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
11030 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
11031 }
11032 }
11033
11034 public int getMinTouchOffset() {
11035 return mMinTouchOffset;
11036 }
11037
11038 public int getMaxTouchOffset() {
11039 return mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070011040 }
11041
Gilles Debunne380b6042010-10-08 16:12:11 -070011042 public void resetTouchOffsets() {
11043 mMinTouchOffset = mMaxTouchOffset = -1;
11044 }
11045
Gilles Debunne05336272010-07-09 20:13:45 -070011046 /**
11047 * @return true iff this controller is currently used to move the selection start.
11048 */
11049 public boolean isSelectionStartDragged() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011050 return mStartHandle != null && mStartHandle.isDragging();
Gilles Debunne05336272010-07-09 20:13:45 -070011051 }
Adam Powell624380a2010-10-02 18:12:02 -070011052
11053 public void onTouchModeChanged(boolean isInTouchMode) {
11054 if (!isInTouchMode) {
11055 hide();
11056 }
11057 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080011058
11059 @Override
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011060 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070011061 final ViewTreeObserver observer = getViewTreeObserver();
11062 observer.removeOnTouchModeChangeListener(this);
11063
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011064 if (mStartHandle != null) mStartHandle.onDetached();
11065 if (mEndHandle != null) mEndHandle.onDetached();
11066 }
Gilles Debunne05336272010-07-09 20:13:45 -070011067 }
11068
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011069 private void hideInsertionPointCursorController() {
Gilles Debunnee587d832010-11-23 20:20:11 -080011070 // No need to create the controller to hide it.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011071 if (mInsertionPointCursorController != null) {
11072 mInsertionPointCursorController.hide();
11073 }
11074 }
11075
Gilles Debunned94f8c52011-01-10 11:29:15 -080011076 /**
11077 * Hides the insertion controller and stops text selection mode, hiding the selection controller
11078 */
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011079 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +010011080 hideCursorControllers();
11081 hideSpanControllers();
11082 }
Luca Zanoline6d36822011-08-30 18:04:34 +010011083
Luca Zanolin1564fc72011-09-07 00:01:28 +010011084 private void hideSpanControllers() {
Luca Zanoline6d36822011-08-30 18:04:34 +010011085 if (mChangeWatcher != null) {
11086 mChangeWatcher.hideControllers();
11087 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011088 }
11089
Luca Zanolin1564fc72011-09-07 00:01:28 +010011090 private void hideCursorControllers() {
11091 hideInsertionPointCursorController();
11092 stopSelectionActionMode();
11093 }
11094
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011095 /**
Gilles Debunne9511b412011-05-23 18:33:48 -070011096 * Get the character offset closest to the specified absolute position. A typical use case is to
11097 * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011098 *
11099 * @param x The horizontal absolute position of a point on screen
11100 * @param y The vertical absolute position of a point on screen
11101 * @return the character offset for the character whose position is closest to the specified
11102 * position. Returns -1 if there is no layout.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011103 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011104 public int getOffsetForPosition(float x, float y) {
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011105 if (getLayout() == null) return -1;
Gilles Debunne9948ad72010-11-24 14:00:46 -080011106 final int line = getLineAtCoordinate(y);
11107 final int offset = getOffsetAtCoordinate(line, x);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011108 return offset;
11109 }
11110
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011111 private float convertToLocalHorizontalCoordinate(float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011112 x -= getTotalPaddingLeft();
11113 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011114 x = Math.max(0.0f, x);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011115 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11116 x += getScrollX();
11117 return x;
11118 }
11119
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011120 private int getLineAtCoordinate(float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011121 y -= getTotalPaddingTop();
11122 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011123 y = Math.max(0.0f, y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011124 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11125 y += getScrollY();
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011126 return getLayout().getLineForVertical((int) y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011127 }
11128
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011129 private int getOffsetAtCoordinate(int line, float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011130 x = convertToLocalHorizontalCoordinate(x);
11131 return getLayout().getOffsetForHorizontal(line, x);
11132 }
11133
11134 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
11135 * in the view. Returns false when the position is in the empty space of left/right of text.
11136 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011137 private boolean isPositionOnText(float x, float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011138 if (getLayout() == null) return false;
11139
11140 final int line = getLineAtCoordinate(y);
11141 x = convertToLocalHorizontalCoordinate(x);
11142
11143 if (x < getLayout().getLineLeft(line)) return false;
11144 if (x > getLayout().getLineRight(line)) return false;
11145 return true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011146 }
11147
Gilles Debunnef170a342010-11-11 11:08:59 -080011148 @Override
11149 public boolean onDragEvent(DragEvent event) {
11150 switch (event.getAction()) {
11151 case DragEvent.ACTION_DRAG_STARTED:
Gilles Debunnee587d832010-11-23 20:20:11 -080011152 return hasInsertionController();
Gilles Debunnef170a342010-11-11 11:08:59 -080011153
11154 case DragEvent.ACTION_DRAG_ENTERED:
11155 TextView.this.requestFocus();
11156 return true;
11157
Gilles Debunne2226a192010-11-30 18:50:51 -080011158 case DragEvent.ACTION_DRAG_LOCATION:
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011159 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunnef170a342010-11-11 11:08:59 -080011160 Selection.setSelection((Spannable)mText, offset);
11161 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011162
Gilles Debunne2226a192010-11-30 18:50:51 -080011163 case DragEvent.ACTION_DROP:
11164 onDrop(event);
Gilles Debunnef170a342010-11-11 11:08:59 -080011165 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011166
Gilles Debunnef170a342010-11-11 11:08:59 -080011167 case DragEvent.ACTION_DRAG_ENDED:
Gilles Debunne4ae0f292010-11-29 14:56:39 -080011168 case DragEvent.ACTION_DRAG_EXITED:
Gilles Debunnef170a342010-11-11 11:08:59 -080011169 default:
11170 return true;
11171 }
11172 }
11173
Gilles Debunne2226a192010-11-30 18:50:51 -080011174 private void onDrop(DragEvent event) {
11175 StringBuilder content = new StringBuilder("");
11176 ClipData clipData = event.getClipData();
11177 final int itemCount = clipData.getItemCount();
11178 for (int i=0; i < itemCount; i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080011179 Item item = clipData.getItemAt(i);
Gilles Debunne2226a192010-11-30 18:50:51 -080011180 content.append(item.coerceToText(TextView.this.mContext));
11181 }
11182
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011183 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunne2226a192010-11-30 18:50:51 -080011184
Gilles Debunneaaa84792010-12-03 11:10:14 -080011185 Object localState = event.getLocalState();
11186 DragLocalState dragLocalState = null;
11187 if (localState instanceof DragLocalState) {
11188 dragLocalState = (DragLocalState) localState;
11189 }
11190 boolean dragDropIntoItself = dragLocalState != null &&
11191 dragLocalState.sourceTextView == this;
11192
11193 if (dragDropIntoItself) {
11194 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
Gilles Debunne2226a192010-11-30 18:50:51 -080011195 // A drop inside the original selection discards the drop.
11196 return;
11197 }
11198 }
11199
11200 final int originalLength = mText.length();
11201 long minMax = prepareSpacesAroundPaste(offset, offset, content);
11202 int min = extractRangeStartFromLong(minMax);
11203 int max = extractRangeEndFromLong(minMax);
11204
11205 Selection.setSelection((Spannable) mText, max);
11206 ((Editable) mText).replace(min, max, content);
11207
Gilles Debunneaaa84792010-12-03 11:10:14 -080011208 if (dragDropIntoItself) {
11209 int dragSourceStart = dragLocalState.start;
11210 int dragSourceEnd = dragLocalState.end;
Gilles Debunne2226a192010-11-30 18:50:51 -080011211 if (max <= dragSourceStart) {
11212 // Inserting text before selection has shifted positions
11213 final int shift = mText.length() - originalLength;
11214 dragSourceStart += shift;
11215 dragSourceEnd += shift;
11216 }
11217
11218 // Delete original selection
11219 ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
11220
11221 // Make sure we do not leave two adjacent spaces.
11222 if ((dragSourceStart == 0 ||
11223 Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
11224 (dragSourceStart == mText.length() ||
11225 Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
11226 final int pos = dragSourceStart == mText.length() ?
11227 dragSourceStart - 1 : dragSourceStart;
11228 ((Editable) mText).delete(pos, pos + 1);
11229 }
11230 }
11231 }
11232
Adam Powell965b9692010-10-21 18:44:32 -070011233 /**
11234 * @return True if this view supports insertion handles.
11235 */
11236 boolean hasInsertionController() {
11237 return mInsertionControllerEnabled;
11238 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011239
Adam Powell965b9692010-10-21 18:44:32 -070011240 /**
11241 * @return True if this view supports selection handles.
11242 */
11243 boolean hasSelectionController() {
11244 return mSelectionControllerEnabled;
11245 }
11246
Gilles Debunne9948ad72010-11-24 14:00:46 -080011247 InsertionPointCursorController getInsertionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011248 if (!mInsertionControllerEnabled) {
11249 return null;
11250 }
11251
11252 if (mInsertionPointCursorController == null) {
11253 mInsertionPointCursorController = new InsertionPointCursorController();
11254
11255 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011256 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011257 }
11258
11259 return mInsertionPointCursorController;
11260 }
11261
Gilles Debunnee587d832010-11-23 20:20:11 -080011262 SelectionModifierCursorController getSelectionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011263 if (!mSelectionControllerEnabled) {
11264 return null;
11265 }
11266
11267 if (mSelectionModifierCursorController == null) {
11268 mSelectionModifierCursorController = new SelectionModifierCursorController();
11269
11270 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011271 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011272 }
11273
11274 return mSelectionModifierCursorController;
11275 }
11276
11277 boolean isInBatchEditMode() {
11278 final InputMethodState ims = mInputMethodState;
11279 if (ims != null) {
11280 return ims.mBatchEditNesting > 0;
11281 }
11282 return mInBatchEditControllers;
11283 }
11284
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011285 @Override
11286 protected void resolveTextDirection() {
Fabrice Di Meglio7810b5f2011-08-24 18:26:14 -070011287 if (hasPasswordTransformationMethod()) {
11288 mTextDir = TextDirectionHeuristics.LOCALE;
11289 return;
11290 }
11291
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011292 // Always need to resolve layout direction first
11293 final boolean defaultIsRtl = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL);
11294
11295 // Then resolve text direction on the parent
Doug Feltcb3791202011-07-07 11:57:48 -070011296 super.resolveTextDirection();
11297
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011298 // Now, we can select the heuristic
Doug Feltcb3791202011-07-07 11:57:48 -070011299 int textDir = getResolvedTextDirection();
11300 switch (textDir) {
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011301 default:
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011302 case TEXT_DIRECTION_FIRST_STRONG:
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011303 mTextDir = (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11304 TextDirectionHeuristics.FIRSTSTRONG_LTR);
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011305 break;
11306 case TEXT_DIRECTION_ANY_RTL:
Fabrice Di Meglioa6461452011-08-19 15:42:04 -070011307 mTextDir = TextDirectionHeuristics.ANYRTL_LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011308 break;
11309 case TEXT_DIRECTION_LTR:
Doug Feltcb3791202011-07-07 11:57:48 -070011310 mTextDir = TextDirectionHeuristics.LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011311 break;
11312 case TEXT_DIRECTION_RTL:
Doug Feltcb3791202011-07-07 11:57:48 -070011313 mTextDir = TextDirectionHeuristics.RTL;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011314 break;
11315 }
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011316 }
11317
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011318 /**
11319 * Subclasses will need to override this method to implement their own way of resolving
11320 * drawables depending on the layout direction.
11321 *
11322 * A call to the super method will be required from the subclasses implementation.
11323 *
11324 */
11325 protected void resolveDrawables() {
11326 // No need to resolve twice
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011327 if (mResolvedDrawables) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011328 return;
11329 }
11330 // No drawable to resolve
11331 if (mDrawables == null) {
11332 return;
11333 }
11334 // No relative drawable to resolve
11335 if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011336 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011337 return;
11338 }
11339
11340 Drawables dr = mDrawables;
11341 switch(getResolvedLayoutDirection()) {
11342 case LAYOUT_DIRECTION_RTL:
11343 if (dr.mDrawableStart != null) {
11344 dr.mDrawableRight = dr.mDrawableStart;
11345
11346 dr.mDrawableSizeRight = dr.mDrawableSizeStart;
11347 dr.mDrawableHeightRight = dr.mDrawableHeightStart;
11348 }
11349 if (dr.mDrawableEnd != null) {
11350 dr.mDrawableLeft = dr.mDrawableEnd;
11351
11352 dr.mDrawableSizeLeft = dr.mDrawableSizeEnd;
11353 dr.mDrawableHeightLeft = dr.mDrawableHeightEnd;
11354 }
11355 break;
11356
11357 case LAYOUT_DIRECTION_LTR:
11358 default:
11359 if (dr.mDrawableStart != null) {
11360 dr.mDrawableLeft = dr.mDrawableStart;
11361
11362 dr.mDrawableSizeLeft = dr.mDrawableSizeStart;
11363 dr.mDrawableHeightLeft = dr.mDrawableHeightStart;
11364 }
11365 if (dr.mDrawableEnd != null) {
11366 dr.mDrawableRight = dr.mDrawableEnd;
11367
11368 dr.mDrawableSizeRight = dr.mDrawableSizeEnd;
11369 dr.mDrawableHeightRight = dr.mDrawableHeightEnd;
11370 }
11371 break;
11372 }
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011373 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011374 }
11375
11376 protected void resetResolvedDrawables() {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011377 mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011378 }
11379
satoka67a3cf2011-09-07 17:14:03 +090011380 /**
11381 * @hide
11382 */
11383 protected void viewClicked(InputMethodManager imm) {
11384 if (imm != null) {
11385 imm.viewClicked(this);
11386 }
11387 }
11388
Romain Guy6d956622010-11-29 11:38:05 -080011389 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011390 private CharSequence mText;
11391 private CharSequence mTransformed;
11392 private BufferType mBufferType = BufferType.NORMAL;
11393
11394 private int mInputType = EditorInfo.TYPE_NULL;
11395 private CharSequence mHint;
11396 private Layout mHintLayout;
11397
11398 private KeyListener mInput;
11399
11400 private MovementMethod mMovement;
11401 private TransformationMethod mTransformation;
Adam Powell7f8f79a2011-07-07 18:35:54 -070011402 private boolean mAllowTransformationLengthChange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011403 private ChangeWatcher mChangeWatcher;
11404
11405 private ArrayList<TextWatcher> mListeners = null;
11406
11407 // display attributes
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011408 private final TextPaint mTextPaint;
Romain Guy939151f2009-04-08 14:22:40 -070011409 private boolean mUserSetTextScaleX;
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011410 private final Paint mHighlightPaint;
Gilles Debunne6fbe5ca2011-09-06 15:24:45 -070011411 private int mHighlightColor = 0x6633B5E5;
Leon Scroggins1ca56262010-11-18 14:03:03 -050011412 /**
11413 * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
11414 * this field being protected. Will be restored as private when lineHeight
11415 * feature request 3215097 is implemented
11416 * @hide
11417 */
11418 protected Layout mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011419
11420 private long mShowCursor;
11421 private Blink mBlink;
11422 private boolean mCursorVisible = true;
11423
Adam Powell965b9692010-10-21 18:44:32 -070011424 // Cursor Controllers.
Gilles Debunne9948ad72010-11-24 14:00:46 -080011425 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunnee587d832010-11-23 20:20:11 -080011426 private SelectionModifierCursorController mSelectionModifierCursorController;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011427 private ActionMode mSelectionActionMode;
Adam Powell965b9692010-10-21 18:44:32 -070011428 private boolean mInsertionControllerEnabled;
11429 private boolean mSelectionControllerEnabled;
11430 private boolean mInBatchEditControllers;
11431
Gilles Debunnecf1e9252010-10-07 20:46:03 -070011432 // These are needed to desambiguate a long click. If the long click comes from ones of these, we
11433 // select from the current cursor position. Otherwise, select from long pressed position.
11434 private boolean mDPadCenterIsDown = false;
11435 private boolean mEnterKeyIsDown = false;
Gilles Debunne528c64882010-10-08 11:56:13 -070011436 private boolean mContextMenuTriggeredByKey = false;
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070011437
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011438 private boolean mSelectAllOnFocus = false;
11439
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -070011440 private int mGravity = Gravity.TOP | Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011441 private boolean mHorizontallyScrolling;
11442
11443 private int mAutoLinkMask;
11444 private boolean mLinksClickable = true;
11445
Gilles Debunne6435a562011-08-04 21:22:30 -070011446 private float mSpacingMult = 1.0f;
11447 private float mSpacingAdd = 0.0f;
Gilles Debunne86b9c782010-11-11 10:43:48 -080011448 private boolean mTextIsSelectable = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011449
11450 private static final int LINES = 1;
11451 private static final int EMS = LINES;
11452 private static final int PIXELS = 2;
11453
11454 private int mMaximum = Integer.MAX_VALUE;
11455 private int mMaxMode = LINES;
11456 private int mMinimum = 0;
11457 private int mMinMode = LINES;
11458
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -070011459 private int mOldMaximum = mMaximum;
11460 private int mOldMaxMode = mMaxMode;
11461
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011462 private int mMaxWidth = Integer.MAX_VALUE;
11463 private int mMaxWidthMode = PIXELS;
11464 private int mMinWidth = 0;
11465 private int mMinWidthMode = PIXELS;
11466
11467 private boolean mSingleLine;
11468 private int mDesiredHeightAtMeasure = -1;
11469 private boolean mIncludePad = true;
11470
11471 // tmp primitives, so we don't alloc them on each draw
11472 private Path mHighlightPath;
11473 private boolean mHighlightPathBogus = true;
11474 private static final RectF sTempRect = new RectF();
11475
11476 // XXX should be much larger
11477 private static final int VERY_WIDE = 16384;
11478
11479 private static final int BLINK = 500;
11480
11481 private static final int ANIMATED_SCROLL_GAP = 250;
11482 private long mLastScroll;
11483 private Scroller mScroller = null;
11484
11485 private BoringLayout.Metrics mBoring;
11486 private BoringLayout.Metrics mHintBoring;
11487
11488 private BoringLayout mSavedLayout, mSavedHintLayout;
11489
Doug Feltcb3791202011-07-07 11:57:48 -070011490 private TextDirectionHeuristic mTextDir = null;
11491
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011492 private static final InputFilter[] NO_FILTERS = new InputFilter[0];
11493 private InputFilter[] mFilters = NO_FILTERS;
11494 private static final Spanned EMPTY_SPANNED = new SpannedString("");
Christopher Tate36d4c3f2011-01-07 13:34:24 -080011495 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080011496 // System wide time for last cut or copy action.
11497 private static long sLastCutOrCopyTime;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080011498 // Used to highlight a word when it is corrected by the IME
11499 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunneda0a3f02010-12-22 10:07:15 -080011500 // New state used to change background based on whether this TextView is multiline.
11501 private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011502}