blob: 81fc069acaf3e0ccd9b70afd21ad5ac0606033ee [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
Gilles Debunne78996c92010-10-12 16:01:47 -070019import android.R;
Dianne Hackborn1040dc42010-08-26 22:11:06 -070020import android.content.ClipData;
Gilles Debunnef170a342010-11-11 11:08:59 -080021import android.content.ClipData.Item;
Gilles Debunne64e54a62010-09-07 19:07:17 -070022import android.content.ClipboardManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.content.Context;
Gilles Debunnee90bed12011-08-30 14:28:27 -070024import android.content.Intent;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070025import android.content.pm.PackageManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.content.res.ColorStateList;
Christopher Tate1373a8e2011-11-10 19:59:13 -080027import android.content.res.CompatibilityInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.content.res.XmlResourceParser;
31import android.graphics.Canvas;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080032import android.graphics.Color;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.graphics.Paint;
34import android.graphics.Path;
35import android.graphics.Rect;
36import android.graphics.RectF;
37import android.graphics.Typeface;
38import android.graphics.drawable.Drawable;
Gilles Debunne64e54a62010-09-07 19:07:17 -070039import android.inputmethodservice.ExtractEditText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import android.os.Bundle;
41import android.os.Handler;
svetoslavganov75986cf2009-05-14 22:28:01 -070042import android.os.Message;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043import android.os.Parcel;
44import android.os.Parcelable;
45import android.os.SystemClock;
Gilles Debunnee90bed12011-08-30 14:28:27 -070046import android.provider.Settings;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.text.BoringLayout;
48import android.text.DynamicLayout;
49import android.text.Editable;
50import android.text.GetChars;
51import android.text.GraphicsOperations;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052import android.text.InputFilter;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -070053import android.text.InputType;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054import android.text.Layout;
55import android.text.ParcelableSpan;
56import android.text.Selection;
57import android.text.SpanWatcher;
58import android.text.Spannable;
svetoslavganov75986cf2009-05-14 22:28:01 -070059import android.text.SpannableString;
Gilles Debunne214a8622011-04-26 15:44:37 -070060import android.text.SpannableStringBuilder;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import android.text.Spanned;
62import android.text.SpannedString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063import android.text.StaticLayout;
Doug Feltcb3791202011-07-07 11:57:48 -070064import android.text.TextDirectionHeuristic;
65import android.text.TextDirectionHeuristics;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066import android.text.TextPaint;
67import android.text.TextUtils;
Adam Powell282e3772011-08-30 16:51:11 -070068import android.text.TextUtils.TruncateAt;
Gilles Debunne0eea6682011-08-29 13:30:31 -070069import android.text.TextWatcher;
Adam Powell7f8f79a2011-07-07 18:35:54 -070070import android.text.method.AllCapsTransformationMethod;
Gilles Debunne86b9c782010-11-11 10:43:48 -080071import android.text.method.ArrowKeyMovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072import android.text.method.DateKeyListener;
73import android.text.method.DateTimeKeyListener;
74import android.text.method.DialerKeyListener;
75import android.text.method.DigitsKeyListener;
76import android.text.method.KeyListener;
77import android.text.method.LinkMovementMethod;
78import android.text.method.MetaKeyKeyListener;
79import android.text.method.MovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080080import android.text.method.PasswordTransformationMethod;
81import android.text.method.SingleLineTransformationMethod;
82import android.text.method.TextKeyListener;
svetoslavganov75986cf2009-05-14 22:28:01 -070083import android.text.method.TimeKeyListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080084import android.text.method.TransformationMethod;
Adam Powell7f8f79a2011-07-07 18:35:54 -070085import android.text.method.TransformationMethod2;
Gilles Debunne214a8622011-04-26 15:44:37 -070086import android.text.method.WordIterator;
Gilles Debunnef3895ed2010-12-21 12:53:58 -080087import android.text.style.ClickableSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +010088import android.text.style.EasyEditSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080089import android.text.style.ParagraphStyle;
Gilles Debunne6435a562011-08-04 21:22:30 -070090import android.text.style.SpellCheckSpan;
Gilles Debunne28294cc2011-08-24 12:02:05 -070091import android.text.style.SuggestionRangeSpan;
Gilles Debunne2037b822011-04-22 13:07:33 -070092import android.text.style.SuggestionSpan;
Gilles Debunne214a8622011-04-26 15:44:37 -070093import android.text.style.TextAppearanceSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080094import android.text.style.URLSpan;
95import android.text.style.UpdateAppearance;
96import android.text.util.Linkify;
97import android.util.AttributeSet;
Gilles Debunne69340442011-03-31 13:37:51 -070098import android.util.DisplayMetrics;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080099import android.util.FloatMath;
svetoslavganov75986cf2009-05-14 22:28:01 -0700100import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101import android.util.TypedValue;
Gilles Debunne27113f82010-08-23 12:09:14 -0700102import android.view.ActionMode;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800103import android.view.ActionMode.Callback;
Gilles Debunnef170a342010-11-11 11:08:59 -0800104import android.view.DragEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800105import android.view.Gravity;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700106import android.view.HapticFeedbackConstants;
Jeff Brown6b53e8d2010-11-10 16:03:06 -0800107import android.view.KeyCharacterMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108import android.view.KeyEvent;
109import android.view.LayoutInflater;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700110import android.view.Menu;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111import android.view.MenuItem;
112import android.view.MotionEvent;
113import android.view.View;
Gilles Debunne65f60412010-10-15 16:18:35 -0700114import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115import android.view.ViewDebug;
Adam Powell8c8293b2010-10-12 14:45:12 -0700116import android.view.ViewGroup;
Gilles Debunne27113f82010-08-23 12:09:14 -0700117import android.view.ViewGroup.LayoutParams;
Adam Powellabcbb1a2010-10-04 21:12:19 -0700118import android.view.ViewParent;
Gilles Debunne3784a7f2011-07-15 13:49:38 -0700119import android.view.ViewRootImpl;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800120import android.view.ViewTreeObserver;
Gilles Debunnecbfbb522010-10-07 16:57:31 -0700121import android.view.WindowManager;
svetoslavganov75986cf2009-05-14 22:28:01 -0700122import android.view.accessibility.AccessibilityEvent;
123import android.view.accessibility.AccessibilityManager;
Svetoslav Ganov8643aa02011-04-20 12:12:33 -0700124import android.view.accessibility.AccessibilityNodeInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125import android.view.animation.AnimationUtils;
126import android.view.inputmethod.BaseInputConnection;
127import android.view.inputmethod.CompletionInfo;
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -0800128import android.view.inputmethod.CorrectionInfo;
svetoslavganov75986cf2009-05-14 22:28:01 -0700129import android.view.inputmethod.EditorInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130import android.view.inputmethod.ExtractedText;
131import android.view.inputmethod.ExtractedTextRequest;
132import android.view.inputmethod.InputConnection;
133import android.view.inputmethod.InputMethodManager;
satok05f24702011-11-02 19:29:35 +0900134import android.view.textservice.SpellCheckerSubtype;
135import android.view.textservice.TextServicesManager;
Gilles Debunne0eea6682011-08-29 13:30:31 -0700136import android.widget.AdapterView.OnItemClickListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137import android.widget.RemoteViews.RemoteView;
138
Gilles Debunne22378292011-08-12 10:38:52 -0700139import com.android.internal.util.FastMath;
140import com.android.internal.widget.EditableInputConnection;
141
142import org.xmlpull.v1.XmlPullParserException;
143
Gilles Debunne27113f82010-08-23 12:09:14 -0700144import java.io.IOException;
145import java.lang.ref.WeakReference;
Gilles Debunne214a8622011-04-26 15:44:37 -0700146import java.text.BreakIterator;
Gilles Debunne27113f82010-08-23 12:09:14 -0700147import java.util.ArrayList;
Luca Zanoline3f89c02011-08-01 09:55:17 +0100148import java.util.Arrays;
149import java.util.Comparator;
150import java.util.HashMap;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700151import java.util.Locale;
Gilles Debunne27113f82010-08-23 12:09:14 -0700152
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153/**
154 * Displays text to the user and optionally allows them to edit it. A TextView
155 * is a complete text editor, however the basic class is configured to not
156 * allow editing; see {@link EditText} for a subclass that configures the text
157 * view for editing.
158 *
159 * <p>
160 * <b>XML attributes</b>
161 * <p>
162 * See {@link android.R.styleable#TextView TextView Attributes},
163 * {@link android.R.styleable#View View Attributes}
164 *
165 * @attr ref android.R.styleable#TextView_text
166 * @attr ref android.R.styleable#TextView_bufferType
167 * @attr ref android.R.styleable#TextView_hint
168 * @attr ref android.R.styleable#TextView_textColor
169 * @attr ref android.R.styleable#TextView_textColorHighlight
170 * @attr ref android.R.styleable#TextView_textColorHint
Romain Guyd6a463a2009-05-21 23:10:10 -0700171 * @attr ref android.R.styleable#TextView_textAppearance
172 * @attr ref android.R.styleable#TextView_textColorLink
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173 * @attr ref android.R.styleable#TextView_textSize
174 * @attr ref android.R.styleable#TextView_textScaleX
175 * @attr ref android.R.styleable#TextView_typeface
176 * @attr ref android.R.styleable#TextView_textStyle
177 * @attr ref android.R.styleable#TextView_cursorVisible
178 * @attr ref android.R.styleable#TextView_maxLines
179 * @attr ref android.R.styleable#TextView_maxHeight
180 * @attr ref android.R.styleable#TextView_lines
181 * @attr ref android.R.styleable#TextView_height
182 * @attr ref android.R.styleable#TextView_minLines
183 * @attr ref android.R.styleable#TextView_minHeight
184 * @attr ref android.R.styleable#TextView_maxEms
185 * @attr ref android.R.styleable#TextView_maxWidth
186 * @attr ref android.R.styleable#TextView_ems
187 * @attr ref android.R.styleable#TextView_width
188 * @attr ref android.R.styleable#TextView_minEms
189 * @attr ref android.R.styleable#TextView_minWidth
190 * @attr ref android.R.styleable#TextView_gravity
191 * @attr ref android.R.styleable#TextView_scrollHorizontally
192 * @attr ref android.R.styleable#TextView_password
193 * @attr ref android.R.styleable#TextView_singleLine
194 * @attr ref android.R.styleable#TextView_selectAllOnFocus
195 * @attr ref android.R.styleable#TextView_includeFontPadding
196 * @attr ref android.R.styleable#TextView_maxLength
197 * @attr ref android.R.styleable#TextView_shadowColor
198 * @attr ref android.R.styleable#TextView_shadowDx
199 * @attr ref android.R.styleable#TextView_shadowDy
200 * @attr ref android.R.styleable#TextView_shadowRadius
201 * @attr ref android.R.styleable#TextView_autoLink
202 * @attr ref android.R.styleable#TextView_linksClickable
203 * @attr ref android.R.styleable#TextView_numeric
204 * @attr ref android.R.styleable#TextView_digits
205 * @attr ref android.R.styleable#TextView_phoneNumber
206 * @attr ref android.R.styleable#TextView_inputMethod
207 * @attr ref android.R.styleable#TextView_capitalize
208 * @attr ref android.R.styleable#TextView_autoText
209 * @attr ref android.R.styleable#TextView_editable
Romain Guyd6a463a2009-05-21 23:10:10 -0700210 * @attr ref android.R.styleable#TextView_freezesText
211 * @attr ref android.R.styleable#TextView_ellipsize
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800212 * @attr ref android.R.styleable#TextView_drawableTop
213 * @attr ref android.R.styleable#TextView_drawableBottom
214 * @attr ref android.R.styleable#TextView_drawableRight
215 * @attr ref android.R.styleable#TextView_drawableLeft
Romain Guyd6a463a2009-05-21 23:10:10 -0700216 * @attr ref android.R.styleable#TextView_drawablePadding
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800217 * @attr ref android.R.styleable#TextView_lineSpacingExtra
218 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
219 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
Romain Guyd6a463a2009-05-21 23:10:10 -0700220 * @attr ref android.R.styleable#TextView_inputType
221 * @attr ref android.R.styleable#TextView_imeOptions
222 * @attr ref android.R.styleable#TextView_privateImeOptions
223 * @attr ref android.R.styleable#TextView_imeActionLabel
224 * @attr ref android.R.styleable#TextView_imeActionId
225 * @attr ref android.R.styleable#TextView_editorExtras
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 */
227@RemoteView
228public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700229 static final String LOG_TAG = "TextView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230 static final boolean DEBUG_EXTRACT = false;
Gilles Debunneb7012e842011-02-24 15:40:38 -0800231
Romain Guy8b55f372010-08-18 17:10:07 -0700232 private static final int PRIORITY = 100;
Gilles Debunne3dbf55c2010-12-16 10:31:51 -0800233 private int mCurrentAlpha = 255;
Adam Powell879fb6b2010-09-20 11:23:56 -0700234
Adam Powellabcbb1a2010-10-04 21:12:19 -0700235 final int[] mTempCoords = new int[2];
236 Rect mTempRect;
Adam Powell879fb6b2010-09-20 11:23:56 -0700237
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 private ColorStateList mTextColor;
239 private int mCurTextColor;
240 private ColorStateList mHintTextColor;
241 private ColorStateList mLinkTextColor;
242 private int mCurHintTextColor;
243 private boolean mFreezesText;
244 private boolean mFrozenWithFocus;
245 private boolean mTemporaryDetach;
Romain Guya440b002010-02-24 15:57:54 -0800246 private boolean mDispatchTemporaryDetach;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800247
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800248 private boolean mDiscardNextActionUp = false;
249 private boolean mIgnoreActionUpEvent = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800250
251 private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
252 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
253
254 private float mShadowRadius, mShadowDx, mShadowDy;
255
256 private static final int PREDRAW_NOT_REGISTERED = 0;
257 private static final int PREDRAW_PENDING = 1;
258 private static final int PREDRAW_DONE = 2;
259 private int mPreDrawState = PREDRAW_NOT_REGISTERED;
260
261 private TextUtils.TruncateAt mEllipsize = null;
262
263 // Enum for the "typeface" XML parameter.
264 // TODO: How can we get this from the XML instead of hardcoding it here?
265 private static final int SANS = 1;
266 private static final int SERIF = 2;
267 private static final int MONOSPACE = 3;
268
269 // Bitfield for the "numeric" XML parameter.
270 // TODO: How can we get this from the XML instead of hardcoding it here?
271 private static final int SIGNED = 2;
272 private static final int DECIMAL = 4;
273
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700274 static class Drawables {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275 final Rect mCompoundRect = new Rect();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700276 Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight,
277 mDrawableStart, mDrawableEnd;
278 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
279 mDrawableSizeStart, mDrawableSizeEnd;
280 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
281 mDrawableHeightStart, mDrawableHeightEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 int mDrawablePadding;
283 }
284 private Drawables mDrawables;
285
286 private CharSequence mError;
287 private boolean mErrorWasChanged;
The Android Open Source Project10592532009-03-18 17:39:46 -0700288 private ErrorPopup mPopup;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 /**
290 * This flag is set if the TextView tries to display an error before it
291 * is attached to the window (so its position is still unknown).
292 * It causes the error to be shown later, when onAttachedToWindow()
293 * is called.
294 */
295 private boolean mShowErrorAfterAttach;
296
297 private CharWrapper mCharWrapper = null;
298
299 private boolean mSelectionMoved = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700300 private boolean mTouchFocusSelected = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800301
302 private Marquee mMarquee;
303 private boolean mRestartMarquee;
304
305 private int mMarqueeRepeatLimit = 3;
306
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700307 static class InputContentType {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700308 int imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800309 String privateImeOptions;
310 CharSequence imeActionLabel;
311 int imeActionId;
312 Bundle extras;
313 OnEditorActionListener onEditorActionListener;
314 boolean enterDown;
315 }
316 InputContentType mInputContentType;
317
Dianne Hackborn0500b3c2011-11-01 15:28:43 -0700318 static class InputMethodState {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 Rect mCursorRectInWindow = new Rect();
320 RectF mTmpRectF = new RectF();
321 float[] mTmpOffset = new float[2];
322 ExtractedTextRequest mExtracting;
323 final ExtractedText mTmpExtracted = new ExtractedText();
324 int mBatchEditNesting;
325 boolean mCursorChanged;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700326 boolean mSelectionModeChanged;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 boolean mContentChanged;
328 int mChangedStart, mChangedEnd, mChangedDelta;
329 }
330 InputMethodState mInputMethodState;
331
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800332 private int mTextSelectHandleLeftRes;
333 private int mTextSelectHandleRightRes;
334 private int mTextSelectHandleRes;
Adam Powellfbb3b472010-10-06 21:04:35 -0700335
Gilles Debunne69340442011-03-31 13:37:51 -0700336 private int mTextEditSuggestionItemLayout;
337 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunne214a8622011-04-26 15:44:37 -0700338 private SuggestionRangeSpan mSuggestionRangeSpan;
Gilles Debunne69340442011-03-31 13:37:51 -0700339
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800340 private int mCursorDrawableRes;
341 private final Drawable[] mCursorDrawable = new Drawable[2];
Gilles Debunne961ebb92011-12-12 10:16:04 -0800342 private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 (split)
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800343
344 private Drawable mSelectHandleLeft;
345 private Drawable mSelectHandleRight;
346 private Drawable mSelectHandleCenter;
Adam Powellb08013c2010-09-16 16:28:11 -0700347
Gilles Debunne21078e42011-08-02 10:22:35 -0700348 // Global listener that detects changes in the global position of the TextView
349 private PositionListener mPositionListener;
350
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700351 private float mLastDownPositionX, mLastDownPositionY;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800352 private Callback mCustomSelectionActionModeCallback;
Gilles Debunne9948ad72010-11-24 14:00:46 -0800353
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800354 private final int mSquaredTouchSlopDistance;
Gilles Debunnec01f3fe2010-12-22 17:07:36 -0800355 // Set when this TextView gained focus with some text selected. Will start selection mode.
356 private boolean mCreatedWithASelection = false;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800357
Gilles Debunnec59269f2011-04-22 11:46:09 -0700358 private WordIterator mWordIterator;
359
Gilles Debunne6435a562011-08-04 21:22:30 -0700360 private SpellChecker mSpellChecker;
361
Gilles Debunne0f4109e2011-10-19 11:26:21 -0700362 private boolean mSoftInputShownOnFocus = true;
Gilles Debunne550efbf2011-10-10 16:49:02 -0700363
Doug Feltc0ccf0c2011-06-23 16:13:18 -0700364 // The alignment to pass to Layout, or null if not resolved.
365 private Layout.Alignment mLayoutAlignment;
366
367 // The default value for mTextAlign.
368 private TextAlign mTextAlign = TextAlign.INHERIT;
369
370 private static enum TextAlign {
371 INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END;
372 }
373
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -0700374 private boolean mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700375
Adam Powell282e3772011-08-30 16:51:11 -0700376 /**
377 * On some devices the fading edges add a performance penalty if used
378 * extensively in the same layout. This mode indicates how the marquee
379 * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
380 */
381 private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
382
383 /**
384 * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
385 * the layout that should be used when the mode switches.
386 */
387 private Layout mSavedMarqueeModeLayout;
388
389 /**
390 * Draw marquee text with fading edges as usual
391 */
392 private static final int MARQUEE_FADE_NORMAL = 0;
393
394 /**
395 * Draw marquee text as ellipsize end while inactive instead of with the fade.
396 * (Useful for devices where the fade can be expensive if overdone)
397 */
398 private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
399
400 /**
401 * Draw marquee text with fading edges because it is currently active/animating.
402 */
403 private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
404
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800405 /*
406 * Kick-start the font cache for the zygote process (to pay the cost of
407 * initializing freetype for our default font only once).
408 */
409 static {
410 Paint p = new Paint();
411 p.setAntiAlias(true);
412 // We don't care about the result, just the side-effect of measuring.
413 p.measureText("H");
414 }
415
416 /**
417 * Interface definition for a callback to be invoked when an action is
418 * performed on the editor.
419 */
420 public interface OnEditorActionListener {
421 /**
422 * Called when an action is being performed.
423 *
424 * @param v The view that was clicked.
425 * @param actionId Identifier of the action. This will be either the
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700426 * identifier you supplied, or {@link EditorInfo#IME_NULL
427 * EditorInfo.IME_NULL} if being called due to the enter key
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800428 * being pressed.
429 * @param event If triggered by an enter key, this is the event;
430 * otherwise, this is null.
431 * @return Return true if you have consumed the action, else false.
432 */
433 boolean onEditorAction(TextView v, int actionId, KeyEvent event);
434 }
Gilles Debunne21078e42011-08-02 10:22:35 -0700435
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800436 public TextView(Context context) {
437 this(context, null);
438 }
439
440 public TextView(Context context,
441 AttributeSet attrs) {
442 this(context, attrs, com.android.internal.R.attr.textViewStyle);
443 }
444
Gilles Debunnee15b3582010-06-16 15:17:21 -0700445 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800446 public TextView(Context context,
447 AttributeSet attrs,
448 int defStyle) {
449 super(context, attrs, defStyle);
450 mText = "";
451
Christopher Tate1373a8e2011-11-10 19:59:13 -0800452 final Resources res = getResources();
453 final CompatibilityInfo compat = res.getCompatibilityInfo();
454
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800455 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Christopher Tate1373a8e2011-11-10 19:59:13 -0800456 mTextPaint.density = res.getDisplayMetrics().density;
457 mTextPaint.setCompatibilityScaling(compat.applicationScale);
Gilles Debunne8cbb4c62011-01-24 12:33:56 -0800458
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800459 // If we get the paint from the skin, we should set it to left, since
460 // the layout always wants it to be left.
461 // mTextPaint.setTextAlign(Paint.Align.LEFT);
462
463 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Christopher Tate1373a8e2011-11-10 19:59:13 -0800464 mHighlightPaint.setCompatibilityScaling(compat.applicationScale);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800465
466 mMovement = getDefaultMovementMethod();
467 mTransformation = null;
468
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800469 int textColorHighlight = 0;
470 ColorStateList textColor = null;
471 ColorStateList textColorHint = null;
472 ColorStateList textColorLink = null;
473 int textSize = 15;
474 int typefaceIndex = -1;
475 int styleIndex = -1;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700476 boolean allCaps = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800477
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700478 final Resources.Theme theme = context.getTheme();
479
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480 /*
481 * Look the appearance up without checking first if it exists because
482 * almost every TextView has one and it greatly simplifies the logic
483 * to be able to parse the appearance first and then let specific tags
484 * for this View override it.
485 */
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700486 TypedArray a = theme.obtainStyledAttributes(
487 attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 TypedArray appearance = null;
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700489 int ap = a.getResourceId(
490 com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
491 a.recycle();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800492 if (ap != -1) {
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700493 appearance = theme.obtainStyledAttributes(
494 ap, com.android.internal.R.styleable.TextAppearance);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800495 }
496 if (appearance != null) {
497 int n = appearance.getIndexCount();
498 for (int i = 0; i < n; i++) {
499 int attr = appearance.getIndex(i);
500
501 switch (attr) {
502 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
503 textColorHighlight = appearance.getColor(attr, textColorHighlight);
504 break;
505
506 case com.android.internal.R.styleable.TextAppearance_textColor:
507 textColor = appearance.getColorStateList(attr);
508 break;
509
510 case com.android.internal.R.styleable.TextAppearance_textColorHint:
511 textColorHint = appearance.getColorStateList(attr);
512 break;
513
514 case com.android.internal.R.styleable.TextAppearance_textColorLink:
515 textColorLink = appearance.getColorStateList(attr);
516 break;
517
518 case com.android.internal.R.styleable.TextAppearance_textSize:
519 textSize = appearance.getDimensionPixelSize(attr, textSize);
520 break;
521
522 case com.android.internal.R.styleable.TextAppearance_typeface:
523 typefaceIndex = appearance.getInt(attr, -1);
524 break;
525
526 case com.android.internal.R.styleable.TextAppearance_textStyle:
527 styleIndex = appearance.getInt(attr, -1);
528 break;
Adam Powell7f8f79a2011-07-07 18:35:54 -0700529
530 case com.android.internal.R.styleable.TextAppearance_textAllCaps:
531 allCaps = appearance.getBoolean(attr, false);
532 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800533 }
534 }
535
536 appearance.recycle();
537 }
538
539 boolean editable = getDefaultEditable();
540 CharSequence inputMethod = null;
541 int numeric = 0;
542 CharSequence digits = null;
543 boolean phone = false;
544 boolean autotext = false;
545 int autocap = -1;
546 int buffertype = 0;
547 boolean selectallonfocus = false;
548 Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700549 drawableBottom = null, drawableStart = null, drawableEnd = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800550 int drawablePadding = 0;
551 int ellipsize = -1;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700552 boolean singleLine = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800553 int maxlength = -1;
554 CharSequence text = "";
Romain Guy4dc4f732009-06-19 15:16:40 -0700555 CharSequence hint = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800556 int shadowcolor = 0;
557 float dx = 0, dy = 0, r = 0;
558 boolean password = false;
559 int inputType = EditorInfo.TYPE_NULL;
560
Dianne Hackbornab0f4852011-09-12 16:59:06 -0700561 a = theme.obtainStyledAttributes(
562 attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
563
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800564 int n = a.getIndexCount();
565 for (int i = 0; i < n; i++) {
566 int attr = a.getIndex(i);
567
568 switch (attr) {
569 case com.android.internal.R.styleable.TextView_editable:
570 editable = a.getBoolean(attr, editable);
571 break;
572
573 case com.android.internal.R.styleable.TextView_inputMethod:
574 inputMethod = a.getText(attr);
575 break;
576
577 case com.android.internal.R.styleable.TextView_numeric:
578 numeric = a.getInt(attr, numeric);
579 break;
580
581 case com.android.internal.R.styleable.TextView_digits:
582 digits = a.getText(attr);
583 break;
584
585 case com.android.internal.R.styleable.TextView_phoneNumber:
586 phone = a.getBoolean(attr, phone);
587 break;
588
589 case com.android.internal.R.styleable.TextView_autoText:
590 autotext = a.getBoolean(attr, autotext);
591 break;
592
593 case com.android.internal.R.styleable.TextView_capitalize:
594 autocap = a.getInt(attr, autocap);
595 break;
596
597 case com.android.internal.R.styleable.TextView_bufferType:
598 buffertype = a.getInt(attr, buffertype);
599 break;
600
601 case com.android.internal.R.styleable.TextView_selectAllOnFocus:
602 selectallonfocus = a.getBoolean(attr, selectallonfocus);
603 break;
604
605 case com.android.internal.R.styleable.TextView_autoLink:
606 mAutoLinkMask = a.getInt(attr, 0);
607 break;
608
609 case com.android.internal.R.styleable.TextView_linksClickable:
610 mLinksClickable = a.getBoolean(attr, true);
611 break;
612
Gilles Debunne550efbf2011-10-10 16:49:02 -0700613// TODO uncomment when this attribute is made public in the next release
614// also add TextView_showSoftInputOnFocus to the list of attributes above
615// case com.android.internal.R.styleable.TextView_showSoftInputOnFocus:
616// setShowSoftInputOnFocus(a.getBoolean(attr, true));
617// break;
618
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800619 case com.android.internal.R.styleable.TextView_drawableLeft:
620 drawableLeft = a.getDrawable(attr);
621 break;
622
623 case com.android.internal.R.styleable.TextView_drawableTop:
624 drawableTop = a.getDrawable(attr);
625 break;
626
627 case com.android.internal.R.styleable.TextView_drawableRight:
628 drawableRight = a.getDrawable(attr);
629 break;
630
631 case com.android.internal.R.styleable.TextView_drawableBottom:
632 drawableBottom = a.getDrawable(attr);
633 break;
634
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -0700635 case com.android.internal.R.styleable.TextView_drawableStart:
636 drawableStart = a.getDrawable(attr);
637 break;
638
639 case com.android.internal.R.styleable.TextView_drawableEnd:
640 drawableEnd = a.getDrawable(attr);
641 break;
642
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800643 case com.android.internal.R.styleable.TextView_drawablePadding:
644 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
645 break;
646
647 case com.android.internal.R.styleable.TextView_maxLines:
648 setMaxLines(a.getInt(attr, -1));
649 break;
650
651 case com.android.internal.R.styleable.TextView_maxHeight:
652 setMaxHeight(a.getDimensionPixelSize(attr, -1));
653 break;
654
655 case com.android.internal.R.styleable.TextView_lines:
656 setLines(a.getInt(attr, -1));
657 break;
658
659 case com.android.internal.R.styleable.TextView_height:
660 setHeight(a.getDimensionPixelSize(attr, -1));
661 break;
662
663 case com.android.internal.R.styleable.TextView_minLines:
664 setMinLines(a.getInt(attr, -1));
665 break;
666
667 case com.android.internal.R.styleable.TextView_minHeight:
668 setMinHeight(a.getDimensionPixelSize(attr, -1));
669 break;
670
671 case com.android.internal.R.styleable.TextView_maxEms:
672 setMaxEms(a.getInt(attr, -1));
673 break;
674
675 case com.android.internal.R.styleable.TextView_maxWidth:
676 setMaxWidth(a.getDimensionPixelSize(attr, -1));
677 break;
678
679 case com.android.internal.R.styleable.TextView_ems:
680 setEms(a.getInt(attr, -1));
681 break;
682
683 case com.android.internal.R.styleable.TextView_width:
684 setWidth(a.getDimensionPixelSize(attr, -1));
685 break;
686
687 case com.android.internal.R.styleable.TextView_minEms:
688 setMinEms(a.getInt(attr, -1));
689 break;
690
691 case com.android.internal.R.styleable.TextView_minWidth:
692 setMinWidth(a.getDimensionPixelSize(attr, -1));
693 break;
694
695 case com.android.internal.R.styleable.TextView_gravity:
696 setGravity(a.getInt(attr, -1));
697 break;
698
699 case com.android.internal.R.styleable.TextView_hint:
Romain Guy4dc4f732009-06-19 15:16:40 -0700700 hint = a.getText(attr);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800701 break;
702
703 case com.android.internal.R.styleable.TextView_text:
704 text = a.getText(attr);
705 break;
706
707 case com.android.internal.R.styleable.TextView_scrollHorizontally:
708 if (a.getBoolean(attr, false)) {
709 setHorizontallyScrolling(true);
710 }
711 break;
712
713 case com.android.internal.R.styleable.TextView_singleLine:
714 singleLine = a.getBoolean(attr, singleLine);
715 break;
716
717 case com.android.internal.R.styleable.TextView_ellipsize:
718 ellipsize = a.getInt(attr, ellipsize);
719 break;
720
721 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
722 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
723 break;
724
725 case com.android.internal.R.styleable.TextView_includeFontPadding:
726 if (!a.getBoolean(attr, true)) {
727 setIncludeFontPadding(false);
728 }
729 break;
730
731 case com.android.internal.R.styleable.TextView_cursorVisible:
732 if (!a.getBoolean(attr, true)) {
733 setCursorVisible(false);
734 }
735 break;
736
737 case com.android.internal.R.styleable.TextView_maxLength:
738 maxlength = a.getInt(attr, -1);
739 break;
740
741 case com.android.internal.R.styleable.TextView_textScaleX:
742 setTextScaleX(a.getFloat(attr, 1.0f));
743 break;
744
745 case com.android.internal.R.styleable.TextView_freezesText:
746 mFreezesText = a.getBoolean(attr, false);
747 break;
748
749 case com.android.internal.R.styleable.TextView_shadowColor:
750 shadowcolor = a.getInt(attr, 0);
751 break;
752
753 case com.android.internal.R.styleable.TextView_shadowDx:
754 dx = a.getFloat(attr, 0);
755 break;
756
757 case com.android.internal.R.styleable.TextView_shadowDy:
758 dy = a.getFloat(attr, 0);
759 break;
760
761 case com.android.internal.R.styleable.TextView_shadowRadius:
762 r = a.getFloat(attr, 0);
763 break;
764
765 case com.android.internal.R.styleable.TextView_enabled:
766 setEnabled(a.getBoolean(attr, isEnabled()));
767 break;
768
769 case com.android.internal.R.styleable.TextView_textColorHighlight:
770 textColorHighlight = a.getColor(attr, textColorHighlight);
771 break;
772
773 case com.android.internal.R.styleable.TextView_textColor:
774 textColor = a.getColorStateList(attr);
775 break;
776
777 case com.android.internal.R.styleable.TextView_textColorHint:
778 textColorHint = a.getColorStateList(attr);
779 break;
780
781 case com.android.internal.R.styleable.TextView_textColorLink:
782 textColorLink = a.getColorStateList(attr);
783 break;
784
785 case com.android.internal.R.styleable.TextView_textSize:
786 textSize = a.getDimensionPixelSize(attr, textSize);
787 break;
788
789 case com.android.internal.R.styleable.TextView_typeface:
790 typefaceIndex = a.getInt(attr, typefaceIndex);
791 break;
792
793 case com.android.internal.R.styleable.TextView_textStyle:
794 styleIndex = a.getInt(attr, styleIndex);
795 break;
796
797 case com.android.internal.R.styleable.TextView_password:
798 password = a.getBoolean(attr, password);
799 break;
800
801 case com.android.internal.R.styleable.TextView_lineSpacingExtra:
802 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
803 break;
804
805 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
806 mSpacingMult = a.getFloat(attr, mSpacingMult);
807 break;
808
809 case com.android.internal.R.styleable.TextView_inputType:
810 inputType = a.getInt(attr, mInputType);
811 break;
812
813 case com.android.internal.R.styleable.TextView_imeOptions:
814 if (mInputContentType == null) {
815 mInputContentType = new InputContentType();
816 }
817 mInputContentType.imeOptions = a.getInt(attr,
818 mInputContentType.imeOptions);
819 break;
820
821 case com.android.internal.R.styleable.TextView_imeActionLabel:
822 if (mInputContentType == null) {
823 mInputContentType = new InputContentType();
824 }
825 mInputContentType.imeActionLabel = a.getText(attr);
826 break;
827
828 case com.android.internal.R.styleable.TextView_imeActionId:
829 if (mInputContentType == null) {
830 mInputContentType = new InputContentType();
831 }
832 mInputContentType.imeActionId = a.getInt(attr,
833 mInputContentType.imeActionId);
834 break;
835
836 case com.android.internal.R.styleable.TextView_privateImeOptions:
837 setPrivateImeOptions(a.getString(attr));
838 break;
839
840 case com.android.internal.R.styleable.TextView_editorExtras:
841 try {
842 setInputExtras(a.getResourceId(attr, 0));
843 } catch (XmlPullParserException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700844 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800845 } catch (IOException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700846 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800847 }
848 break;
Adam Powellb08013c2010-09-16 16:28:11 -0700849
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800850 case com.android.internal.R.styleable.TextView_textCursorDrawable:
851 mCursorDrawableRes = a.getResourceId(attr, 0);
852 break;
853
Adam Powellb08013c2010-09-16 16:28:11 -0700854 case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
855 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
856 break;
857
858 case com.android.internal.R.styleable.TextView_textSelectHandleRight:
859 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
860 break;
861
862 case com.android.internal.R.styleable.TextView_textSelectHandle:
863 mTextSelectHandleRes = a.getResourceId(attr, 0);
864 break;
Gilles Debunne7b9652b2010-10-26 16:27:12 -0700865
Gilles Debunne69340442011-03-31 13:37:51 -0700866 case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
867 mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
868 break;
869
Gilles Debunne86b9c782010-11-11 10:43:48 -0800870 case com.android.internal.R.styleable.TextView_textIsSelectable:
871 mTextIsSelectable = a.getBoolean(attr, false);
872 break;
Gilles Debunnef3a135b2011-05-23 16:28:47 -0700873
Adam Powell7f8f79a2011-07-07 18:35:54 -0700874 case com.android.internal.R.styleable.TextView_textAllCaps:
875 allCaps = a.getBoolean(attr, false);
876 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800877 }
878 }
879 a.recycle();
880
881 BufferType bufferType = BufferType.EDITABLE;
882
Gilles Debunned7483bf2010-11-10 10:47:45 -0800883 final int variation =
884 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
885 final boolean passwordInputType = variation
886 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
887 final boolean webPasswordInputType = variation
888 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
Ken Wakasa82d731a2010-12-24 23:42:41 +0900889 final boolean numberPasswordInputType = variation
890 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Gilles Debunned7483bf2010-11-10 10:47:45 -0800891
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800892 if (inputMethod != null) {
Gilles Debunnee15b3582010-06-16 15:17:21 -0700893 Class<?> c;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800894
895 try {
896 c = Class.forName(inputMethod.toString());
897 } catch (ClassNotFoundException ex) {
898 throw new RuntimeException(ex);
899 }
900
901 try {
902 mInput = (KeyListener) c.newInstance();
903 } catch (InstantiationException ex) {
904 throw new RuntimeException(ex);
905 } catch (IllegalAccessException ex) {
906 throw new RuntimeException(ex);
907 }
908 try {
909 mInputType = inputType != EditorInfo.TYPE_NULL
910 ? inputType
911 : mInput.getInputType();
912 } catch (IncompatibleClassChangeError e) {
913 mInputType = EditorInfo.TYPE_CLASS_TEXT;
914 }
915 } else if (digits != null) {
916 mInput = DigitsKeyListener.getInstance(digits.toString());
Dianne Hackborn7ed6ee52009-09-10 18:41:28 -0700917 // If no input type was specified, we will default to generic
918 // text, since we can't tell the IME about the set of digits
919 // that was selected.
920 mInputType = inputType != EditorInfo.TYPE_NULL
921 ? inputType : EditorInfo.TYPE_CLASS_TEXT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800922 } else if (inputType != EditorInfo.TYPE_NULL) {
923 setInputType(inputType, true);
Gilles Debunne91a08cf2010-11-08 17:34:49 -0800924 // If set, the input type overrides what was set using the deprecated singleLine flag.
925 singleLine = !isMultilineInputType(inputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800926 } else if (phone) {
927 mInput = DialerKeyListener.getInstance();
Dianne Hackborn49a1a9b52009-03-24 20:06:11 -0700928 mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800929 } else if (numeric != 0) {
930 mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
931 (numeric & DECIMAL) != 0);
932 inputType = EditorInfo.TYPE_CLASS_NUMBER;
933 if ((numeric & SIGNED) != 0) {
934 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
935 }
936 if ((numeric & DECIMAL) != 0) {
937 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
938 }
939 mInputType = inputType;
940 } else if (autotext || autocap != -1) {
941 TextKeyListener.Capitalize cap;
942
943 inputType = EditorInfo.TYPE_CLASS_TEXT;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700944
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800945 switch (autocap) {
946 case 1:
947 cap = TextKeyListener.Capitalize.SENTENCES;
948 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
949 break;
950
951 case 2:
952 cap = TextKeyListener.Capitalize.WORDS;
953 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
954 break;
955
956 case 3:
957 cap = TextKeyListener.Capitalize.CHARACTERS;
958 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
959 break;
960
961 default:
962 cap = TextKeyListener.Capitalize.NONE;
963 break;
964 }
965
966 mInput = TextKeyListener.getInstance(autotext, cap);
967 mInputType = inputType;
Gilles Debunne86b9c782010-11-11 10:43:48 -0800968 } else if (mTextIsSelectable) {
969 // Prevent text changes from keyboard.
970 mInputType = EditorInfo.TYPE_NULL;
971 mInput = null;
972 bufferType = BufferType.SPANNABLE;
Gilles Debunnecbcb3452010-12-17 15:31:02 -0800973 // Required to request focus while in touch mode.
974 setFocusableInTouchMode(true);
Gilles Debunne86b9c782010-11-11 10:43:48 -0800975 // So that selection can be changed using arrow keys and touch is handled.
976 setMovementMethod(ArrowKeyMovementMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800977 } else if (editable) {
978 mInput = TextKeyListener.getInstance();
979 mInputType = EditorInfo.TYPE_CLASS_TEXT;
980 } else {
981 mInput = null;
982
983 switch (buffertype) {
984 case 0:
985 bufferType = BufferType.NORMAL;
986 break;
987 case 1:
988 bufferType = BufferType.SPANNABLE;
989 break;
990 case 2:
991 bufferType = BufferType.EDITABLE;
992 break;
993 }
994 }
995
Gilles Debunned7483bf2010-11-10 10:47:45 -0800996 // mInputType has been set from inputType, possibly modified by mInputMethod.
997 // Specialize mInputType to [web]password if we have a text class and the original input
998 // type was a password.
999 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
1000 if (password || passwordInputType) {
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -04001001 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
Gilles Debunned7483bf2010-11-10 10:47:45 -08001002 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -04001003 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001004 if (webPasswordInputType) {
1005 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1006 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
1007 }
Ken Wakasa82d731a2010-12-24 23:42:41 +09001008 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
1009 if (numberPasswordInputType) {
1010 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
1011 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
1012 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013 }
1014
1015 if (selectallonfocus) {
1016 mSelectAllOnFocus = true;
1017
1018 if (bufferType == BufferType.NORMAL)
1019 bufferType = BufferType.SPANNABLE;
1020 }
1021
1022 setCompoundDrawablesWithIntrinsicBounds(
1023 drawableLeft, drawableTop, drawableRight, drawableBottom);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001024 setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001025 setCompoundDrawablePadding(drawablePadding);
1026
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08001027 // Same as setSingleLine(), but make sure the transformation method and the maximum number
Gilles Debunne066460f2010-12-15 17:31:51 -08001028 // of lines of height are unchanged for multi-line TextViews.
Gilles Debunned7483bf2010-11-10 10:47:45 -08001029 setInputTypeSingleLine(singleLine);
Gilles Debunne066460f2010-12-15 17:31:51 -08001030 applySingleLine(singleLine, singleLine, singleLine);
Gilles Debunned7483bf2010-11-10 10:47:45 -08001031
Gilles Debunne91a08cf2010-11-08 17:34:49 -08001032 if (singleLine && mInput == null && ellipsize < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001033 ellipsize = 3; // END
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001034 }
1035
1036 switch (ellipsize) {
1037 case 1:
1038 setEllipsize(TextUtils.TruncateAt.START);
1039 break;
1040 case 2:
1041 setEllipsize(TextUtils.TruncateAt.MIDDLE);
1042 break;
1043 case 3:
1044 setEllipsize(TextUtils.TruncateAt.END);
1045 break;
1046 case 4:
Adam Powell282e3772011-08-30 16:51:11 -07001047 if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
1048 setHorizontalFadingEdgeEnabled(true);
1049 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
1050 } else {
1051 setHorizontalFadingEdgeEnabled(false);
1052 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
1053 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001054 setEllipsize(TextUtils.TruncateAt.MARQUEE);
1055 break;
1056 }
1057
1058 setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
1059 setHintTextColor(textColorHint);
1060 setLinkTextColor(textColorLink);
1061 if (textColorHighlight != 0) {
1062 setHighlightColor(textColorHighlight);
1063 }
1064 setRawTextSize(textSize);
1065
Adam Powell7f8f79a2011-07-07 18:35:54 -07001066 if (allCaps) {
1067 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
1068 }
1069
Ken Wakasa82d731a2010-12-24 23:42:41 +09001070 if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001071 setTransformationMethod(PasswordTransformationMethod.getInstance());
1072 typefaceIndex = MONOSPACE;
Gilles Debunned7483bf2010-11-10 10:47:45 -08001073 } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1074 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08001075 typefaceIndex = MONOSPACE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001076 }
1077
1078 setTypefaceByIndex(typefaceIndex, styleIndex);
1079
1080 if (shadowcolor != 0) {
1081 setShadowLayer(r, dx, dy, shadowcolor);
1082 }
1083
1084 if (maxlength >= 0) {
1085 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1086 } else {
1087 setFilters(NO_FILTERS);
1088 }
1089
1090 setText(text, bufferType);
Romain Guy4dc4f732009-06-19 15:16:40 -07001091 if (hint != null) setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001092
1093 /*
1094 * Views are not normally focusable unless specified to be.
1095 * However, TextViews that have input or movement methods *are*
1096 * focusable by default.
1097 */
1098 a = context.obtainStyledAttributes(attrs,
1099 com.android.internal.R.styleable.View,
1100 defStyle, 0);
1101
1102 boolean focusable = mMovement != null || mInput != null;
1103 boolean clickable = focusable;
1104 boolean longClickable = focusable;
1105
1106 n = a.getIndexCount();
1107 for (int i = 0; i < n; i++) {
1108 int attr = a.getIndex(i);
1109
1110 switch (attr) {
1111 case com.android.internal.R.styleable.View_focusable:
1112 focusable = a.getBoolean(attr, focusable);
1113 break;
1114
1115 case com.android.internal.R.styleable.View_clickable:
1116 clickable = a.getBoolean(attr, clickable);
1117 break;
1118
1119 case com.android.internal.R.styleable.View_longClickable:
1120 longClickable = a.getBoolean(attr, longClickable);
1121 break;
1122 }
1123 }
1124 a.recycle();
1125
1126 setFocusable(focusable);
1127 setClickable(clickable);
1128 setLongClickable(longClickable);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07001129
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001130 prepareCursorControllers();
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08001131
1132 final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1133 final int touchSlop = viewConfiguration.getScaledTouchSlop();
1134 mSquaredTouchSlopDistance = touchSlop * touchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001135 }
1136
1137 private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1138 Typeface tf = null;
1139 switch (typefaceIndex) {
1140 case SANS:
1141 tf = Typeface.SANS_SERIF;
1142 break;
1143
1144 case SERIF:
1145 tf = Typeface.SERIF;
1146 break;
1147
1148 case MONOSPACE:
1149 tf = Typeface.MONOSPACE;
1150 break;
1151 }
1152
1153 setTypeface(tf, styleIndex);
1154 }
1155
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001156 private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
1157 boolean hasRelativeDrawables = (start != null) || (end != null);
1158 if (hasRelativeDrawables) {
1159 Drawables dr = mDrawables;
1160 if (dr == null) {
1161 mDrawables = dr = new Drawables();
1162 }
1163 final Rect compoundRect = dr.mCompoundRect;
1164 int[] state = getDrawableState();
1165 if (start != null) {
1166 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1167 start.setState(state);
1168 start.copyBounds(compoundRect);
1169 start.setCallback(this);
1170
1171 dr.mDrawableStart = start;
1172 dr.mDrawableSizeStart = compoundRect.width();
1173 dr.mDrawableHeightStart = compoundRect.height();
1174 } else {
1175 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1176 }
1177 if (end != null) {
1178 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1179 end.setState(state);
1180 end.copyBounds(compoundRect);
1181 end.setCallback(this);
1182
1183 dr.mDrawableEnd = end;
1184 dr.mDrawableSizeEnd = compoundRect.width();
1185 dr.mDrawableHeightEnd = compoundRect.height();
1186 } else {
1187 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1188 }
1189 }
1190 }
1191
Janos Levai042856c2010-10-15 02:53:58 +03001192 @Override
1193 public void setEnabled(boolean enabled) {
1194 if (enabled == isEnabled()) {
1195 return;
1196 }
1197
1198 if (!enabled) {
1199 // Hide the soft input if the currently active TextView is disabled
1200 InputMethodManager imm = InputMethodManager.peekInstance();
1201 if (imm != null && imm.isActive(this)) {
1202 imm.hideSoftInputFromWindow(getWindowToken(), 0);
1203 }
1204 }
Gilles Debunne545c4d42011-11-29 10:37:15 -08001205
Janos Levai042856c2010-10-15 02:53:58 +03001206 super.setEnabled(enabled);
Gilles Debunne545c4d42011-11-29 10:37:15 -08001207
Dianne Hackbornbc823852011-09-18 17:19:50 -07001208 if (enabled) {
1209 // Make sure IME is updated with current editor info.
1210 InputMethodManager imm = InputMethodManager.peekInstance();
1211 if (imm != null) imm.restartInput(this);
1212 }
Mark Wagnerf8185112011-10-25 16:33:41 -07001213
Gilles Debunne545c4d42011-11-29 10:37:15 -08001214 prepareCursorControllers();
1215
Mark Wagnerf8185112011-10-25 16:33:41 -07001216 // start or stop the cursor blinking as appropriate
1217 makeBlink();
Janos Levai042856c2010-10-15 02:53:58 +03001218 }
1219
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001220 /**
1221 * Sets the typeface and style in which the text should be displayed,
1222 * and turns on the fake bold and italic bits in the Paint if the
1223 * Typeface that you provided does not have all the bits in the
1224 * style that you specified.
1225 *
1226 * @attr ref android.R.styleable#TextView_typeface
1227 * @attr ref android.R.styleable#TextView_textStyle
1228 */
1229 public void setTypeface(Typeface tf, int style) {
1230 if (style > 0) {
1231 if (tf == null) {
1232 tf = Typeface.defaultFromStyle(style);
1233 } else {
1234 tf = Typeface.create(tf, style);
1235 }
1236
1237 setTypeface(tf);
1238 // now compute what (if any) algorithmic styling is needed
1239 int typefaceStyle = tf != null ? tf.getStyle() : 0;
1240 int need = style & ~typefaceStyle;
1241 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1242 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1243 } else {
1244 mTextPaint.setFakeBoldText(false);
1245 mTextPaint.setTextSkewX(0);
1246 setTypeface(tf);
1247 }
1248 }
1249
1250 /**
1251 * Subclasses override this to specify that they have a KeyListener
1252 * by default even if not specifically called for in the XML options.
1253 */
1254 protected boolean getDefaultEditable() {
1255 return false;
1256 }
1257
1258 /**
1259 * Subclasses override this to specify a default movement method.
1260 */
1261 protected MovementMethod getDefaultMovementMethod() {
1262 return null;
1263 }
1264
1265 /**
1266 * Return the text the TextView is displaying. If setText() was called with
1267 * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1268 * the return value from this method to Spannable or Editable, respectively.
1269 *
1270 * Note: The content of the return value should not be modified. If you want
1271 * a modifiable one, you should make your own copy first.
1272 */
1273 @ViewDebug.CapturedViewProperty
1274 public CharSequence getText() {
1275 return mText;
1276 }
1277
1278 /**
1279 * Returns the length, in characters, of the text managed by this TextView
1280 */
1281 public int length() {
1282 return mText.length();
1283 }
1284
1285 /**
1286 * Return the text the TextView is displaying as an Editable object. If
1287 * the text is not editable, null is returned.
1288 *
1289 * @see #getText
1290 */
1291 public Editable getEditableText() {
1292 return (mText instanceof Editable) ? (Editable)mText : null;
1293 }
1294
1295 /**
1296 * @return the height of one standard line in pixels. Note that markup
1297 * within the text can cause individual lines to be taller or shorter
1298 * than this height, and the layout may contain additional first-
1299 * or last-line padding.
1300 */
1301 public int getLineHeight() {
Gilles Debunne96e6b8b2010-12-14 13:43:45 -08001302 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001303 }
1304
1305 /**
1306 * @return the Layout that is currently being used to display the text.
1307 * This can be null if the text or width has recently changes.
1308 */
1309 public final Layout getLayout() {
1310 return mLayout;
1311 }
1312
1313 /**
1314 * @return the current key listener for this TextView.
1315 * This will frequently be null for non-EditText TextViews.
1316 */
1317 public final KeyListener getKeyListener() {
1318 return mInput;
1319 }
1320
1321 /**
1322 * Sets the key listener to be used with this TextView. This can be null
1323 * to disallow user input. Note that this method has significant and
1324 * subtle interactions with soft keyboards and other input method:
1325 * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1326 * for important details. Calling this method will replace the current
1327 * content type of the text view with the content type returned by the
1328 * key listener.
1329 * <p>
1330 * Be warned that if you want a TextView with a key listener or movement
1331 * method not to be focusable, or if you want a TextView without a
1332 * key listener or movement method to be focusable, you must call
1333 * {@link #setFocusable} again after calling this to get the focusability
1334 * back the way you want it.
1335 *
1336 * @attr ref android.R.styleable#TextView_numeric
1337 * @attr ref android.R.styleable#TextView_digits
1338 * @attr ref android.R.styleable#TextView_phoneNumber
1339 * @attr ref android.R.styleable#TextView_inputMethod
1340 * @attr ref android.R.styleable#TextView_capitalize
1341 * @attr ref android.R.styleable#TextView_autoText
1342 */
1343 public void setKeyListener(KeyListener input) {
1344 setKeyListenerOnly(input);
1345 fixFocusableAndClickableSettings();
1346
1347 if (input != null) {
1348 try {
1349 mInputType = mInput.getInputType();
1350 } catch (IncompatibleClassChangeError e) {
1351 mInputType = EditorInfo.TYPE_CLASS_TEXT;
1352 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001353 // Change inputType, without affecting transformation.
1354 // No need to applySingleLine since mSingleLine is unchanged.
1355 setInputTypeSingleLine(mSingleLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001356 } else {
1357 mInputType = EditorInfo.TYPE_NULL;
1358 }
1359
1360 InputMethodManager imm = InputMethodManager.peekInstance();
1361 if (imm != null) imm.restartInput(this);
1362 }
1363
1364 private void setKeyListenerOnly(KeyListener input) {
1365 mInput = input;
1366 if (mInput != null && !(mText instanceof Editable))
1367 setText(mText);
1368
1369 setFilters((Editable) mText, mFilters);
1370 }
1371
1372 /**
1373 * @return the movement method being used for this TextView.
1374 * This will frequently be null for non-EditText TextViews.
1375 */
1376 public final MovementMethod getMovementMethod() {
1377 return mMovement;
1378 }
1379
1380 /**
1381 * Sets the movement method (arrow key handler) to be used for
1382 * this TextView. This can be null to disallow using the arrow keys
1383 * to move the cursor or scroll the view.
1384 * <p>
1385 * Be warned that if you want a TextView with a key listener or movement
1386 * method not to be focusable, or if you want a TextView without a
1387 * key listener or movement method to be focusable, you must call
1388 * {@link #setFocusable} again after calling this to get the focusability
1389 * back the way you want it.
1390 */
1391 public final void setMovementMethod(MovementMethod movement) {
1392 mMovement = movement;
1393
1394 if (mMovement != null && !(mText instanceof Spannable))
1395 setText(mText);
1396
1397 fixFocusableAndClickableSettings();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001398
Gilles Debunnebaaace52010-10-01 15:47:13 -07001399 // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001400 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001401 }
1402
1403 private void fixFocusableAndClickableSettings() {
1404 if ((mMovement != null) || mInput != null) {
1405 setFocusable(true);
1406 setClickable(true);
1407 setLongClickable(true);
1408 } else {
1409 setFocusable(false);
1410 setClickable(false);
1411 setLongClickable(false);
1412 }
1413 }
1414
1415 /**
1416 * @return the current transformation method for this TextView.
1417 * This will frequently be null except for single-line and password
1418 * fields.
1419 */
1420 public final TransformationMethod getTransformationMethod() {
1421 return mTransformation;
1422 }
1423
1424 /**
1425 * Sets the transformation that is applied to the text that this
1426 * TextView is displaying.
1427 *
1428 * @attr ref android.R.styleable#TextView_password
1429 * @attr ref android.R.styleable#TextView_singleLine
1430 */
1431 public final void setTransformationMethod(TransformationMethod method) {
1432 if (method == mTransformation) {
1433 // Avoid the setText() below if the transformation is
1434 // the same.
1435 return;
1436 }
1437 if (mTransformation != null) {
1438 if (mText instanceof Spannable) {
1439 ((Spannable) mText).removeSpan(mTransformation);
1440 }
1441 }
1442
1443 mTransformation = method;
1444
Adam Powell7f8f79a2011-07-07 18:35:54 -07001445 if (method instanceof TransformationMethod2) {
1446 TransformationMethod2 method2 = (TransformationMethod2) method;
1447 mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable);
1448 method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
1449 } else {
1450 mAllowTransformationLengthChange = false;
1451 }
1452
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001453 setText(mText);
1454 }
1455
1456 /**
1457 * Returns the top padding of the view, plus space for the top
1458 * Drawable if any.
1459 */
1460 public int getCompoundPaddingTop() {
1461 final Drawables dr = mDrawables;
1462 if (dr == null || dr.mDrawableTop == null) {
1463 return mPaddingTop;
1464 } else {
1465 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1466 }
1467 }
1468
1469 /**
1470 * Returns the bottom padding of the view, plus space for the bottom
1471 * Drawable if any.
1472 */
1473 public int getCompoundPaddingBottom() {
1474 final Drawables dr = mDrawables;
1475 if (dr == null || dr.mDrawableBottom == null) {
1476 return mPaddingBottom;
1477 } else {
1478 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1479 }
1480 }
1481
1482 /**
1483 * Returns the left padding of the view, plus space for the left
1484 * Drawable if any.
1485 */
1486 public int getCompoundPaddingLeft() {
1487 final Drawables dr = mDrawables;
1488 if (dr == null || dr.mDrawableLeft == null) {
1489 return mPaddingLeft;
1490 } else {
1491 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1492 }
1493 }
1494
1495 /**
1496 * Returns the right padding of the view, plus space for the right
1497 * Drawable if any.
1498 */
1499 public int getCompoundPaddingRight() {
1500 final Drawables dr = mDrawables;
1501 if (dr == null || dr.mDrawableRight == null) {
1502 return mPaddingRight;
1503 } else {
1504 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1505 }
1506 }
1507
1508 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001509 * Returns the start padding of the view, plus space for the start
1510 * Drawable if any.
1511 *
1512 * @hide
1513 */
1514 public int getCompoundPaddingStart() {
1515 resolveDrawables();
1516 switch(getResolvedLayoutDirection()) {
1517 default:
1518 case LAYOUT_DIRECTION_LTR:
1519 return getCompoundPaddingLeft();
1520 case LAYOUT_DIRECTION_RTL:
1521 return getCompoundPaddingRight();
1522 }
1523 }
1524
1525 /**
1526 * Returns the end padding of the view, plus space for the end
1527 * Drawable if any.
1528 *
1529 * @hide
1530 */
1531 public int getCompoundPaddingEnd() {
1532 resolveDrawables();
1533 switch(getResolvedLayoutDirection()) {
1534 default:
1535 case LAYOUT_DIRECTION_LTR:
1536 return getCompoundPaddingRight();
1537 case LAYOUT_DIRECTION_RTL:
1538 return getCompoundPaddingLeft();
1539 }
1540 }
1541
1542 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001543 * Returns the extended top padding of the view, including both the
1544 * top Drawable if any and any extra space to keep more than maxLines
1545 * of text from showing. It is only valid to call this after measuring.
1546 */
1547 public int getExtendedPaddingTop() {
1548 if (mMaxMode != LINES) {
1549 return getCompoundPaddingTop();
1550 }
1551
1552 if (mLayout.getLineCount() <= mMaximum) {
1553 return getCompoundPaddingTop();
1554 }
1555
1556 int top = getCompoundPaddingTop();
1557 int bottom = getCompoundPaddingBottom();
1558 int viewht = getHeight() - top - bottom;
1559 int layoutht = mLayout.getLineTop(mMaximum);
1560
1561 if (layoutht >= viewht) {
1562 return top;
1563 }
1564
1565 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1566 if (gravity == Gravity.TOP) {
1567 return top;
1568 } else if (gravity == Gravity.BOTTOM) {
1569 return top + viewht - layoutht;
1570 } else { // (gravity == Gravity.CENTER_VERTICAL)
1571 return top + (viewht - layoutht) / 2;
1572 }
1573 }
1574
1575 /**
1576 * Returns the extended bottom padding of the view, including both the
1577 * bottom Drawable if any and any extra space to keep more than maxLines
1578 * of text from showing. It is only valid to call this after measuring.
1579 */
1580 public int getExtendedPaddingBottom() {
1581 if (mMaxMode != LINES) {
1582 return getCompoundPaddingBottom();
1583 }
1584
1585 if (mLayout.getLineCount() <= mMaximum) {
1586 return getCompoundPaddingBottom();
1587 }
1588
1589 int top = getCompoundPaddingTop();
1590 int bottom = getCompoundPaddingBottom();
1591 int viewht = getHeight() - top - bottom;
1592 int layoutht = mLayout.getLineTop(mMaximum);
1593
1594 if (layoutht >= viewht) {
1595 return bottom;
1596 }
1597
1598 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1599 if (gravity == Gravity.TOP) {
1600 return bottom + viewht - layoutht;
1601 } else if (gravity == Gravity.BOTTOM) {
1602 return bottom;
1603 } else { // (gravity == Gravity.CENTER_VERTICAL)
1604 return bottom + (viewht - layoutht) / 2;
1605 }
1606 }
1607
1608 /**
1609 * Returns the total left padding of the view, including the left
1610 * Drawable if any.
1611 */
1612 public int getTotalPaddingLeft() {
1613 return getCompoundPaddingLeft();
1614 }
1615
1616 /**
1617 * Returns the total right padding of the view, including the right
1618 * Drawable if any.
1619 */
1620 public int getTotalPaddingRight() {
1621 return getCompoundPaddingRight();
1622 }
1623
1624 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001625 * Returns the total start padding of the view, including the start
1626 * Drawable if any.
1627 *
1628 * @hide
1629 */
1630 public int getTotalPaddingStart() {
1631 return getCompoundPaddingStart();
1632 }
1633
1634 /**
1635 * Returns the total end padding of the view, including the end
1636 * Drawable if any.
1637 *
1638 * @hide
1639 */
1640 public int getTotalPaddingEnd() {
1641 return getCompoundPaddingEnd();
1642 }
1643
1644 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001645 * Returns the total top padding of the view, including the top
1646 * Drawable if any, the extra space to keep more than maxLines
1647 * from showing, and the vertical offset for gravity, if any.
1648 */
1649 public int getTotalPaddingTop() {
1650 return getExtendedPaddingTop() + getVerticalOffset(true);
1651 }
1652
1653 /**
1654 * Returns the total bottom padding of the view, including the bottom
1655 * Drawable if any, the extra space to keep more than maxLines
1656 * from showing, and the vertical offset for gravity, if any.
1657 */
1658 public int getTotalPaddingBottom() {
1659 return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1660 }
1661
1662 /**
1663 * Sets the Drawables (if any) to appear to the left of, above,
1664 * to the right of, and below the text. Use null if you do not
1665 * want a Drawable there. The Drawables must already have had
1666 * {@link Drawable#setBounds} called.
1667 *
1668 * @attr ref android.R.styleable#TextView_drawableLeft
1669 * @attr ref android.R.styleable#TextView_drawableTop
1670 * @attr ref android.R.styleable#TextView_drawableRight
1671 * @attr ref android.R.styleable#TextView_drawableBottom
1672 */
1673 public void setCompoundDrawables(Drawable left, Drawable top,
1674 Drawable right, Drawable bottom) {
1675 Drawables dr = mDrawables;
1676
1677 final boolean drawables = left != null || top != null
1678 || right != null || bottom != null;
1679
1680 if (!drawables) {
1681 // Clearing drawables... can we free the data structure?
1682 if (dr != null) {
1683 if (dr.mDrawablePadding == 0) {
1684 mDrawables = null;
1685 } else {
1686 // We need to retain the last set padding, so just clear
1687 // out all of the fields in the existing structure.
Romain Guy48540eb2009-05-19 16:44:57 -07001688 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001689 dr.mDrawableLeft = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001690 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001691 dr.mDrawableTop = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001692 if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001693 dr.mDrawableRight = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001694 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001695 dr.mDrawableBottom = null;
1696 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1697 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1698 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1699 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1700 }
1701 }
1702 } else {
1703 if (dr == null) {
1704 mDrawables = dr = new Drawables();
1705 }
1706
Romain Guy48540eb2009-05-19 16:44:57 -07001707 if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1708 dr.mDrawableLeft.setCallback(null);
1709 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001710 dr.mDrawableLeft = left;
Romain Guy8e618e52010-03-08 12:18:20 -08001711
1712 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001713 dr.mDrawableTop.setCallback(null);
1714 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001715 dr.mDrawableTop = top;
Romain Guy8e618e52010-03-08 12:18:20 -08001716
1717 if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001718 dr.mDrawableRight.setCallback(null);
1719 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001720 dr.mDrawableRight = right;
Romain Guy8e618e52010-03-08 12:18:20 -08001721
1722 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001723 dr.mDrawableBottom.setCallback(null);
1724 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001725 dr.mDrawableBottom = bottom;
1726
1727 final Rect compoundRect = dr.mCompoundRect;
Romain Guy48540eb2009-05-19 16:44:57 -07001728 int[] state;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001729
1730 state = getDrawableState();
1731
1732 if (left != null) {
1733 left.setState(state);
1734 left.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001735 left.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001736 dr.mDrawableSizeLeft = compoundRect.width();
1737 dr.mDrawableHeightLeft = compoundRect.height();
1738 } else {
1739 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1740 }
1741
1742 if (right != null) {
1743 right.setState(state);
1744 right.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001745 right.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001746 dr.mDrawableSizeRight = compoundRect.width();
1747 dr.mDrawableHeightRight = compoundRect.height();
1748 } else {
1749 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1750 }
1751
1752 if (top != null) {
1753 top.setState(state);
1754 top.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001755 top.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001756 dr.mDrawableSizeTop = compoundRect.height();
1757 dr.mDrawableWidthTop = compoundRect.width();
1758 } else {
1759 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1760 }
1761
1762 if (bottom != null) {
1763 bottom.setState(state);
1764 bottom.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001765 bottom.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001766 dr.mDrawableSizeBottom = compoundRect.height();
1767 dr.mDrawableWidthBottom = compoundRect.width();
1768 } else {
1769 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1770 }
1771 }
1772
1773 invalidate();
1774 requestLayout();
1775 }
1776
1777 /**
1778 * Sets the Drawables (if any) to appear to the left of, above,
1779 * to the right of, and below the text. Use 0 if you do not
1780 * want a Drawable there. The Drawables' bounds will be set to
1781 * their intrinsic bounds.
1782 *
1783 * @param left Resource identifier of the left Drawable.
1784 * @param top Resource identifier of the top Drawable.
1785 * @param right Resource identifier of the right Drawable.
1786 * @param bottom Resource identifier of the bottom Drawable.
1787 *
1788 * @attr ref android.R.styleable#TextView_drawableLeft
1789 * @attr ref android.R.styleable#TextView_drawableTop
1790 * @attr ref android.R.styleable#TextView_drawableRight
1791 * @attr ref android.R.styleable#TextView_drawableBottom
1792 */
1793 public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1794 final Resources resources = getContext().getResources();
1795 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1796 top != 0 ? resources.getDrawable(top) : null,
1797 right != 0 ? resources.getDrawable(right) : null,
1798 bottom != 0 ? resources.getDrawable(bottom) : null);
1799 }
1800
1801 /**
1802 * Sets the Drawables (if any) to appear to the left of, above,
1803 * to the right of, and below the text. Use null if you do not
1804 * want a Drawable there. The Drawables' bounds will be set to
1805 * their intrinsic bounds.
1806 *
1807 * @attr ref android.R.styleable#TextView_drawableLeft
1808 * @attr ref android.R.styleable#TextView_drawableTop
1809 * @attr ref android.R.styleable#TextView_drawableRight
1810 * @attr ref android.R.styleable#TextView_drawableBottom
1811 */
1812 public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1813 Drawable right, Drawable bottom) {
1814
1815 if (left != null) {
1816 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1817 }
1818 if (right != null) {
1819 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1820 }
1821 if (top != null) {
1822 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1823 }
1824 if (bottom != null) {
1825 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1826 }
1827 setCompoundDrawables(left, top, right, bottom);
1828 }
1829
1830 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07001831 * Sets the Drawables (if any) to appear to the start of, above,
1832 * to the end of, and below the text. Use null if you do not
1833 * want a Drawable there. The Drawables must already have had
1834 * {@link Drawable#setBounds} called.
1835 *
1836 * @attr ref android.R.styleable#TextView_drawableStart
1837 * @attr ref android.R.styleable#TextView_drawableTop
1838 * @attr ref android.R.styleable#TextView_drawableEnd
1839 * @attr ref android.R.styleable#TextView_drawableBottom
1840 *
1841 * @hide
1842 */
1843 public void setCompoundDrawablesRelative(Drawable start, Drawable top,
1844 Drawable end, Drawable bottom) {
1845 Drawables dr = mDrawables;
1846
1847 final boolean drawables = start != null || top != null
1848 || end != null || bottom != null;
1849
1850 if (!drawables) {
1851 // Clearing drawables... can we free the data structure?
1852 if (dr != null) {
1853 if (dr.mDrawablePadding == 0) {
1854 mDrawables = null;
1855 } else {
1856 // We need to retain the last set padding, so just clear
1857 // out all of the fields in the existing structure.
1858 if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
1859 dr.mDrawableStart = null;
1860 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1861 dr.mDrawableTop = null;
1862 if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
1863 dr.mDrawableEnd = null;
1864 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1865 dr.mDrawableBottom = null;
1866 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1867 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1868 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1869 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1870 }
1871 }
1872 } else {
1873 if (dr == null) {
1874 mDrawables = dr = new Drawables();
1875 }
1876
1877 if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
1878 dr.mDrawableStart.setCallback(null);
1879 }
1880 dr.mDrawableStart = start;
1881
1882 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1883 dr.mDrawableTop.setCallback(null);
1884 }
1885 dr.mDrawableTop = top;
1886
1887 if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
1888 dr.mDrawableEnd.setCallback(null);
1889 }
1890 dr.mDrawableEnd = end;
1891
1892 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1893 dr.mDrawableBottom.setCallback(null);
1894 }
1895 dr.mDrawableBottom = bottom;
1896
1897 final Rect compoundRect = dr.mCompoundRect;
1898 int[] state;
1899
1900 state = getDrawableState();
1901
1902 if (start != null) {
1903 start.setState(state);
1904 start.copyBounds(compoundRect);
1905 start.setCallback(this);
1906 dr.mDrawableSizeStart = compoundRect.width();
1907 dr.mDrawableHeightStart = compoundRect.height();
1908 } else {
1909 dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
1910 }
1911
1912 if (end != null) {
1913 end.setState(state);
1914 end.copyBounds(compoundRect);
1915 end.setCallback(this);
1916 dr.mDrawableSizeEnd = compoundRect.width();
1917 dr.mDrawableHeightEnd = compoundRect.height();
1918 } else {
1919 dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
1920 }
1921
1922 if (top != null) {
1923 top.setState(state);
1924 top.copyBounds(compoundRect);
1925 top.setCallback(this);
1926 dr.mDrawableSizeTop = compoundRect.height();
1927 dr.mDrawableWidthTop = compoundRect.width();
1928 } else {
1929 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1930 }
1931
1932 if (bottom != null) {
1933 bottom.setState(state);
1934 bottom.copyBounds(compoundRect);
1935 bottom.setCallback(this);
1936 dr.mDrawableSizeBottom = compoundRect.height();
1937 dr.mDrawableWidthBottom = compoundRect.width();
1938 } else {
1939 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1940 }
1941 }
1942
1943 resolveDrawables();
1944 invalidate();
1945 requestLayout();
1946 }
1947
1948 /**
1949 * Sets the Drawables (if any) to appear to the start of, above,
1950 * to the end of, and below the text. Use 0 if you do not
1951 * want a Drawable there. The Drawables' bounds will be set to
1952 * their intrinsic bounds.
1953 *
1954 * @param start Resource identifier of the start Drawable.
1955 * @param top Resource identifier of the top Drawable.
1956 * @param end Resource identifier of the end Drawable.
1957 * @param bottom Resource identifier of the bottom Drawable.
1958 *
1959 * @attr ref android.R.styleable#TextView_drawableStart
1960 * @attr ref android.R.styleable#TextView_drawableTop
1961 * @attr ref android.R.styleable#TextView_drawableEnd
1962 * @attr ref android.R.styleable#TextView_drawableBottom
1963 *
1964 * @hide
1965 */
1966 public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end,
1967 int bottom) {
1968 resetResolvedDrawables();
1969 final Resources resources = getContext().getResources();
1970 setCompoundDrawablesRelativeWithIntrinsicBounds(
1971 start != 0 ? resources.getDrawable(start) : null,
1972 top != 0 ? resources.getDrawable(top) : null,
1973 end != 0 ? resources.getDrawable(end) : null,
1974 bottom != 0 ? resources.getDrawable(bottom) : null);
1975 }
1976
1977 /**
1978 * Sets the Drawables (if any) to appear to the start of, above,
1979 * to the end of, and below the text. Use null if you do not
1980 * want a Drawable there. The Drawables' bounds will be set to
1981 * their intrinsic bounds.
1982 *
1983 * @attr ref android.R.styleable#TextView_drawableStart
1984 * @attr ref android.R.styleable#TextView_drawableTop
1985 * @attr ref android.R.styleable#TextView_drawableEnd
1986 * @attr ref android.R.styleable#TextView_drawableBottom
1987 *
1988 * @hide
1989 */
1990 public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top,
1991 Drawable end, Drawable bottom) {
1992
1993 resetResolvedDrawables();
1994 if (start != null) {
1995 start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
1996 }
1997 if (end != null) {
1998 end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
1999 }
2000 if (top != null) {
2001 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
2002 }
2003 if (bottom != null) {
2004 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
2005 }
2006 setCompoundDrawablesRelative(start, top, end, bottom);
2007 }
2008
2009 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002010 * Returns drawables for the left, top, right, and bottom borders.
2011 */
2012 public Drawable[] getCompoundDrawables() {
2013 final Drawables dr = mDrawables;
2014 if (dr != null) {
2015 return new Drawable[] {
2016 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
2017 };
2018 } else {
2019 return new Drawable[] { null, null, null, null };
2020 }
2021 }
2022
2023 /**
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002024 * Returns drawables for the start, top, end, and bottom borders.
2025 *
2026 * @hide
2027 */
2028 public Drawable[] getCompoundDrawablesRelative() {
2029 final Drawables dr = mDrawables;
2030 if (dr != null) {
2031 return new Drawable[] {
2032 dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom
2033 };
2034 } else {
2035 return new Drawable[] { null, null, null, null };
2036 }
2037 }
2038
2039 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002040 * Sets the size of the padding between the compound drawables and
2041 * the text.
2042 *
2043 * @attr ref android.R.styleable#TextView_drawablePadding
2044 */
2045 public void setCompoundDrawablePadding(int pad) {
2046 Drawables dr = mDrawables;
2047 if (pad == 0) {
2048 if (dr != null) {
2049 dr.mDrawablePadding = pad;
2050 }
2051 } else {
2052 if (dr == null) {
2053 mDrawables = dr = new Drawables();
2054 }
2055 dr.mDrawablePadding = pad;
2056 }
2057
2058 invalidate();
2059 requestLayout();
2060 }
2061
2062 /**
2063 * Returns the padding between the compound drawables and the text.
2064 */
2065 public int getCompoundDrawablePadding() {
2066 final Drawables dr = mDrawables;
2067 return dr != null ? dr.mDrawablePadding : 0;
2068 }
2069
2070 @Override
2071 public void setPadding(int left, int top, int right, int bottom) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07002072 if (left != mPaddingLeft ||
2073 right != mPaddingRight ||
2074 top != mPaddingTop ||
2075 bottom != mPaddingBottom) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002076 nullLayouts();
2077 }
2078
2079 // the super call will requestLayout()
2080 super.setPadding(left, top, right, bottom);
2081 invalidate();
2082 }
2083
2084 /**
2085 * Gets the autolink mask of the text. See {@link
2086 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2087 * possible values.
2088 *
2089 * @attr ref android.R.styleable#TextView_autoLink
2090 */
2091 public final int getAutoLinkMask() {
2092 return mAutoLinkMask;
2093 }
2094
2095 /**
2096 * Sets the text color, size, style, hint color, and highlight color
2097 * from the specified TextAppearance resource.
2098 */
2099 public void setTextAppearance(Context context, int resid) {
2100 TypedArray appearance =
2101 context.obtainStyledAttributes(resid,
2102 com.android.internal.R.styleable.TextAppearance);
2103
2104 int color;
2105 ColorStateList colors;
2106 int ts;
2107
2108 color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
2109 if (color != 0) {
2110 setHighlightColor(color);
2111 }
2112
2113 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2114 TextAppearance_textColor);
2115 if (colors != null) {
2116 setTextColor(colors);
2117 }
2118
2119 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
2120 TextAppearance_textSize, 0);
2121 if (ts != 0) {
2122 setRawTextSize(ts);
2123 }
2124
2125 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2126 TextAppearance_textColorHint);
2127 if (colors != null) {
2128 setHintTextColor(colors);
2129 }
2130
2131 colors = appearance.getColorStateList(com.android.internal.R.styleable.
2132 TextAppearance_textColorLink);
2133 if (colors != null) {
2134 setLinkTextColor(colors);
2135 }
2136
2137 int typefaceIndex, styleIndex;
2138
2139 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
2140 TextAppearance_typeface, -1);
2141 styleIndex = appearance.getInt(com.android.internal.R.styleable.
2142 TextAppearance_textStyle, -1);
2143
2144 setTypefaceByIndex(typefaceIndex, styleIndex);
Gilles Debunne21078e42011-08-02 10:22:35 -07002145
Adam Powell7f8f79a2011-07-07 18:35:54 -07002146 if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps,
2147 false)) {
2148 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
2149 }
2150
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002151 appearance.recycle();
2152 }
2153
2154 /**
2155 * @return the size (in pixels) of the default text size in this TextView.
2156 */
2157 public float getTextSize() {
2158 return mTextPaint.getTextSize();
2159 }
2160
2161 /**
2162 * Set the default text size to the given value, interpreted as "scaled
2163 * pixel" units. This size is adjusted based on the current density and
2164 * user font size preference.
2165 *
2166 * @param size The scaled pixel size.
2167 *
2168 * @attr ref android.R.styleable#TextView_textSize
2169 */
2170 @android.view.RemotableViewMethod
2171 public void setTextSize(float size) {
2172 setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
2173 }
2174
2175 /**
2176 * Set the default text size to a given unit and value. See {@link
2177 * TypedValue} for the possible dimension units.
2178 *
2179 * @param unit The desired dimension unit.
2180 * @param size The desired size in the given units.
2181 *
2182 * @attr ref android.R.styleable#TextView_textSize
2183 */
2184 public void setTextSize(int unit, float size) {
2185 Context c = getContext();
2186 Resources r;
2187
2188 if (c == null)
2189 r = Resources.getSystem();
2190 else
2191 r = c.getResources();
2192
2193 setRawTextSize(TypedValue.applyDimension(
2194 unit, size, r.getDisplayMetrics()));
2195 }
2196
2197 private void setRawTextSize(float size) {
2198 if (size != mTextPaint.getTextSize()) {
2199 mTextPaint.setTextSize(size);
2200
2201 if (mLayout != null) {
2202 nullLayouts();
2203 requestLayout();
2204 invalidate();
2205 }
2206 }
2207 }
2208
2209 /**
2210 * @return the extent by which text is currently being stretched
2211 * horizontally. This will usually be 1.
2212 */
2213 public float getTextScaleX() {
2214 return mTextPaint.getTextScaleX();
2215 }
2216
2217 /**
2218 * Sets the extent by which text should be stretched horizontally.
2219 *
2220 * @attr ref android.R.styleable#TextView_textScaleX
2221 */
2222 @android.view.RemotableViewMethod
2223 public void setTextScaleX(float size) {
2224 if (size != mTextPaint.getTextScaleX()) {
Romain Guy939151f2009-04-08 14:22:40 -07002225 mUserSetTextScaleX = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002226 mTextPaint.setTextScaleX(size);
2227
2228 if (mLayout != null) {
2229 nullLayouts();
2230 requestLayout();
2231 invalidate();
2232 }
2233 }
2234 }
2235
2236 /**
2237 * Sets the typeface and style in which the text should be displayed.
2238 * Note that not all Typeface families actually have bold and italic
2239 * variants, so you may need to use
2240 * {@link #setTypeface(Typeface, int)} to get the appearance
2241 * that you actually want.
2242 *
2243 * @attr ref android.R.styleable#TextView_typeface
2244 * @attr ref android.R.styleable#TextView_textStyle
2245 */
2246 public void setTypeface(Typeface tf) {
2247 if (mTextPaint.getTypeface() != tf) {
2248 mTextPaint.setTypeface(tf);
2249
2250 if (mLayout != null) {
2251 nullLayouts();
2252 requestLayout();
2253 invalidate();
2254 }
2255 }
2256 }
2257
2258 /**
2259 * @return the current typeface and style in which the text is being
2260 * displayed.
2261 */
2262 public Typeface getTypeface() {
2263 return mTextPaint.getTypeface();
2264 }
2265
2266 /**
2267 * Sets the text color for all the states (normal, selected,
2268 * focused) to be this color.
2269 *
2270 * @attr ref android.R.styleable#TextView_textColor
2271 */
2272 @android.view.RemotableViewMethod
2273 public void setTextColor(int color) {
2274 mTextColor = ColorStateList.valueOf(color);
2275 updateTextColors();
2276 }
2277
2278 /**
2279 * Sets the text color.
2280 *
2281 * @attr ref android.R.styleable#TextView_textColor
2282 */
2283 public void setTextColor(ColorStateList colors) {
2284 if (colors == null) {
2285 throw new NullPointerException();
2286 }
2287
2288 mTextColor = colors;
2289 updateTextColors();
2290 }
2291
2292 /**
2293 * Return the set of text colors.
2294 *
2295 * @return Returns the set of text colors.
2296 */
2297 public final ColorStateList getTextColors() {
2298 return mTextColor;
2299 }
2300
2301 /**
2302 * <p>Return the current color selected for normal text.</p>
2303 *
2304 * @return Returns the current text color.
2305 */
2306 public final int getCurrentTextColor() {
2307 return mCurTextColor;
2308 }
2309
2310 /**
2311 * Sets the color used to display the selection highlight.
2312 *
2313 * @attr ref android.R.styleable#TextView_textColorHighlight
2314 */
2315 @android.view.RemotableViewMethod
2316 public void setHighlightColor(int color) {
2317 if (mHighlightColor != color) {
2318 mHighlightColor = color;
2319 invalidate();
2320 }
2321 }
2322
2323 /**
2324 * Gives the text a shadow of the specified radius and color, the specified
2325 * distance from its normal position.
2326 *
2327 * @attr ref android.R.styleable#TextView_shadowColor
2328 * @attr ref android.R.styleable#TextView_shadowDx
2329 * @attr ref android.R.styleable#TextView_shadowDy
2330 * @attr ref android.R.styleable#TextView_shadowRadius
2331 */
2332 public void setShadowLayer(float radius, float dx, float dy, int color) {
2333 mTextPaint.setShadowLayer(radius, dx, dy, color);
2334
2335 mShadowRadius = radius;
2336 mShadowDx = dx;
2337 mShadowDy = dy;
2338
2339 invalidate();
2340 }
2341
2342 /**
2343 * @return the base paint used for the text. Please use this only to
2344 * consult the Paint's properties and not to change them.
2345 */
2346 public TextPaint getPaint() {
2347 return mTextPaint;
2348 }
2349
2350 /**
2351 * Sets the autolink mask of the text. See {@link
2352 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
2353 * possible values.
2354 *
2355 * @attr ref android.R.styleable#TextView_autoLink
2356 */
2357 @android.view.RemotableViewMethod
2358 public final void setAutoLinkMask(int mask) {
2359 mAutoLinkMask = mask;
2360 }
2361
2362 /**
2363 * Sets whether the movement method will automatically be set to
2364 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2365 * set to nonzero and links are detected in {@link #setText}.
2366 * The default is true.
2367 *
2368 * @attr ref android.R.styleable#TextView_linksClickable
2369 */
2370 @android.view.RemotableViewMethod
2371 public final void setLinksClickable(boolean whether) {
2372 mLinksClickable = whether;
2373 }
2374
2375 /**
2376 * Returns whether the movement method will automatically be set to
2377 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
2378 * set to nonzero and links are detected in {@link #setText}.
2379 * The default is true.
2380 *
2381 * @attr ref android.R.styleable#TextView_linksClickable
2382 */
2383 public final boolean getLinksClickable() {
2384 return mLinksClickable;
2385 }
2386
2387 /**
Gilles Debunne550efbf2011-10-10 16:49:02 -07002388 * Sets whether the soft input method will be made visible when this
2389 * TextView gets focused. The default is true.
2390 *
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002391 * @attr ref android.R.styleable#TextView_softInputShownOnFocus
Gilles Debunne550efbf2011-10-10 16:49:02 -07002392 * @hide
2393 */
2394 @android.view.RemotableViewMethod
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002395 public final void setSoftInputShownOnFocus(boolean show) {
2396 mSoftInputShownOnFocus = show;
Gilles Debunne550efbf2011-10-10 16:49:02 -07002397 }
2398
2399 /**
2400 * Returns whether the soft input method will be made visible when this
2401 * TextView gets focused. The default is true.
2402 *
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002403 * @attr ref android.R.styleable#TextView_softInputShownOnFocus
Gilles Debunne550efbf2011-10-10 16:49:02 -07002404 * @hide
2405 */
Gilles Debunne0f4109e2011-10-19 11:26:21 -07002406 public final boolean getSoftInputShownOnFocus() {
2407 return mSoftInputShownOnFocus;
Gilles Debunne550efbf2011-10-10 16:49:02 -07002408 }
2409
2410 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002411 * Returns the list of URLSpans attached to the text
2412 * (by {@link Linkify} or otherwise) if any. You can call
2413 * {@link URLSpan#getURL} on them to find where they link to
2414 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
2415 * to find the region of the text they are attached to.
2416 */
2417 public URLSpan[] getUrls() {
2418 if (mText instanceof Spanned) {
2419 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
2420 } else {
2421 return new URLSpan[0];
2422 }
2423 }
2424
2425 /**
2426 * Sets the color of the hint text.
2427 *
2428 * @attr ref android.R.styleable#TextView_textColorHint
2429 */
2430 @android.view.RemotableViewMethod
2431 public final void setHintTextColor(int color) {
2432 mHintTextColor = ColorStateList.valueOf(color);
2433 updateTextColors();
2434 }
2435
2436 /**
2437 * Sets the color of the hint text.
2438 *
2439 * @attr ref android.R.styleable#TextView_textColorHint
2440 */
2441 public final void setHintTextColor(ColorStateList colors) {
2442 mHintTextColor = colors;
2443 updateTextColors();
2444 }
2445
2446 /**
2447 * <p>Return the color used to paint the hint text.</p>
2448 *
2449 * @return Returns the list of hint text colors.
2450 */
2451 public final ColorStateList getHintTextColors() {
2452 return mHintTextColor;
2453 }
2454
2455 /**
2456 * <p>Return the current color selected to paint the hint text.</p>
2457 *
2458 * @return Returns the current hint text color.
2459 */
2460 public final int getCurrentHintTextColor() {
2461 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2462 }
2463
2464 /**
2465 * Sets the color of links in the text.
2466 *
2467 * @attr ref android.R.styleable#TextView_textColorLink
2468 */
2469 @android.view.RemotableViewMethod
2470 public final void setLinkTextColor(int color) {
2471 mLinkTextColor = ColorStateList.valueOf(color);
2472 updateTextColors();
2473 }
2474
2475 /**
2476 * Sets the color of links in the text.
2477 *
2478 * @attr ref android.R.styleable#TextView_textColorLink
2479 */
2480 public final void setLinkTextColor(ColorStateList colors) {
2481 mLinkTextColor = colors;
2482 updateTextColors();
2483 }
2484
2485 /**
2486 * <p>Returns the color used to paint links in the text.</p>
2487 *
2488 * @return Returns the list of link text colors.
2489 */
2490 public final ColorStateList getLinkTextColors() {
2491 return mLinkTextColor;
2492 }
2493
2494 /**
2495 * Sets the horizontal alignment of the text and the
2496 * vertical gravity that will be used when there is extra space
2497 * in the TextView beyond what is required for the text itself.
2498 *
2499 * @see android.view.Gravity
2500 * @attr ref android.R.styleable#TextView_gravity
2501 */
2502 public void setGravity(int gravity) {
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002503 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -07002504 gravity |= Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002505 }
2506 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2507 gravity |= Gravity.TOP;
2508 }
2509
2510 boolean newLayout = false;
2511
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07002512 if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=
2513 (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002514 newLayout = true;
2515 }
2516
2517 if (gravity != mGravity) {
2518 invalidate();
Fabrice Di Meglio9f513842011-10-12 11:43:27 -07002519 mLayoutAlignment = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002520 }
2521
2522 mGravity = gravity;
2523
2524 if (mLayout != null && newLayout) {
2525 // XXX this is heavy-handed because no actual content changes.
2526 int want = mLayout.getWidth();
2527 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2528
2529 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2530 mRight - mLeft - getCompoundPaddingLeft() -
2531 getCompoundPaddingRight(), true);
2532 }
2533 }
2534
2535 /**
2536 * Returns the horizontal and vertical alignment of this TextView.
2537 *
2538 * @see android.view.Gravity
2539 * @attr ref android.R.styleable#TextView_gravity
2540 */
2541 public int getGravity() {
2542 return mGravity;
2543 }
2544
2545 /**
2546 * @return the flags on the Paint being used to display the text.
2547 * @see Paint#getFlags
2548 */
2549 public int getPaintFlags() {
2550 return mTextPaint.getFlags();
2551 }
2552
2553 /**
2554 * Sets flags on the Paint being used to display the text and
2555 * reflows the text if they are different from the old flags.
2556 * @see Paint#setFlags
2557 */
2558 @android.view.RemotableViewMethod
2559 public void setPaintFlags(int flags) {
2560 if (mTextPaint.getFlags() != flags) {
2561 mTextPaint.setFlags(flags);
2562
2563 if (mLayout != null) {
2564 nullLayouts();
2565 requestLayout();
2566 invalidate();
2567 }
2568 }
2569 }
2570
2571 /**
2572 * Sets whether the text should be allowed to be wider than the
2573 * View is. If false, it will be wrapped to the width of the View.
2574 *
2575 * @attr ref android.R.styleable#TextView_scrollHorizontally
2576 */
2577 public void setHorizontallyScrolling(boolean whether) {
Gilles Debunne22378292011-08-12 10:38:52 -07002578 if (mHorizontallyScrolling != whether) {
2579 mHorizontallyScrolling = whether;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002580
Gilles Debunne22378292011-08-12 10:38:52 -07002581 if (mLayout != null) {
2582 nullLayouts();
2583 requestLayout();
2584 invalidate();
2585 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002586 }
2587 }
2588
2589 /**
Gilles Debunnef2a02012011-10-27 11:10:14 -07002590 * Returns whether the text is allowed to be wider than the View is.
2591 * If false, the text will be wrapped to the width of the View.
2592 *
2593 * @attr ref android.R.styleable#TextView_scrollHorizontally
2594 * @hide
2595 */
2596 public boolean getHorizontallyScrolling() {
2597 return mHorizontallyScrolling;
2598 }
2599
2600 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002601 * Makes the TextView at least this many lines tall.
2602 *
2603 * Setting this value overrides any other (minimum) height setting. A single line TextView will
2604 * set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002605 *
2606 * @attr ref android.R.styleable#TextView_minLines
2607 */
2608 @android.view.RemotableViewMethod
2609 public void setMinLines(int minlines) {
2610 mMinimum = minlines;
2611 mMinMode = LINES;
2612
2613 requestLayout();
2614 invalidate();
2615 }
2616
2617 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002618 * Makes the TextView at least this many pixels tall.
2619 *
2620 * Setting this value overrides any other (minimum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002621 *
2622 * @attr ref android.R.styleable#TextView_minHeight
2623 */
2624 @android.view.RemotableViewMethod
2625 public void setMinHeight(int minHeight) {
2626 mMinimum = minHeight;
2627 mMinMode = PIXELS;
2628
2629 requestLayout();
2630 invalidate();
2631 }
2632
2633 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002634 * Makes the TextView at most this many lines tall.
2635 *
2636 * Setting this value overrides any other (maximum) height setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002637 *
2638 * @attr ref android.R.styleable#TextView_maxLines
2639 */
2640 @android.view.RemotableViewMethod
2641 public void setMaxLines(int maxlines) {
2642 mMaximum = maxlines;
2643 mMaxMode = LINES;
2644
2645 requestLayout();
2646 invalidate();
2647 }
2648
2649 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002650 * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the
2651 * {@link #setMaxLines(int)} method.
2652 *
2653 * Setting this value overrides any other (maximum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002654 *
2655 * @attr ref android.R.styleable#TextView_maxHeight
2656 */
2657 @android.view.RemotableViewMethod
2658 public void setMaxHeight(int maxHeight) {
2659 mMaximum = maxHeight;
2660 mMaxMode = PIXELS;
2661
2662 requestLayout();
2663 invalidate();
2664 }
2665
2666 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002667 * Makes the TextView exactly this many lines tall.
2668 *
2669 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2670 * height setting. A single line TextView will set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002671 *
2672 * @attr ref android.R.styleable#TextView_lines
2673 */
2674 @android.view.RemotableViewMethod
2675 public void setLines(int lines) {
2676 mMaximum = mMinimum = lines;
2677 mMaxMode = mMinMode = LINES;
2678
2679 requestLayout();
2680 invalidate();
2681 }
2682
2683 /**
2684 * Makes the TextView exactly this many pixels tall.
2685 * You could do the same thing by specifying this number in the
2686 * LayoutParams.
2687 *
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002688 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2689 * height setting.
2690 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002691 * @attr ref android.R.styleable#TextView_height
2692 */
2693 @android.view.RemotableViewMethod
2694 public void setHeight(int pixels) {
2695 mMaximum = mMinimum = pixels;
2696 mMaxMode = mMinMode = PIXELS;
2697
2698 requestLayout();
2699 invalidate();
2700 }
2701
2702 /**
2703 * Makes the TextView at least this many ems wide
2704 *
2705 * @attr ref android.R.styleable#TextView_minEms
2706 */
2707 @android.view.RemotableViewMethod
2708 public void setMinEms(int minems) {
2709 mMinWidth = minems;
2710 mMinWidthMode = EMS;
2711
2712 requestLayout();
2713 invalidate();
2714 }
2715
2716 /**
2717 * Makes the TextView at least this many pixels wide
2718 *
2719 * @attr ref android.R.styleable#TextView_minWidth
2720 */
2721 @android.view.RemotableViewMethod
2722 public void setMinWidth(int minpixels) {
2723 mMinWidth = minpixels;
2724 mMinWidthMode = PIXELS;
2725
2726 requestLayout();
2727 invalidate();
2728 }
2729
2730 /**
2731 * Makes the TextView at most this many ems wide
2732 *
2733 * @attr ref android.R.styleable#TextView_maxEms
2734 */
2735 @android.view.RemotableViewMethod
2736 public void setMaxEms(int maxems) {
2737 mMaxWidth = maxems;
2738 mMaxWidthMode = EMS;
2739
2740 requestLayout();
2741 invalidate();
2742 }
2743
2744 /**
2745 * Makes the TextView at most this many pixels wide
2746 *
2747 * @attr ref android.R.styleable#TextView_maxWidth
2748 */
2749 @android.view.RemotableViewMethod
2750 public void setMaxWidth(int maxpixels) {
2751 mMaxWidth = maxpixels;
2752 mMaxWidthMode = PIXELS;
2753
2754 requestLayout();
2755 invalidate();
2756 }
2757
2758 /**
2759 * Makes the TextView exactly this many ems wide
2760 *
2761 * @attr ref android.R.styleable#TextView_ems
2762 */
2763 @android.view.RemotableViewMethod
2764 public void setEms(int ems) {
2765 mMaxWidth = mMinWidth = ems;
2766 mMaxWidthMode = mMinWidthMode = EMS;
2767
2768 requestLayout();
2769 invalidate();
2770 }
2771
2772 /**
2773 * Makes the TextView exactly this many pixels wide.
2774 * You could do the same thing by specifying this number in the
2775 * LayoutParams.
2776 *
2777 * @attr ref android.R.styleable#TextView_width
2778 */
2779 @android.view.RemotableViewMethod
2780 public void setWidth(int pixels) {
2781 mMaxWidth = mMinWidth = pixels;
2782 mMaxWidthMode = mMinWidthMode = PIXELS;
2783
2784 requestLayout();
2785 invalidate();
2786 }
2787
2788
2789 /**
2790 * Sets line spacing for this TextView. Each line will have its height
2791 * multiplied by <code>mult</code> and have <code>add</code> added to it.
2792 *
2793 * @attr ref android.R.styleable#TextView_lineSpacingExtra
2794 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2795 */
2796 public void setLineSpacing(float add, float mult) {
Gilles Debunne22378292011-08-12 10:38:52 -07002797 if (mSpacingAdd != add || mSpacingMult != mult) {
2798 mSpacingAdd = add;
2799 mSpacingMult = mult;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002800
Gilles Debunne22378292011-08-12 10:38:52 -07002801 if (mLayout != null) {
2802 nullLayouts();
2803 requestLayout();
2804 invalidate();
2805 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002806 }
2807 }
2808
2809 /**
2810 * Convenience method: Append the specified text to the TextView's
2811 * display buffer, upgrading it to BufferType.EDITABLE if it was
2812 * not already editable.
2813 */
2814 public final void append(CharSequence text) {
2815 append(text, 0, text.length());
2816 }
2817
2818 /**
2819 * Convenience method: Append the specified text slice to the TextView's
2820 * display buffer, upgrading it to BufferType.EDITABLE if it was
2821 * not already editable.
2822 */
2823 public void append(CharSequence text, int start, int end) {
2824 if (!(mText instanceof Editable)) {
2825 setText(mText, BufferType.EDITABLE);
2826 }
2827
2828 ((Editable) mText).append(text, start, end);
2829 }
2830
2831 private void updateTextColors() {
2832 boolean inval = false;
2833 int color = mTextColor.getColorForState(getDrawableState(), 0);
2834 if (color != mCurTextColor) {
2835 mCurTextColor = color;
2836 inval = true;
2837 }
2838 if (mLinkTextColor != null) {
2839 color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2840 if (color != mTextPaint.linkColor) {
2841 mTextPaint.linkColor = color;
2842 inval = true;
2843 }
2844 }
2845 if (mHintTextColor != null) {
2846 color = mHintTextColor.getColorForState(getDrawableState(), 0);
2847 if (color != mCurHintTextColor && mText.length() == 0) {
2848 mCurHintTextColor = color;
2849 inval = true;
2850 }
2851 }
2852 if (inval) {
2853 invalidate();
2854 }
2855 }
2856
2857 @Override
2858 protected void drawableStateChanged() {
2859 super.drawableStateChanged();
2860 if (mTextColor != null && mTextColor.isStateful()
2861 || (mHintTextColor != null && mHintTextColor.isStateful())
2862 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2863 updateTextColors();
2864 }
2865
2866 final Drawables dr = mDrawables;
2867 if (dr != null) {
2868 int[] state = getDrawableState();
2869 if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2870 dr.mDrawableTop.setState(state);
2871 }
2872 if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2873 dr.mDrawableBottom.setState(state);
2874 }
2875 if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2876 dr.mDrawableLeft.setState(state);
2877 }
2878 if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2879 dr.mDrawableRight.setState(state);
2880 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07002881 if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) {
2882 dr.mDrawableStart.setState(state);
2883 }
2884 if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) {
2885 dr.mDrawableEnd.setState(state);
2886 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002887 }
2888 }
2889
2890 /**
2891 * User interface state that is stored by TextView for implementing
2892 * {@link View#onSaveInstanceState}.
2893 */
2894 public static class SavedState extends BaseSavedState {
2895 int selStart;
2896 int selEnd;
2897 CharSequence text;
2898 boolean frozenWithFocus;
The Android Open Source Project4df24232009-03-05 14:34:35 -08002899 CharSequence error;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002900
2901 SavedState(Parcelable superState) {
2902 super(superState);
2903 }
2904
2905 @Override
2906 public void writeToParcel(Parcel out, int flags) {
2907 super.writeToParcel(out, flags);
2908 out.writeInt(selStart);
2909 out.writeInt(selEnd);
2910 out.writeInt(frozenWithFocus ? 1 : 0);
2911 TextUtils.writeToParcel(text, out, flags);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002912
2913 if (error == null) {
2914 out.writeInt(0);
2915 } else {
2916 out.writeInt(1);
2917 TextUtils.writeToParcel(error, out, flags);
2918 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002919 }
2920
2921 @Override
2922 public String toString() {
2923 String str = "TextView.SavedState{"
2924 + Integer.toHexString(System.identityHashCode(this))
2925 + " start=" + selStart + " end=" + selEnd;
2926 if (text != null) {
2927 str += " text=" + text;
2928 }
2929 return str + "}";
2930 }
2931
Gilles Debunnee15b3582010-06-16 15:17:21 -07002932 @SuppressWarnings("hiding")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002933 public static final Parcelable.Creator<SavedState> CREATOR
2934 = new Parcelable.Creator<SavedState>() {
2935 public SavedState createFromParcel(Parcel in) {
2936 return new SavedState(in);
2937 }
2938
2939 public SavedState[] newArray(int size) {
2940 return new SavedState[size];
2941 }
2942 };
2943
2944 private SavedState(Parcel in) {
2945 super(in);
2946 selStart = in.readInt();
2947 selEnd = in.readInt();
2948 frozenWithFocus = (in.readInt() != 0);
2949 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002950
2951 if (in.readInt() != 0) {
2952 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2953 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002954 }
2955 }
2956
2957 @Override
2958 public Parcelable onSaveInstanceState() {
2959 Parcelable superState = super.onSaveInstanceState();
2960
2961 // Save state if we are forced to
2962 boolean save = mFreezesText;
2963 int start = 0;
2964 int end = 0;
2965
2966 if (mText != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07002967 start = getSelectionStart();
2968 end = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002969 if (start >= 0 || end >= 0) {
2970 // Or save state if there is a selection
2971 save = true;
2972 }
2973 }
2974
2975 if (save) {
2976 SavedState ss = new SavedState(superState);
2977 // XXX Should also save the current scroll position!
2978 ss.selStart = start;
2979 ss.selEnd = end;
2980
2981 if (mText instanceof Spanned) {
2982 /*
2983 * Calling setText() strips off any ChangeWatchers;
2984 * strip them now to avoid leaking references.
2985 * But do it to a copy so that if there are any
2986 * further changes to the text of this view, it
2987 * won't get into an inconsistent state.
2988 */
2989
2990 Spannable sp = new SpannableString(mText);
2991
Gilles Debunne176cd0d2011-09-29 16:37:27 -07002992 for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002993 sp.removeSpan(cw);
2994 }
2995
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07002996 removeMisspelledSpans(sp);
Gilles Debunneaa67eef2011-06-01 18:03:37 -07002997 sp.removeSpan(mSuggestionRangeSpan);
2998
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002999 ss.text = sp;
3000 } else {
3001 ss.text = mText.toString();
3002 }
3003
3004 if (isFocused() && start >= 0 && end >= 0) {
3005 ss.frozenWithFocus = true;
3006 }
3007
The Android Open Source Project4df24232009-03-05 14:34:35 -08003008 ss.error = mError;
3009
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003010 return ss;
3011 }
3012
3013 return superState;
3014 }
3015
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07003016 void removeMisspelledSpans(Spannable spannable) {
3017 SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
3018 SuggestionSpan.class);
3019 for (int i = 0; i < suggestionSpans.length; i++) {
3020 int flags = suggestionSpans[i].getFlags();
3021 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
3022 && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3023 spannable.removeSpan(suggestionSpans[i]);
3024 }
3025 }
3026 }
3027
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003028 @Override
3029 public void onRestoreInstanceState(Parcelable state) {
3030 if (!(state instanceof SavedState)) {
3031 super.onRestoreInstanceState(state);
3032 return;
3033 }
3034
3035 SavedState ss = (SavedState)state;
3036 super.onRestoreInstanceState(ss.getSuperState());
3037
3038 // XXX restore buffer type too, as well as lots of other stuff
3039 if (ss.text != null) {
3040 setText(ss.text);
3041 }
3042
3043 if (ss.selStart >= 0 && ss.selEnd >= 0) {
3044 if (mText instanceof Spannable) {
3045 int len = mText.length();
3046
3047 if (ss.selStart > len || ss.selEnd > len) {
3048 String restored = "";
3049
3050 if (ss.text != null) {
3051 restored = "(restored) ";
3052 }
3053
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003054 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003055 "/" + ss.selEnd + " out of range for " + restored +
3056 "text " + mText);
3057 } else {
3058 Selection.setSelection((Spannable) mText, ss.selStart,
3059 ss.selEnd);
3060
3061 if (ss.frozenWithFocus) {
3062 mFrozenWithFocus = true;
3063 }
3064 }
3065 }
3066 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003067
3068 if (ss.error != null) {
Romain Guy9bc9fa12009-07-21 16:57:29 -07003069 final CharSequence error = ss.error;
3070 // Display the error later, after the first layout pass
3071 post(new Runnable() {
3072 public void run() {
3073 setError(error);
3074 }
3075 });
The Android Open Source Project4df24232009-03-05 14:34:35 -08003076 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003077 }
3078
3079 /**
3080 * Control whether this text view saves its entire text contents when
3081 * freezing to an icicle, in addition to dynamic state such as cursor
3082 * position. By default this is false, not saving the text. Set to true
3083 * if the text in the text view is not being saved somewhere else in
3084 * persistent storage (such as in a content provider) so that if the
3085 * view is later thawed the user will not lose their data.
3086 *
3087 * @param freezesText Controls whether a frozen icicle should include the
3088 * entire text data: true to include it, false to not.
3089 *
3090 * @attr ref android.R.styleable#TextView_freezesText
3091 */
3092 @android.view.RemotableViewMethod
3093 public void setFreezesText(boolean freezesText) {
3094 mFreezesText = freezesText;
3095 }
3096
3097 /**
3098 * Return whether this text view is including its entire text contents
3099 * in frozen icicles.
3100 *
3101 * @return Returns true if text is included, false if it isn't.
3102 *
3103 * @see #setFreezesText
3104 */
3105 public boolean getFreezesText() {
3106 return mFreezesText;
3107 }
3108
3109 ///////////////////////////////////////////////////////////////////////////
3110
3111 /**
3112 * Sets the Factory used to create new Editables.
3113 */
3114 public final void setEditableFactory(Editable.Factory factory) {
3115 mEditableFactory = factory;
3116 setText(mText);
3117 }
3118
3119 /**
3120 * Sets the Factory used to create new Spannables.
3121 */
3122 public final void setSpannableFactory(Spannable.Factory factory) {
3123 mSpannableFactory = factory;
3124 setText(mText);
3125 }
3126
3127 /**
3128 * Sets the string value of the TextView. TextView <em>does not</em> accept
3129 * HTML-like formatting, which you can do with text strings in XML resource files.
3130 * To style your strings, attach android.text.style.* objects to a
3131 * {@link android.text.SpannableString SpannableString}, or see the
3132 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
Gilles Debunne21078e42011-08-02 10:22:35 -07003133 * Available Resource Types</a> documentation for an example of setting
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003134 * formatted text in the XML resource file.
3135 *
3136 * @attr ref android.R.styleable#TextView_text
3137 */
3138 @android.view.RemotableViewMethod
3139 public final void setText(CharSequence text) {
3140 setText(text, mBufferType);
3141 }
3142
3143 /**
3144 * Like {@link #setText(CharSequence)},
3145 * except that the cursor position (if any) is retained in the new text.
3146 *
3147 * @param text The new text to place in the text view.
3148 *
3149 * @see #setText(CharSequence)
3150 */
3151 @android.view.RemotableViewMethod
3152 public final void setTextKeepState(CharSequence text) {
3153 setTextKeepState(text, mBufferType);
3154 }
3155
3156 /**
3157 * Sets the text that this TextView is to display (see
3158 * {@link #setText(CharSequence)}) and also sets whether it is stored
3159 * in a styleable/spannable buffer and whether it is editable.
3160 *
3161 * @attr ref android.R.styleable#TextView_text
3162 * @attr ref android.R.styleable#TextView_bufferType
3163 */
3164 public void setText(CharSequence text, BufferType type) {
3165 setText(text, type, true, 0);
3166
3167 if (mCharWrapper != null) {
3168 mCharWrapper.mChars = null;
3169 }
3170 }
3171
3172 private void setText(CharSequence text, BufferType type,
3173 boolean notifyBefore, int oldlen) {
3174 if (text == null) {
3175 text = "";
3176 }
3177
Luca Zanoline0760452011-09-08 12:03:37 +01003178 // If suggestions are not enabled, remove the suggestion spans from the text
3179 if (!isSuggestionsEnabled()) {
3180 text = removeSuggestionSpans(text);
3181 }
3182
Romain Guy939151f2009-04-08 14:22:40 -07003183 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
3184
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003185 if (text instanceof Spanned &&
3186 ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
Adam Powell282e3772011-08-30 16:51:11 -07003187 if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
3188 setHorizontalFadingEdgeEnabled(true);
3189 mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
3190 } else {
3191 setHorizontalFadingEdgeEnabled(false);
3192 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
3193 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003194 setEllipsize(TextUtils.TruncateAt.MARQUEE);
3195 }
3196
3197 int n = mFilters.length;
3198 for (int i = 0; i < n; i++) {
3199 CharSequence out = mFilters[i].filter(text, 0, text.length(),
3200 EMPTY_SPANNED, 0, 0);
3201 if (out != null) {
3202 text = out;
3203 }
3204 }
3205
3206 if (notifyBefore) {
3207 if (mText != null) {
3208 oldlen = mText.length();
3209 sendBeforeTextChanged(mText, 0, oldlen, text.length());
3210 } else {
3211 sendBeforeTextChanged("", 0, 0, text.length());
3212 }
3213 }
3214
3215 boolean needEditableForNotification = false;
3216
3217 if (mListeners != null && mListeners.size() != 0) {
3218 needEditableForNotification = true;
3219 }
3220
Gilles Debunne6435a562011-08-04 21:22:30 -07003221 if (type == BufferType.EDITABLE || mInput != null || needEditableForNotification) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003222 Editable t = mEditableFactory.newEditable(text);
3223 text = t;
3224 setFilters(t, mFilters);
3225 InputMethodManager imm = InputMethodManager.peekInstance();
3226 if (imm != null) imm.restartInput(this);
3227 } else if (type == BufferType.SPANNABLE || mMovement != null) {
3228 text = mSpannableFactory.newSpannable(text);
3229 } else if (!(text instanceof CharWrapper)) {
3230 text = TextUtils.stringOrSpannedString(text);
3231 }
3232
3233 if (mAutoLinkMask != 0) {
3234 Spannable s2;
3235
3236 if (type == BufferType.EDITABLE || text instanceof Spannable) {
3237 s2 = (Spannable) text;
3238 } else {
3239 s2 = mSpannableFactory.newSpannable(text);
3240 }
3241
3242 if (Linkify.addLinks(s2, mAutoLinkMask)) {
3243 text = s2;
3244 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
3245
3246 /*
3247 * We must go ahead and set the text before changing the
3248 * movement method, because setMovementMethod() may call
3249 * setText() again to try to upgrade the buffer type.
3250 */
3251 mText = text;
3252
Gilles Debunnecbcb3452010-12-17 15:31:02 -08003253 // Do not change the movement method for text that support text selection as it
3254 // would prevent an arbitrary cursor displacement.
Gilles Debunnebb588da2011-07-11 18:26:19 -07003255 if (mLinksClickable && !textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003256 setMovementMethod(LinkMovementMethod.getInstance());
3257 }
3258 }
3259 }
3260
3261 mBufferType = type;
3262 mText = text;
3263
Adam Powell7f8f79a2011-07-07 18:35:54 -07003264 if (mTransformation == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003265 mTransformed = text;
Adam Powell7f8f79a2011-07-07 18:35:54 -07003266 } else {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003267 mTransformed = mTransformation.getTransformation(text, this);
Adam Powell7f8f79a2011-07-07 18:35:54 -07003268 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003269
3270 final int textLength = text.length();
3271
Adam Powell7f8f79a2011-07-07 18:35:54 -07003272 if (text instanceof Spannable && !mAllowTransformationLengthChange) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003273 Spannable sp = (Spannable) text;
3274
3275 // Remove any ChangeWatchers that might have come
3276 // from other TextViews.
3277 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
3278 final int count = watchers.length;
3279 for (int i = 0; i < count; i++)
3280 sp.removeSpan(watchers[i]);
3281
3282 if (mChangeWatcher == null)
3283 mChangeWatcher = new ChangeWatcher();
3284
3285 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
3286 (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
3287
3288 if (mInput != null) {
3289 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
3290 }
3291
3292 if (mTransformation != null) {
3293 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003294 }
3295
3296 if (mMovement != null) {
3297 mMovement.initialize(this, (Spannable) text);
3298
3299 /*
3300 * Initializing the movement method will have set the
3301 * selection, so reset mSelectionMoved to keep that from
3302 * interfering with the normal on-focus selection-setting.
3303 */
3304 mSelectionMoved = false;
3305 }
3306 }
3307
3308 if (mLayout != null) {
3309 checkForRelayout();
3310 }
3311
3312 sendOnTextChanged(text, 0, oldlen, textLength);
3313 onTextChanged(text, 0, oldlen, textLength);
3314
3315 if (needEditableForNotification) {
3316 sendAfterTextChanged((Editable) text);
3317 }
Gilles Debunne05336272010-07-09 20:13:45 -07003318
Gilles Debunnebaaace52010-10-01 15:47:13 -07003319 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
Gilles Debunnef788a9f2010-07-22 10:17:23 -07003320 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003321 }
3322
3323 /**
3324 * Sets the TextView to display the specified slice of the specified
3325 * char array. You must promise that you will not change the contents
3326 * of the array except for right before another call to setText(),
3327 * since the TextView has no way to know that the text
3328 * has changed and that it needs to invalidate and re-layout.
3329 */
3330 public final void setText(char[] text, int start, int len) {
3331 int oldlen = 0;
3332
3333 if (start < 0 || len < 0 || start + len > text.length) {
3334 throw new IndexOutOfBoundsException(start + ", " + len);
3335 }
3336
3337 /*
3338 * We must do the before-notification here ourselves because if
3339 * the old text is a CharWrapper we destroy it before calling
3340 * into the normal path.
3341 */
3342 if (mText != null) {
3343 oldlen = mText.length();
3344 sendBeforeTextChanged(mText, 0, oldlen, len);
3345 } else {
3346 sendBeforeTextChanged("", 0, 0, len);
3347 }
3348
3349 if (mCharWrapper == null) {
3350 mCharWrapper = new CharWrapper(text, start, len);
3351 } else {
3352 mCharWrapper.set(text, start, len);
3353 }
3354
3355 setText(mCharWrapper, mBufferType, false, oldlen);
3356 }
3357
Gilles Debunne3bca69b2011-05-23 18:20:22 -07003358 private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003359 private char[] mChars;
3360 private int mStart, mLength;
3361
3362 public CharWrapper(char[] chars, int start, int len) {
3363 mChars = chars;
3364 mStart = start;
3365 mLength = len;
3366 }
3367
3368 /* package */ void set(char[] chars, int start, int len) {
3369 mChars = chars;
3370 mStart = start;
3371 mLength = len;
3372 }
3373
3374 public int length() {
3375 return mLength;
3376 }
3377
3378 public char charAt(int off) {
3379 return mChars[off + mStart];
3380 }
3381
Gilles Debunnee15b3582010-06-16 15:17:21 -07003382 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003383 public String toString() {
3384 return new String(mChars, mStart, mLength);
3385 }
3386
3387 public CharSequence subSequence(int start, int end) {
3388 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3389 throw new IndexOutOfBoundsException(start + ", " + end);
3390 }
3391
3392 return new String(mChars, start + mStart, end - start);
3393 }
3394
3395 public void getChars(int start, int end, char[] buf, int off) {
3396 if (start < 0 || end < 0 || start > mLength || end > mLength) {
3397 throw new IndexOutOfBoundsException(start + ", " + end);
3398 }
3399
3400 System.arraycopy(mChars, start + mStart, buf, off, end - start);
3401 }
3402
3403 public void drawText(Canvas c, int start, int end,
3404 float x, float y, Paint p) {
3405 c.drawText(mChars, start + mStart, end - start, x, y, p);
3406 }
3407
Doug Feltf47d7402010-04-21 16:01:52 -07003408 public void drawTextRun(Canvas c, int start, int end,
Doug Felt0c702b82010-05-14 10:55:42 -07003409 int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
3410 int count = end - start;
3411 int contextCount = contextEnd - contextStart;
3412 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
3413 contextCount, x, y, flags, p);
Doug Feltf47d7402010-04-21 16:01:52 -07003414 }
3415
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003416 public float measureText(int start, int end, Paint p) {
3417 return p.measureText(mChars, start + mStart, end - start);
3418 }
3419
3420 public int getTextWidths(int start, int end, float[] widths, Paint p) {
3421 return p.getTextWidths(mChars, start + mStart, end - start, widths);
3422 }
Doug Felt0c702b82010-05-14 10:55:42 -07003423
3424 public float getTextRunAdvances(int start, int end, int contextStart,
3425 int contextEnd, int flags, float[] advances, int advancesIndex,
3426 Paint p) {
3427 int count = end - start;
3428 int contextCount = contextEnd - contextStart;
3429 return p.getTextRunAdvances(mChars, start + mStart, count,
3430 contextStart + mStart, contextCount, flags, advances,
3431 advancesIndex);
3432 }
3433
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003434 public float getTextRunAdvances(int start, int end, int contextStart,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003435 int contextEnd, int flags, float[] advances, int advancesIndex,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003436 Paint p, int reserved) {
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003437 int count = end - start;
3438 int contextCount = contextEnd - contextStart;
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003439 return p.getTextRunAdvances(mChars, start + mStart, count,
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003440 contextStart + mStart, contextCount, flags, advances,
Fabrice Di Meglio0a1413e2011-04-21 17:36:26 -07003441 advancesIndex, reserved);
Fabrice Di Meglioeee49c62011-03-24 17:21:23 -07003442 }
3443
Doug Felt0c702b82010-05-14 10:55:42 -07003444 public int getTextRunCursor(int contextStart, int contextEnd, int flags,
3445 int offset, int cursorOpt, Paint p) {
3446 int contextCount = contextEnd - contextStart;
3447 return p.getTextRunCursor(mChars, contextStart + mStart,
3448 contextCount, flags, offset + mStart, cursorOpt);
3449 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003450 }
3451
3452 /**
3453 * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3454 * except that the cursor position (if any) is retained in the new text.
3455 *
3456 * @see #setText(CharSequence, android.widget.TextView.BufferType)
3457 */
3458 public final void setTextKeepState(CharSequence text, BufferType type) {
3459 int start = getSelectionStart();
3460 int end = getSelectionEnd();
3461 int len = text.length();
3462
3463 setText(text, type);
3464
3465 if (start >= 0 || end >= 0) {
3466 if (mText instanceof Spannable) {
3467 Selection.setSelection((Spannable) mText,
3468 Math.max(0, Math.min(start, len)),
3469 Math.max(0, Math.min(end, len)));
3470 }
3471 }
3472 }
3473
3474 @android.view.RemotableViewMethod
3475 public final void setText(int resid) {
3476 setText(getContext().getResources().getText(resid));
3477 }
3478
3479 public final void setText(int resid, BufferType type) {
3480 setText(getContext().getResources().getText(resid), type);
3481 }
3482
3483 /**
3484 * Sets the text to be displayed when the text of the TextView is empty.
3485 * Null means to use the normal empty text. The hint does not currently
3486 * participate in determining the size of the view.
3487 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003488 * @attr ref android.R.styleable#TextView_hint
3489 */
3490 @android.view.RemotableViewMethod
3491 public final void setHint(CharSequence hint) {
3492 mHint = TextUtils.stringOrSpannedString(hint);
3493
3494 if (mLayout != null) {
3495 checkForRelayout();
3496 }
3497
Romain Guy4dc4f732009-06-19 15:16:40 -07003498 if (mText.length() == 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003499 invalidate();
Romain Guy4dc4f732009-06-19 15:16:40 -07003500 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003501 }
3502
3503 /**
3504 * Sets the text to be displayed when the text of the TextView is empty,
3505 * from a resource.
3506 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003507 * @attr ref android.R.styleable#TextView_hint
3508 */
3509 @android.view.RemotableViewMethod
3510 public final void setHint(int resid) {
3511 setHint(getContext().getResources().getText(resid));
3512 }
3513
3514 /**
3515 * Returns the hint that is displayed when the text of the TextView
3516 * is empty.
3517 *
3518 * @attr ref android.R.styleable#TextView_hint
3519 */
3520 @ViewDebug.CapturedViewProperty
3521 public CharSequence getHint() {
3522 return mHint;
3523 }
3524
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003525 private static boolean isMultilineInputType(int type) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003526 return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3527 (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3528 }
3529
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003530 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003531 * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3532 * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3533 * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
3534 * then a soft keyboard will not be displayed for this text view.
3535 *
3536 * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3537 * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3538 * type.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003539 *
3540 * @see #getInputType()
3541 * @see #setRawInputType(int)
3542 * @see android.text.InputType
3543 * @attr ref android.R.styleable#TextView_inputType
3544 */
3545 public void setInputType(int type) {
Bjorn Bringertad8da912009-09-17 10:47:35 +01003546 final boolean wasPassword = isPasswordInputType(mInputType);
3547 final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003548 setInputType(type, false);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003549 final boolean isPassword = isPasswordInputType(type);
3550 final boolean isVisiblePassword = isVisiblePasswordInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003551 boolean forceUpdate = false;
3552 if (isPassword) {
3553 setTransformationMethod(PasswordTransformationMethod.getInstance());
3554 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003555 } else if (isVisiblePassword) {
Amith Yamasania8c0edb2009-09-27 16:51:21 -07003556 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3557 forceUpdate = true;
3558 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003559 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003560 } else if (wasPassword || wasVisiblePassword) {
3561 // not in password mode, clean up typeface and transformation
3562 setTypefaceByIndex(-1, -1);
3563 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3564 forceUpdate = true;
3565 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003566 }
3567
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003568 boolean singleLine = !isMultilineInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003569
3570 // We need to update the single line mode if it has changed or we
3571 // were previously in password mode.
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003572 if (mSingleLine != singleLine || forceUpdate) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003573 // Change single line mode, but only change the transformation if
3574 // we are not in password mode.
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003575 applySingleLine(singleLine, !isPassword, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003576 }
3577
Luca Zanoline0760452011-09-08 12:03:37 +01003578 if (!isSuggestionsEnabled()) {
3579 mText = removeSuggestionSpans(mText);
3580 }
3581
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003582 InputMethodManager imm = InputMethodManager.peekInstance();
3583 if (imm != null) imm.restartInput(this);
3584 }
3585
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07003586 /**
3587 * It would be better to rely on the input type for everything. A password inputType should have
3588 * a password transformation. We should hence use isPasswordInputType instead of this method.
3589 *
3590 * We should:
3591 * - Call setInputType in setKeyListener instead of changing the input type directly (which
3592 * would install the correct transformation).
3593 * - Refuse the installation of a non-password transformation in setTransformation if the input
3594 * type is password.
3595 *
3596 * However, this is like this for legacy reasons and we cannot break existing apps. This method
3597 * is useful since it matches what the user can see (obfuscated text or not).
3598 *
3599 * @return true if the current transformation method is of the password type.
3600 */
3601 private boolean hasPasswordTransformationMethod() {
3602 return mTransformation instanceof PasswordTransformationMethod;
3603 }
3604
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003605 private static boolean isPasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003606 final int variation =
3607 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003608 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003609 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3610 || variation
Ken Wakasa82d731a2010-12-24 23:42:41 +09003611 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3612 || variation
3613 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003614 }
3615
Gilles Debunne3784a7f2011-07-15 13:49:38 -07003616 private static boolean isVisiblePasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003617 final int variation =
3618 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003619 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003620 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003621 }
3622
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003623 /**
3624 * Directly change the content type integer of the text view, without
3625 * modifying any other state.
3626 * @see #setInputType(int)
3627 * @see android.text.InputType
3628 * @attr ref android.R.styleable#TextView_inputType
3629 */
3630 public void setRawInputType(int type) {
3631 mInputType = type;
3632 }
3633
3634 private void setInputType(int type, boolean direct) {
3635 final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3636 KeyListener input;
3637 if (cls == EditorInfo.TYPE_CLASS_TEXT) {
Gilles Debunnee67b58a2010-08-31 15:55:31 -07003638 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003639 TextKeyListener.Capitalize cap;
3640 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3641 cap = TextKeyListener.Capitalize.CHARACTERS;
3642 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3643 cap = TextKeyListener.Capitalize.WORDS;
3644 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3645 cap = TextKeyListener.Capitalize.SENTENCES;
3646 } else {
3647 cap = TextKeyListener.Capitalize.NONE;
3648 }
3649 input = TextKeyListener.getInstance(autotext, cap);
3650 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3651 input = DigitsKeyListener.getInstance(
3652 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3653 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3654 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3655 switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3656 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3657 input = DateKeyListener.getInstance();
3658 break;
3659 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3660 input = TimeKeyListener.getInstance();
3661 break;
3662 default:
3663 input = DateTimeKeyListener.getInstance();
3664 break;
3665 }
3666 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3667 input = DialerKeyListener.getInstance();
3668 } else {
3669 input = TextKeyListener.getInstance();
3670 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003671 setRawInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003672 if (direct) mInput = input;
3673 else {
3674 setKeyListenerOnly(input);
3675 }
3676 }
3677
3678 /**
3679 * Get the type of the content.
3680 *
3681 * @see #setInputType(int)
3682 * @see android.text.InputType
3683 */
3684 public int getInputType() {
3685 return mInputType;
3686 }
3687
3688 /**
3689 * Change the editor type integer associated with the text view, which
3690 * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3691 * has focus.
3692 * @see #getImeOptions
3693 * @see android.view.inputmethod.EditorInfo
3694 * @attr ref android.R.styleable#TextView_imeOptions
3695 */
3696 public void setImeOptions(int imeOptions) {
3697 if (mInputContentType == null) {
3698 mInputContentType = new InputContentType();
3699 }
3700 mInputContentType.imeOptions = imeOptions;
3701 }
3702
3703 /**
3704 * Get the type of the IME editor.
3705 *
3706 * @see #setImeOptions(int)
3707 * @see android.view.inputmethod.EditorInfo
3708 */
3709 public int getImeOptions() {
3710 return mInputContentType != null
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003711 ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003712 }
3713
3714 /**
3715 * Change the custom IME action associated with the text view, which
3716 * will be reported to an IME with {@link EditorInfo#actionLabel}
3717 * and {@link EditorInfo#actionId} when it has focus.
3718 * @see #getImeActionLabel
3719 * @see #getImeActionId
3720 * @see android.view.inputmethod.EditorInfo
3721 * @attr ref android.R.styleable#TextView_imeActionLabel
3722 * @attr ref android.R.styleable#TextView_imeActionId
3723 */
3724 public void setImeActionLabel(CharSequence label, int actionId) {
3725 if (mInputContentType == null) {
3726 mInputContentType = new InputContentType();
3727 }
3728 mInputContentType.imeActionLabel = label;
3729 mInputContentType.imeActionId = actionId;
3730 }
3731
3732 /**
3733 * Get the IME action label previous set with {@link #setImeActionLabel}.
3734 *
3735 * @see #setImeActionLabel
3736 * @see android.view.inputmethod.EditorInfo
3737 */
3738 public CharSequence getImeActionLabel() {
3739 return mInputContentType != null
3740 ? mInputContentType.imeActionLabel : null;
3741 }
3742
3743 /**
3744 * Get the IME action ID previous set with {@link #setImeActionLabel}.
3745 *
3746 * @see #setImeActionLabel
3747 * @see android.view.inputmethod.EditorInfo
3748 */
3749 public int getImeActionId() {
3750 return mInputContentType != null
3751 ? mInputContentType.imeActionId : 0;
3752 }
3753
3754 /**
3755 * Set a special listener to be called when an action is performed
3756 * on the text view. This will be called when the enter key is pressed,
3757 * or when an action supplied to the IME is selected by the user. Setting
3758 * this means that the normal hard key event will not insert a newline
3759 * into the text view, even if it is multi-line; holding down the ALT
3760 * modifier will, however, allow the user to insert a newline character.
3761 */
3762 public void setOnEditorActionListener(OnEditorActionListener l) {
3763 if (mInputContentType == null) {
3764 mInputContentType = new InputContentType();
3765 }
3766 mInputContentType.onEditorActionListener = l;
3767 }
3768
3769 /**
3770 * Called when an attached input method calls
3771 * {@link InputConnection#performEditorAction(int)
3772 * InputConnection.performEditorAction()}
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003773 * for this text view. The default implementation will call your action
3774 * listener supplied to {@link #setOnEditorActionListener}, or perform
3775 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003776 * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3777 * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003778 * EditorInfo.IME_ACTION_DONE}.
3779 *
3780 * <p>For backwards compatibility, if no IME options have been set and the
3781 * text view would not normally advance focus on enter, then
3782 * the NEXT and DONE actions received here will be turned into an enter
3783 * key down/up pair to go through the normal key handling.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003784 *
3785 * @param actionCode The code of the action being performed.
3786 *
3787 * @see #setOnEditorActionListener
3788 */
3789 public void onEditorAction(int actionCode) {
3790 final InputContentType ict = mInputContentType;
3791 if (ict != null) {
3792 if (ict.onEditorActionListener != null) {
3793 if (ict.onEditorActionListener.onEditorAction(this,
3794 actionCode, null)) {
3795 return;
3796 }
3797 }
Gilles Debunne64794482011-11-30 15:45:28 -08003798
The Android Open Source Project4df24232009-03-05 14:34:35 -08003799 // This is the handling for some default action.
3800 // Note that for backwards compatibility we don't do this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003801 // default handling if explicit ime options have not been given,
The Android Open Source Project10592532009-03-18 17:39:46 -07003802 // instead turning this into the normal enter key codes that an
The Android Open Source Project4df24232009-03-05 14:34:35 -08003803 // app may be expecting.
3804 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003805 View v = focusSearch(FOCUS_FORWARD);
The Android Open Source Project4df24232009-03-05 14:34:35 -08003806 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003807 if (!v.requestFocus(FOCUS_FORWARD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003808 throw new IllegalStateException("focus search returned a view " +
3809 "that wasn't able to take focus!");
3810 }
3811 }
3812 return;
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003813
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003814 } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003815 View v = focusSearch(FOCUS_BACKWARD);
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003816 if (v != null) {
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003817 if (!v.requestFocus(FOCUS_BACKWARD)) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003818 throw new IllegalStateException("focus search returned a view " +
3819 "that wasn't able to take focus!");
3820 }
3821 }
3822 return;
3823
The Android Open Source Project4df24232009-03-05 14:34:35 -08003824 } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3825 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08003826 if (imm != null && imm.isActive(this)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003827 imm.hideSoftInputFromWindow(getWindowToken(), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003828 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003829 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003830 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003831 }
Svetoslav Ganova53efe92011-09-08 18:08:36 -07003832
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003833 Handler h = getHandler();
The Android Open Source Project10592532009-03-18 17:39:46 -07003834 if (h != null) {
3835 long eventTime = SystemClock.uptimeMillis();
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003836 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003837 new KeyEvent(eventTime, eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003838 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3839 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003840 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3841 | KeyEvent.FLAG_EDITOR_ACTION)));
Dianne Hackborn6dd005b2011-07-18 13:22:50 -07003842 h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
The Android Open Source Project10592532009-03-18 17:39:46 -07003843 new KeyEvent(SystemClock.uptimeMillis(), eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003844 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3845 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003846 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3847 | KeyEvent.FLAG_EDITOR_ACTION)));
3848 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003849 }
Gilles Debunne64794482011-11-30 15:45:28 -08003850
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003851 /**
3852 * Set the private content type of the text, which is the
3853 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3854 * field that will be filled in when creating an input connection.
3855 *
3856 * @see #getPrivateImeOptions()
3857 * @see EditorInfo#privateImeOptions
3858 * @attr ref android.R.styleable#TextView_privateImeOptions
3859 */
3860 public void setPrivateImeOptions(String type) {
3861 if (mInputContentType == null) mInputContentType = new InputContentType();
3862 mInputContentType.privateImeOptions = type;
3863 }
3864
3865 /**
3866 * Get the private type of the content.
3867 *
3868 * @see #setPrivateImeOptions(String)
3869 * @see EditorInfo#privateImeOptions
3870 */
3871 public String getPrivateImeOptions() {
3872 return mInputContentType != null
3873 ? mInputContentType.privateImeOptions : null;
3874 }
3875
3876 /**
3877 * Set the extra input data of the text, which is the
3878 * {@link EditorInfo#extras TextBoxAttribute.extras}
3879 * Bundle that will be filled in when creating an input connection. The
3880 * given integer is the resource ID of an XML resource holding an
3881 * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3882 *
3883 * @see #getInputExtras(boolean)
3884 * @see EditorInfo#extras
3885 * @attr ref android.R.styleable#TextView_editorExtras
3886 */
3887 public void setInputExtras(int xmlResId)
3888 throws XmlPullParserException, IOException {
3889 XmlResourceParser parser = getResources().getXml(xmlResId);
3890 if (mInputContentType == null) mInputContentType = new InputContentType();
3891 mInputContentType.extras = new Bundle();
3892 getResources().parseBundleExtras(parser, mInputContentType.extras);
3893 }
3894
3895 /**
3896 * Retrieve the input extras currently associated with the text view, which
3897 * can be viewed as well as modified.
3898 *
3899 * @param create If true, the extras will be created if they don't already
3900 * exist. Otherwise, null will be returned if none have been created.
Gilles Debunnee15b3582010-06-16 15:17:21 -07003901 * @see #setInputExtras(int)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003902 * @see EditorInfo#extras
3903 * @attr ref android.R.styleable#TextView_editorExtras
3904 */
3905 public Bundle getInputExtras(boolean create) {
3906 if (mInputContentType == null) {
3907 if (!create) return null;
3908 mInputContentType = new InputContentType();
3909 }
3910 if (mInputContentType.extras == null) {
3911 if (!create) return null;
3912 mInputContentType.extras = new Bundle();
3913 }
3914 return mInputContentType.extras;
3915 }
3916
3917 /**
3918 * Returns the error message that was set to be displayed with
3919 * {@link #setError}, or <code>null</code> if no error was set
3920 * or if it the error was cleared by the widget after user input.
3921 */
3922 public CharSequence getError() {
3923 return mError;
3924 }
3925
3926 /**
3927 * Sets the right-hand compound drawable of the TextView to the "error"
3928 * icon and sets an error message that will be displayed in a popup when
3929 * the TextView has focus. The icon and error message will be reset to
3930 * null when any key events cause changes to the TextView's text. If the
3931 * <code>error</code> is <code>null</code>, the error message and icon
3932 * will be cleared.
3933 */
3934 @android.view.RemotableViewMethod
3935 public void setError(CharSequence error) {
3936 if (error == null) {
3937 setError(null, null);
3938 } else {
3939 Drawable dr = getContext().getResources().
Gilles Debunnea85467b2011-01-19 16:53:31 -08003940 getDrawable(com.android.internal.R.drawable.indicator_input_error);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003941
3942 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3943 setError(error, dr);
3944 }
3945 }
3946
3947 /**
3948 * Sets the right-hand compound drawable of the TextView to the specified
3949 * icon and sets an error message that will be displayed in a popup when
3950 * the TextView has focus. The icon and error message will be reset to
3951 * null when any key events cause changes to the TextView's text. The
3952 * drawable must already have had {@link Drawable#setBounds} set on it.
3953 * If the <code>error</code> is <code>null</code>, the error message will
3954 * be cleared (and you should provide a <code>null</code> icon as well).
3955 */
3956 public void setError(CharSequence error, Drawable icon) {
3957 error = TextUtils.stringOrSpannedString(error);
3958
3959 mError = error;
3960 mErrorWasChanged = true;
3961 final Drawables dr = mDrawables;
3962 if (dr != null) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07003963 switch (getResolvedLayoutDirection()) {
3964 default:
3965 case LAYOUT_DIRECTION_LTR:
3966 setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
3967 dr.mDrawableBottom);
3968 break;
3969 case LAYOUT_DIRECTION_RTL:
3970 setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
3971 dr.mDrawableBottom);
3972 break;
3973 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003974 } else {
3975 setCompoundDrawables(null, null, icon, null);
3976 }
3977
3978 if (error == null) {
3979 if (mPopup != null) {
3980 if (mPopup.isShowing()) {
3981 mPopup.dismiss();
3982 }
3983
3984 mPopup = null;
3985 }
3986 } else {
3987 if (isFocused()) {
3988 showError();
3989 }
3990 }
3991 }
3992
3993 private void showError() {
3994 if (getWindowToken() == null) {
3995 mShowErrorAfterAttach = true;
3996 return;
3997 }
3998
3999 if (mPopup == null) {
4000 LayoutInflater inflater = LayoutInflater.from(getContext());
Gilles Debunnea85467b2011-01-19 16:53:31 -08004001 final TextView err = (TextView) inflater.inflate(
4002 com.android.internal.R.layout.textview_hint, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004003
Romain Guy9bc9fa12009-07-21 16:57:29 -07004004 final float scale = getResources().getDisplayMetrics().density;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004005 mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004006 mPopup.setFocusable(false);
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -07004007 // The user is entering text, so the input method is needed. We
4008 // don't want the popup to be displayed on top of it.
4009 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004010 }
4011
4012 TextView tv = (TextView) mPopup.getContentView();
4013 chooseSize(mPopup, mError, tv);
4014 tv.setText(mError);
4015
4016 mPopup.showAsDropDown(this, getErrorX(), getErrorY());
The Android Open Source Project10592532009-03-18 17:39:46 -07004017 mPopup.fixDirection(mPopup.isAboveAnchor());
4018 }
4019
4020 private static class ErrorPopup extends PopupWindow {
4021 private boolean mAbove = false;
Gilles Debunnee15b3582010-06-16 15:17:21 -07004022 private final TextView mView;
Gilles Debunne5f059e42011-01-12 17:49:12 -08004023 private int mPopupInlineErrorBackgroundId = 0;
4024 private int mPopupInlineErrorAboveBackgroundId = 0;
The Android Open Source Project10592532009-03-18 17:39:46 -07004025
4026 ErrorPopup(TextView v, int width, int height) {
4027 super(v, width, height);
4028 mView = v;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004029 // Make sure the TextView has a background set as it will be used the first time it is
4030 // shown and positionned. Initialized with below background, which should have
4031 // dimensions identical to the above version for this to work (and is more likely).
4032 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4033 com.android.internal.R.styleable.Theme_errorMessageBackground);
4034 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
The Android Open Source Project10592532009-03-18 17:39:46 -07004035 }
4036
4037 void fixDirection(boolean above) {
4038 mAbove = above;
4039
4040 if (above) {
Gilles Debunne5f059e42011-01-12 17:49:12 -08004041 mPopupInlineErrorAboveBackgroundId =
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004042 getResourceId(mPopupInlineErrorAboveBackgroundId,
4043 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07004044 } else {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004045 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4046 com.android.internal.R.styleable.Theme_errorMessageBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07004047 }
Gilles Debunne5f059e42011-01-12 17:49:12 -08004048
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004049 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4050 mPopupInlineErrorBackgroundId);
Gilles Debunne5f059e42011-01-12 17:49:12 -08004051 }
4052
4053 private int getResourceId(int currentId, int index) {
4054 if (currentId == 0) {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004055 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4056 R.styleable.Theme);
Gilles Debunne5f059e42011-01-12 17:49:12 -08004057 currentId = styledAttributes.getResourceId(index, 0);
4058 styledAttributes.recycle();
4059 }
4060 return currentId;
The Android Open Source Project10592532009-03-18 17:39:46 -07004061 }
4062
4063 @Override
4064 public void update(int x, int y, int w, int h, boolean force) {
4065 super.update(x, y, w, h, force);
4066
4067 boolean above = isAboveAnchor();
4068 if (above != mAbove) {
4069 fixDirection(above);
4070 }
4071 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004072 }
4073
4074 /**
4075 * Returns the Y offset to make the pointy top of the error point
4076 * at the middle of the error icon.
4077 */
4078 private int getErrorX() {
4079 /*
4080 * The "25" is the distance between the point and the right edge
4081 * of the background
4082 */
Romain Guy9bc9fa12009-07-21 16:57:29 -07004083 final float scale = getResources().getDisplayMetrics().density;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004084
4085 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004086 return getWidth() - mPopup.getWidth() - getPaddingRight() -
4087 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004088 }
4089
4090 /**
4091 * Returns the Y offset to make the pointy top of the error point
4092 * at the bottom of the error icon.
4093 */
4094 private int getErrorY() {
4095 /*
4096 * Compound, not extended, because the icon is not clipped
4097 * if the text height is smaller.
4098 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004099 final int compoundPaddingTop = getCompoundPaddingTop();
4100 int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004101
4102 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004103 int icontop = compoundPaddingTop +
4104 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004105
4106 /*
4107 * The "2" is the distance between the point and the top edge
4108 * of the background.
4109 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08004110 final float scale = getResources().getDisplayMetrics().density;
4111 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
4112 (int) (2 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004113 }
4114
4115 private void hideError() {
4116 if (mPopup != null) {
4117 if (mPopup.isShowing()) {
4118 mPopup.dismiss();
4119 }
4120 }
4121
4122 mShowErrorAfterAttach = false;
4123 }
4124
4125 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
4126 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
4127 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
4128
Fabrice Di Meglioe4231462011-09-08 18:15:50 -07004129 int defaultWidthInPixels = getResources().getDimensionPixelSize(
4130 com.android.internal.R.dimen.textview_error_popup_default_width);
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004131 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004132 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
4133 float max = 0;
4134 for (int i = 0; i < l.getLineCount(); i++) {
4135 max = Math.max(max, l.getLineWidth(i));
4136 }
4137
4138 /*
Fabrice Di Meglio33438be2011-09-08 15:05:23 -07004139 * Now set the popup size to be big enough for the text plus the border capped
4140 * to DEFAULT_MAX_POPUP_WIDTH
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004141 */
4142 pop.setWidth(wid + (int) Math.ceil(max));
4143 pop.setHeight(ht + l.getHeight());
4144 }
4145
4146
4147 @Override
4148 protected boolean setFrame(int l, int t, int r, int b) {
4149 boolean result = super.setFrame(l, t, r, b);
4150
4151 if (mPopup != null) {
4152 TextView tv = (TextView) mPopup.getContentView();
4153 chooseSize(mPopup, mError, tv);
Eric Fischerfa0d2532009-09-17 17:01:59 -07004154 mPopup.update(this, getErrorX(), getErrorY(),
4155 mPopup.getWidth(), mPopup.getHeight());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004156 }
4157
Romain Guy986003d2009-03-25 17:42:35 -07004158 restartMarqueeIfNeeded();
4159
4160 return result;
4161 }
4162
4163 private void restartMarqueeIfNeeded() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004164 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4165 mRestartMarquee = false;
4166 startMarquee();
4167 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004168 }
4169
4170 /**
4171 * Sets the list of input filters that will be used if the buffer is
4172 * Editable. Has no effect otherwise.
4173 *
4174 * @attr ref android.R.styleable#TextView_maxLength
4175 */
4176 public void setFilters(InputFilter[] filters) {
4177 if (filters == null) {
4178 throw new IllegalArgumentException();
4179 }
4180
4181 mFilters = filters;
4182
4183 if (mText instanceof Editable) {
4184 setFilters((Editable) mText, filters);
4185 }
4186 }
4187
4188 /**
4189 * Sets the list of input filters on the specified Editable,
4190 * and includes mInput in the list if it is an InputFilter.
4191 */
4192 private void setFilters(Editable e, InputFilter[] filters) {
4193 if (mInput instanceof InputFilter) {
4194 InputFilter[] nf = new InputFilter[filters.length + 1];
4195
4196 System.arraycopy(filters, 0, nf, 0, filters.length);
4197 nf[filters.length] = (InputFilter) mInput;
4198
4199 e.setFilters(nf);
4200 } else {
4201 e.setFilters(filters);
4202 }
4203 }
4204
4205 /**
4206 * Returns the current list of input filters.
4207 */
4208 public InputFilter[] getFilters() {
4209 return mFilters;
4210 }
4211
4212 /////////////////////////////////////////////////////////////////////////
4213
4214 private int getVerticalOffset(boolean forceNormal) {
4215 int voffset = 0;
4216 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4217
4218 Layout l = mLayout;
4219 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4220 l = mHintLayout;
4221 }
4222
4223 if (gravity != Gravity.TOP) {
4224 int boxht;
4225
4226 if (l == mHintLayout) {
4227 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4228 getCompoundPaddingBottom();
4229 } else {
4230 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4231 getExtendedPaddingBottom();
4232 }
4233 int textht = l.getHeight();
4234
4235 if (textht < boxht) {
4236 if (gravity == Gravity.BOTTOM)
4237 voffset = boxht - textht;
4238 else // (gravity == Gravity.CENTER_VERTICAL)
4239 voffset = (boxht - textht) >> 1;
4240 }
4241 }
4242 return voffset;
4243 }
4244
4245 private int getBottomVerticalOffset(boolean forceNormal) {
4246 int voffset = 0;
4247 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
4248
4249 Layout l = mLayout;
4250 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
4251 l = mHintLayout;
4252 }
4253
4254 if (gravity != Gravity.BOTTOM) {
4255 int boxht;
4256
4257 if (l == mHintLayout) {
4258 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
4259 getCompoundPaddingBottom();
4260 } else {
4261 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
4262 getExtendedPaddingBottom();
4263 }
4264 int textht = l.getHeight();
4265
4266 if (textht < boxht) {
4267 if (gravity == Gravity.TOP)
4268 voffset = boxht - textht;
4269 else // (gravity == Gravity.CENTER_VERTICAL)
4270 voffset = (boxht - textht) >> 1;
4271 }
4272 }
4273 return voffset;
4274 }
4275
4276 private void invalidateCursorPath() {
4277 if (mHighlightPathBogus) {
4278 invalidateCursor();
4279 } else {
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004280 final int horizontalPadding = getCompoundPaddingLeft();
4281 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004282
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004283 if (mCursorCount == 0) {
4284 synchronized (sTempRect) {
4285 /*
4286 * The reason for this concern about the thickness of the
4287 * cursor and doing the floor/ceil on the coordinates is that
4288 * some EditTexts (notably textfields in the Browser) have
4289 * anti-aliased text where not all the characters are
4290 * necessarily at integer-multiple locations. This should
4291 * make sure the entire cursor gets invalidated instead of
4292 * sometimes missing half a pixel.
4293 */
4294 float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
4295 if (thick < 1.0f) {
4296 thick = 1.0f;
4297 }
4298
4299 thick /= 2.0f;
4300
4301 mHighlightPath.computeBounds(sTempRect, false);
4302
4303 invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
4304 (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
4305 (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
4306 (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004307 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004308 } else {
4309 for (int i = 0; i < mCursorCount; i++) {
4310 Rect bounds = mCursorDrawable[i].getBounds();
4311 invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
4312 bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
4313 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004314 }
4315 }
4316 }
4317
4318 private void invalidateCursor() {
Gilles Debunne05336272010-07-09 20:13:45 -07004319 int where = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004320
4321 invalidateCursor(where, where, where);
4322 }
4323
4324 private void invalidateCursor(int a, int b, int c) {
Gilles Debunne8615ac92011-11-29 15:25:03 -08004325 if (a >= 0 || b >= 0 || c >= 0) {
4326 int start = Math.min(Math.min(a, b), c);
4327 int end = Math.max(Math.max(a, b), c);
Gilles Debunne961ebb92011-12-12 10:16:04 -08004328 invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
Gilles Debunne8615ac92011-11-29 15:25:03 -08004329 }
4330 }
4331
4332 /**
4333 * Invalidates the region of text enclosed between the start and end text offsets.
4334 *
4335 * @hide
4336 */
Gilles Debunne961ebb92011-12-12 10:16:04 -08004337 void invalidateRegion(int start, int end, boolean invalidateCursor) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004338 if (mLayout == null) {
4339 invalidate();
4340 } else {
Gilles Debunne8615ac92011-11-29 15:25:03 -08004341 int lineStart = mLayout.getLineForOffset(start);
4342 int top = mLayout.getLineTop(lineStart);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004343
4344 // This is ridiculous, but the descent from the line above
4345 // can hang down into the line we really want to redraw,
4346 // so we have to invalidate part of the line above to make
4347 // sure everything that needs to be redrawn really is.
4348 // (But not the whole line above, because that would cause
4349 // the same problem with the descenders on the line above it!)
Gilles Debunne8615ac92011-11-29 15:25:03 -08004350 if (lineStart > 0) {
4351 top -= mLayout.getLineDescent(lineStart - 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004352 }
4353
Gilles Debunne8615ac92011-11-29 15:25:03 -08004354 int lineEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004355
Gilles Debunne8615ac92011-11-29 15:25:03 -08004356 if (start == end)
4357 lineEnd = lineStart;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004358 else
Gilles Debunne8615ac92011-11-29 15:25:03 -08004359 lineEnd = mLayout.getLineForOffset(end);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004360
Gilles Debunne8615ac92011-11-29 15:25:03 -08004361 int bottom = mLayout.getLineBottom(lineEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004362
Gilles Debunne961ebb92011-12-12 10:16:04 -08004363 if (invalidateCursor) {
4364 for (int i = 0; i < mCursorCount; i++) {
4365 Rect bounds = mCursorDrawable[i].getBounds();
4366 top = Math.min(top, bounds.top);
4367 bottom = Math.max(bottom, bounds.bottom);
4368 }
4369 }
4370
Gilles Debunne8615ac92011-11-29 15:25:03 -08004371 final int compoundPaddingLeft = getCompoundPaddingLeft();
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004372 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
Gilles Debunne8615ac92011-11-29 15:25:03 -08004373
4374 int left, right;
Gilles Debunne961ebb92011-12-12 10:16:04 -08004375 if (lineStart == lineEnd && !invalidateCursor) {
Gilles Debunne8615ac92011-11-29 15:25:03 -08004376 left = (int) mLayout.getPrimaryHorizontal(start);
4377 right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
4378 left += compoundPaddingLeft;
4379 right += compoundPaddingLeft;
4380 } else {
4381 // Rectangle bounding box when the region spans several lines
4382 left = compoundPaddingLeft;
4383 right = getWidth() - getCompoundPaddingRight();
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004384 }
4385
Gilles Debunne8615ac92011-11-29 15:25:03 -08004386 invalidate(mScrollX + left, verticalPadding + top,
4387 mScrollX + right, verticalPadding + bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004388 }
4389 }
4390
4391 private void registerForPreDraw() {
4392 final ViewTreeObserver observer = getViewTreeObserver();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004393
4394 if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
4395 observer.addOnPreDrawListener(this);
4396 mPreDrawState = PREDRAW_PENDING;
4397 } else if (mPreDrawState == PREDRAW_DONE) {
4398 mPreDrawState = PREDRAW_PENDING;
4399 }
4400
4401 // else state is PREDRAW_PENDING, so keep waiting.
4402 }
4403
4404 /**
4405 * {@inheritDoc}
4406 */
4407 public boolean onPreDraw() {
4408 if (mPreDrawState != PREDRAW_PENDING) {
4409 return true;
4410 }
4411
4412 if (mLayout == null) {
4413 assumeLayout();
4414 }
4415
4416 boolean changed = false;
4417
4418 if (mMovement != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07004419 /* This code also provides auto-scrolling when a cursor is moved using a
4420 * CursorController (insertion point or selection limits).
4421 * For selection, ensure start or end is visible depending on controller's state.
4422 */
4423 int curs = getSelectionEnd();
Gilles Debunnee587d832010-11-23 20:20:11 -08004424 // Do not create the controller if it is not already created.
4425 if (mSelectionModifierCursorController != null &&
4426 mSelectionModifierCursorController.isSelectionStartDragged()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07004427 curs = getSelectionStart();
Gilles Debunne05336272010-07-09 20:13:45 -07004428 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004429
4430 /*
4431 * TODO: This should really only keep the end in view if
4432 * it already was before the text changed. I'm not sure
4433 * of a good way to tell from here if it was.
4434 */
4435 if (curs < 0 &&
4436 (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
4437 curs = mText.length();
4438 }
4439
4440 if (curs >= 0) {
4441 changed = bringPointIntoView(curs);
4442 }
4443 } else {
4444 changed = bringTextIntoView();
4445 }
4446
Gilles Debunne64e54a62010-09-07 19:07:17 -07004447 // This has to be checked here since:
4448 // - onFocusChanged cannot start it when focus is given to a view with selected text (after
4449 // a screen rotation) since layout is not yet initialized at that point.
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08004450 if (mCreatedWithASelection) {
4451 startSelectionActionMode();
4452 mCreatedWithASelection = false;
4453 }
4454
4455 // Phone specific code (there is no ExtractEditText on tablets).
4456 // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
4457 // not be set. Do the test here instead.
4458 if (this instanceof ExtractEditText && hasSelection()) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004459 startSelectionActionMode();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07004460 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07004461
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004462 mPreDrawState = PREDRAW_DONE;
4463 return !changed;
4464 }
4465
4466 @Override
4467 protected void onAttachedToWindow() {
4468 super.onAttachedToWindow();
4469
4470 mTemporaryDetach = false;
4471
4472 if (mShowErrorAfterAttach) {
4473 showError();
4474 mShowErrorAfterAttach = false;
4475 }
Adam Powell624380a2010-10-02 18:12:02 -07004476
4477 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004478 // No need to create the controller.
4479 // The get method will add the listener on controller creation.
4480 if (mInsertionPointCursorController != null) {
4481 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4482 }
4483 if (mSelectionModifierCursorController != null) {
4484 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell624380a2010-10-02 18:12:02 -07004485 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004486
4487 // Resolve drawables as the layout direction has been resolved
4488 resolveDrawables();
Gilles Debunnec115fa02011-12-07 13:38:31 -08004489
4490 updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004491 }
4492
4493 @Override
4494 protected void onDetachedFromWindow() {
4495 super.onDetachedFromWindow();
4496
Adam Powell624380a2010-10-02 18:12:02 -07004497 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004498 if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4499 observer.removeOnPreDrawListener(this);
4500 mPreDrawState = PREDRAW_NOT_REGISTERED;
4501 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004502
4503 if (mError != null) {
4504 hideError();
4505 }
Adam Powellba0a2c32010-09-28 17:41:23 -07004506
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004507 if (mBlink != null) {
Gilles Debunne3d010062011-02-18 14:16:41 -08004508 mBlink.removeCallbacks(mBlink);
Gilles Debunnef48e83b2010-12-06 18:36:08 -08004509 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08004510
4511 if (mInsertionPointCursorController != null) {
4512 mInsertionPointCursorController.onDetached();
4513 }
4514
4515 if (mSelectionModifierCursorController != null) {
4516 mSelectionModifierCursorController.onDetached();
4517 }
4518
Adam Powellba0a2c32010-09-28 17:41:23 -07004519 hideControllers();
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004520
4521 resetResolvedDrawables();
Gilles Debunne186aaf92011-09-16 14:26:12 -07004522
4523 if (mSpellChecker != null) {
4524 mSpellChecker.closeSession();
Gilles Debunne186aaf92011-09-16 14:26:12 -07004525 // Forces the creation of a new SpellChecker next time this window is created.
4526 // Will handle the cases where the settings has been changed in the meantime.
4527 mSpellChecker = null;
4528 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004529 }
4530
4531 @Override
4532 protected boolean isPaddingOffsetRequired() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004533 return mShadowRadius != 0 || mDrawables != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004534 }
4535
4536 @Override
4537 protected int getLeftPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004538 return getCompoundPaddingLeft() - mPaddingLeft +
4539 (int) Math.min(0, mShadowDx - mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004540 }
4541
4542 @Override
4543 protected int getTopPaddingOffset() {
4544 return (int) Math.min(0, mShadowDy - mShadowRadius);
4545 }
4546
4547 @Override
4548 protected int getBottomPaddingOffset() {
4549 return (int) Math.max(0, mShadowDy + mShadowRadius);
4550 }
4551
4552 @Override
4553 protected int getRightPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004554 return -(getCompoundPaddingRight() - mPaddingRight) +
4555 (int) Math.max(0, mShadowDx + mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004556 }
4557
4558 @Override
4559 protected boolean verifyDrawable(Drawable who) {
4560 final boolean verified = super.verifyDrawable(who);
4561 if (!verified && mDrawables != null) {
4562 return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004563 who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom ||
4564 who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004565 }
4566 return verified;
4567 }
4568
4569 @Override
Dianne Hackborne2136772010-11-04 15:08:59 -07004570 public void jumpDrawablesToCurrentState() {
4571 super.jumpDrawablesToCurrentState();
4572 if (mDrawables != null) {
4573 if (mDrawables.mDrawableLeft != null) {
4574 mDrawables.mDrawableLeft.jumpToCurrentState();
4575 }
4576 if (mDrawables.mDrawableTop != null) {
4577 mDrawables.mDrawableTop.jumpToCurrentState();
4578 }
4579 if (mDrawables.mDrawableRight != null) {
4580 mDrawables.mDrawableRight.jumpToCurrentState();
4581 }
4582 if (mDrawables.mDrawableBottom != null) {
4583 mDrawables.mDrawableBottom.jumpToCurrentState();
4584 }
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004585 if (mDrawables.mDrawableStart != null) {
4586 mDrawables.mDrawableStart.jumpToCurrentState();
4587 }
4588 if (mDrawables.mDrawableEnd != null) {
4589 mDrawables.mDrawableEnd.jumpToCurrentState();
4590 }
Dianne Hackborne2136772010-11-04 15:08:59 -07004591 }
4592 }
4593
4594 @Override
Romain Guy3c77d392009-05-20 11:26:50 -07004595 public void invalidateDrawable(Drawable drawable) {
4596 if (verifyDrawable(drawable)) {
4597 final Rect dirty = drawable.getBounds();
4598 int scrollX = mScrollX;
4599 int scrollY = mScrollY;
4600
4601 // IMPORTANT: The coordinates below are based on the coordinates computed
4602 // for each compound drawable in onDraw(). Make sure to update each section
4603 // accordingly.
4604 final TextView.Drawables drawables = mDrawables;
Romain Guya6cd4e02009-05-20 15:09:21 -07004605 if (drawables != null) {
4606 if (drawable == drawables.mDrawableLeft) {
4607 final int compoundPaddingTop = getCompoundPaddingTop();
4608 final int compoundPaddingBottom = getCompoundPaddingBottom();
4609 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004610
Romain Guya6cd4e02009-05-20 15:09:21 -07004611 scrollX += mPaddingLeft;
4612 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4613 } else if (drawable == drawables.mDrawableRight) {
4614 final int compoundPaddingTop = getCompoundPaddingTop();
4615 final int compoundPaddingBottom = getCompoundPaddingBottom();
4616 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004617
Romain Guya6cd4e02009-05-20 15:09:21 -07004618 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4619 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4620 } else if (drawable == drawables.mDrawableTop) {
4621 final int compoundPaddingLeft = getCompoundPaddingLeft();
4622 final int compoundPaddingRight = getCompoundPaddingRight();
4623 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004624
Romain Guya6cd4e02009-05-20 15:09:21 -07004625 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4626 scrollY += mPaddingTop;
4627 } else if (drawable == drawables.mDrawableBottom) {
4628 final int compoundPaddingLeft = getCompoundPaddingLeft();
4629 final int compoundPaddingRight = getCompoundPaddingRight();
4630 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004631
Romain Guya6cd4e02009-05-20 15:09:21 -07004632 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4633 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4634 }
Romain Guy3c77d392009-05-20 11:26:50 -07004635 }
4636
4637 invalidate(dirty.left + scrollX, dirty.top + scrollY,
4638 dirty.right + scrollX, dirty.bottom + scrollY);
4639 }
4640 }
4641
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004642 /**
4643 * @hide
4644 */
Romain Guy3c77d392009-05-20 11:26:50 -07004645 @Override
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004646 public int getResolvedLayoutDirection(Drawable who) {
4647 if (who == null) return View.LAYOUT_DIRECTION_LTR;
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004648 if (mDrawables != null) {
4649 final Drawables drawables = mDrawables;
4650 if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004651 who == drawables.mDrawableTop || who == drawables.mDrawableBottom ||
4652 who == drawables.mDrawableStart || who == drawables.mDrawableEnd) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004653 return getResolvedLayoutDirection();
Fabrice Di Meglio83fa41b2011-05-31 16:12:38 -07004654 }
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004655 }
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004656 return super.getResolvedLayoutDirection(who);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004657 }
4658
4659 @Override
Romain Guyc4d8eb62010-08-18 20:48:33 -07004660 protected boolean onSetAlpha(int alpha) {
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004661 // Alpha is supported if and only if the drawing can be done in one pass.
4662 // TODO text with spans with a background color currently do not respect this alpha.
4663 if (getBackground() == null) {
Romain Guyc4d8eb62010-08-18 20:48:33 -07004664 mCurrentAlpha = alpha;
4665 final Drawables dr = mDrawables;
4666 if (dr != null) {
Michael Jurka406f0522010-09-15 18:48:48 -07004667 if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4668 if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4669 if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4670 if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -07004671 if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha);
4672 if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha);
Romain Guyc4d8eb62010-08-18 20:48:33 -07004673 }
4674 return true;
4675 }
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004676
4677 mCurrentAlpha = 255;
Romain Guyc4d8eb62010-08-18 20:48:33 -07004678 return false;
4679 }
4680
Gilles Debunne86b9c782010-11-11 10:43:48 -08004681 /**
4682 * When a TextView is used to display a useful piece of information to the user (such as a
4683 * contact's address), it should be made selectable, so that the user can select and copy this
4684 * content.
4685 *
4686 * Use {@link #setTextIsSelectable(boolean)} or the
4687 * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
Gilles Debunnee12f9992010-12-17 11:04:55 -08004688 * selectable (text is not selectable by default).
Gilles Debunne6f100f32010-12-13 18:04:20 -08004689 *
Gilles Debunnebb588da2011-07-11 18:26:19 -07004690 * Note that this method simply returns the state of this flag. Although this flag has to be set
4691 * in order to select text in non-editable TextView, the content of an {@link EditText} can
4692 * always be selected, independently of the value of this flag.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004693 *
4694 * @return True if the text displayed in this TextView can be selected by the user.
4695 *
4696 * @attr ref android.R.styleable#TextView_textIsSelectable
4697 */
4698 public boolean isTextSelectable() {
4699 return mTextIsSelectable;
4700 }
4701
4702 /**
4703 * Sets whether or not (default) the content of this view is selectable by the user.
Gilles Debunne6f100f32010-12-13 18:04:20 -08004704 *
Gilles Debunnee12f9992010-12-17 11:04:55 -08004705 * Note that this methods affect the {@link #setFocusable(boolean)},
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004706 * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4707 * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4708 * customized.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004709 *
4710 * See {@link #isTextSelectable} for details.
4711 *
4712 * @param selectable Whether or not the content of this TextView should be selectable.
4713 */
4714 public void setTextIsSelectable(boolean selectable) {
4715 if (mTextIsSelectable == selectable) return;
4716
4717 mTextIsSelectable = selectable;
4718
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004719 setFocusableInTouchMode(selectable);
Gilles Debunne86b9c782010-11-11 10:43:48 -08004720 setFocusable(selectable);
4721 setClickable(selectable);
4722 setLongClickable(selectable);
4723
4724 // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4725
4726 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4727 setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4728
4729 // Called by setText above, but safer in case of future code changes
4730 prepareCursorControllers();
4731 }
4732
4733 @Override
4734 protected int[] onCreateDrawableState(int extraSpace) {
Gilles Debunnefb817032011-01-13 13:52:49 -08004735 final int[] drawableState;
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004736
Gilles Debunnefb817032011-01-13 13:52:49 -08004737 if (mSingleLine) {
4738 drawableState = super.onCreateDrawableState(extraSpace);
4739 } else {
4740 drawableState = super.onCreateDrawableState(extraSpace + 1);
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004741 mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4742 }
4743
Gilles Debunne86b9c782010-11-11 10:43:48 -08004744 if (mTextIsSelectable) {
4745 // Disable pressed state, which was introduced when TextView was made clickable.
4746 // Prevents text color change.
4747 // setClickable(false) would have a similar effect, but it also disables focus changes
4748 // and long press actions, which are both needed by text selection.
4749 final int length = drawableState.length;
4750 for (int i = 0; i < length; i++) {
4751 if (drawableState[i] == R.attr.state_pressed) {
4752 final int[] nonPressedState = new int[length - 1];
4753 System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4754 System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4755 return nonPressedState;
4756 }
4757 }
4758 }
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004759
Gilles Debunne86b9c782010-11-11 10:43:48 -08004760 return drawableState;
4761 }
4762
Romain Guyc4d8eb62010-08-18 20:48:33 -07004763 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004764 protected void onDraw(Canvas canvas) {
Michael Jurka2b942d52011-03-01 13:26:11 -08004765 if (mPreDrawState == PREDRAW_DONE) {
4766 final ViewTreeObserver observer = getViewTreeObserver();
4767 observer.removeOnPreDrawListener(this);
4768 mPreDrawState = PREDRAW_NOT_REGISTERED;
4769 }
4770
Romain Guy909cbaf2010-10-13 18:19:48 -07004771 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4772
Romain Guy986003d2009-03-25 17:42:35 -07004773 restartMarqueeIfNeeded();
4774
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004775 // Draw the background for this view
4776 super.onDraw(canvas);
4777
4778 final int compoundPaddingLeft = getCompoundPaddingLeft();
4779 final int compoundPaddingTop = getCompoundPaddingTop();
4780 final int compoundPaddingRight = getCompoundPaddingRight();
4781 final int compoundPaddingBottom = getCompoundPaddingBottom();
4782 final int scrollX = mScrollX;
4783 final int scrollY = mScrollY;
4784 final int right = mRight;
4785 final int left = mLeft;
4786 final int bottom = mBottom;
4787 final int top = mTop;
4788
4789 final Drawables dr = mDrawables;
4790 if (dr != null) {
4791 /*
4792 * Compound, not extended, because the icon is not clipped
4793 * if the text height is smaller.
4794 */
4795
4796 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4797 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4798
Romain Guy3c77d392009-05-20 11:26:50 -07004799 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4800 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004801 if (dr.mDrawableLeft != null) {
4802 canvas.save();
4803 canvas.translate(scrollX + mPaddingLeft,
4804 scrollY + compoundPaddingTop +
4805 (vspace - dr.mDrawableHeightLeft) / 2);
4806 dr.mDrawableLeft.draw(canvas);
4807 canvas.restore();
4808 }
4809
Romain Guy3c77d392009-05-20 11:26:50 -07004810 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4811 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004812 if (dr.mDrawableRight != null) {
4813 canvas.save();
4814 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4815 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4816 dr.mDrawableRight.draw(canvas);
4817 canvas.restore();
4818 }
4819
Romain Guy3c77d392009-05-20 11:26:50 -07004820 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4821 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004822 if (dr.mDrawableTop != null) {
4823 canvas.save();
4824 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4825 scrollY + mPaddingTop);
4826 dr.mDrawableTop.draw(canvas);
4827 canvas.restore();
4828 }
4829
Romain Guy3c77d392009-05-20 11:26:50 -07004830 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4831 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004832 if (dr.mDrawableBottom != null) {
4833 canvas.save();
4834 canvas.translate(scrollX + compoundPaddingLeft +
4835 (hspace - dr.mDrawableWidthBottom) / 2,
4836 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4837 dr.mDrawableBottom.draw(canvas);
4838 canvas.restore();
4839 }
4840 }
4841
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004842 int color = mCurTextColor;
4843
4844 if (mLayout == null) {
4845 assumeLayout();
4846 }
4847
4848 Layout layout = mLayout;
4849 int cursorcolor = color;
4850
4851 if (mHint != null && mText.length() == 0) {
4852 if (mHintTextColor != null) {
4853 color = mCurHintTextColor;
4854 }
4855
4856 layout = mHintLayout;
4857 }
4858
4859 mTextPaint.setColor(color);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004860 if (mCurrentAlpha != 255) {
4861 // If set, the alpha will override the color's alpha. Multiply the alphas.
4862 mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4863 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004864 mTextPaint.drawableState = getDrawableState();
4865
4866 canvas.save();
4867 /* Would be faster if we didn't have to do this. Can we chop the
4868 (displayable) text so that we don't need to do this ever?
4869 */
4870
4871 int extendedPaddingTop = getExtendedPaddingTop();
4872 int extendedPaddingBottom = getExtendedPaddingBottom();
4873
4874 float clipLeft = compoundPaddingLeft + scrollX;
4875 float clipTop = extendedPaddingTop + scrollY;
4876 float clipRight = right - left - compoundPaddingRight + scrollX;
4877 float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4878
4879 if (mShadowRadius != 0) {
4880 clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4881 clipRight += Math.max(0, mShadowDx + mShadowRadius);
4882
4883 clipTop += Math.min(0, mShadowDy - mShadowRadius);
4884 clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4885 }
4886
4887 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4888
4889 int voffsetText = 0;
4890 int voffsetCursor = 0;
4891
4892 // translate in by our padding
4893 {
4894 /* shortcircuit calling getVerticaOffset() */
4895 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4896 voffsetText = getVerticalOffset(false);
4897 voffsetCursor = getVerticalOffset(true);
4898 }
4899 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4900 }
4901
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07004902 final int layoutDirection = getResolvedLayoutDirection();
4903 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Adam Powell282e3772011-08-30 16:51:11 -07004904 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
4905 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004906 if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07004907 (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004908 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4909 getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4910 }
4911
4912 if (mMarquee != null && mMarquee.isRunning()) {
4913 canvas.translate(-mMarquee.mScroll, 0.0f);
4914 }
4915 }
4916
4917 Path highlight = null;
4918 int selStart = -1, selEnd = -1;
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004919 boolean drawCursor = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004920
4921 // If there is no movement method, then there can be no selection.
4922 // Check that first and attempt to skip everything having to do with
4923 // the cursor.
4924 // XXX This is not strictly true -- a program could set the
4925 // selection manually if it really wanted to.
4926 if (mMovement != null && (isFocused() || isPressed())) {
Gilles Debunne05336272010-07-09 20:13:45 -07004927 selStart = getSelectionStart();
4928 selEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004929
Gilles Debunnebb588da2011-07-11 18:26:19 -07004930 if (selStart >= 0) {
Gilles Debunnefd419b02011-08-25 11:53:26 -07004931 if (mHighlightPath == null) mHighlightPath = new Path();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004932
4933 if (selStart == selEnd) {
Gilles Debunnebb588da2011-07-11 18:26:19 -07004934 if (isCursorVisible() &&
Gilles Debunne86b9c782010-11-11 10:43:48 -08004935 (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004936 if (mHighlightPathBogus) {
4937 mHighlightPath.reset();
4938 mLayout.getCursorPath(selStart, mHighlightPath, mText);
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004939 updateCursorsPositions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004940 mHighlightPathBogus = false;
4941 }
4942
4943 // XXX should pass to skin instead of drawing directly
4944 mHighlightPaint.setColor(cursorcolor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004945 if (mCurrentAlpha != 255) {
4946 mHighlightPaint.setAlpha(
4947 (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4948 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004949 mHighlightPaint.setStyle(Paint.Style.STROKE);
Gilles Debunne46b7d442011-02-17 16:03:10 -08004950 highlight = mHighlightPath;
Gilles Debunneeca97a32011-02-23 17:48:28 -08004951 drawCursor = mCursorCount > 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004952 }
Gilles Debunnebb588da2011-07-11 18:26:19 -07004953 } else if (textCanBeSelected()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004954 if (mHighlightPathBogus) {
4955 mHighlightPath.reset();
4956 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4957 mHighlightPathBogus = false;
4958 }
4959
4960 // XXX should pass to skin instead of drawing directly
4961 mHighlightPaint.setColor(mHighlightColor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004962 if (mCurrentAlpha != 255) {
4963 mHighlightPaint.setAlpha(
4964 (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4965 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004966 mHighlightPaint.setStyle(Paint.Style.FILL);
4967
4968 highlight = mHighlightPath;
4969 }
4970 }
4971 }
4972
4973 /* Comment out until we decide what to do about animations
4974 boolean isLinearTextOn = false;
4975 if (currentTransformation != null) {
4976 isLinearTextOn = mTextPaint.isLinearTextOn();
4977 Matrix m = currentTransformation.getMatrix();
4978 if (!m.isIdentity()) {
4979 // mTextPaint.setLinearTextOn(true);
4980 }
4981 }
4982 */
4983
4984 final InputMethodState ims = mInputMethodState;
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004985 final int cursorOffsetVertical = voffsetCursor - voffsetText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004986 if (ims != null && ims.mBatchEditNesting == 0) {
4987 InputMethodManager imm = InputMethodManager.peekInstance();
4988 if (imm != null) {
4989 if (imm.isActive(this)) {
4990 boolean reported = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07004991 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004992 // We are in extract mode and the content has changed
4993 // in some way... just report complete new text to the
4994 // input method.
4995 reported = reportExtractedText();
4996 }
4997 if (!reported && highlight != null) {
4998 int candStart = -1;
4999 int candEnd = -1;
5000 if (mText instanceof Spannable) {
5001 Spannable sp = (Spannable)mText;
5002 candStart = EditableInputConnection.getComposingSpanStart(sp);
5003 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
5004 }
5005 imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
5006 }
5007 }
5008
5009 if (imm.isWatchingCursor(this) && highlight != null) {
5010 highlight.computeBounds(ims.mTmpRectF, true);
5011 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
5012
5013 canvas.getMatrix().mapPoints(ims.mTmpOffset);
5014 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
5015
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005016 ims.mTmpRectF.offset(0, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005017
5018 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
5019 (int)(ims.mTmpRectF.top + 0.5),
5020 (int)(ims.mTmpRectF.right + 0.5),
5021 (int)(ims.mTmpRectF.bottom + 0.5));
5022
5023 imm.updateCursor(this,
5024 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
5025 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
5026 }
5027 }
5028 }
5029
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005030 if (mCorrectionHighlighter != null) {
5031 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
5032 }
5033
Gilles Debunne46b7d442011-02-17 16:03:10 -08005034 if (drawCursor) {
5035 drawCursor(canvas, cursorOffsetVertical);
5036 // Rely on the drawable entirely, do not draw the cursor line.
5037 // Has to be done after the IMM related code above which relies on the highlight.
5038 highlight = null;
5039 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005040
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005041 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005042
Romain Guyc2303192009-04-03 17:37:18 -07005043 if (mMarquee != null && mMarquee.shouldDrawGhost()) {
5044 canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005045 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
Romain Guyc2303192009-04-03 17:37:18 -07005046 }
5047
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005048 /* Comment out until we decide what to do about animations
5049 if (currentTransformation != null) {
5050 mTextPaint.setLinearTextOn(isLinearTextOn);
5051 }
5052 */
5053
5054 canvas.restore();
Leon Scroggins56426252010-11-01 15:45:37 -04005055 }
5056
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005057 private void updateCursorsPositions() {
Gilles Debunneeca97a32011-02-23 17:48:28 -08005058 if (mCursorDrawableRes == 0) {
5059 mCursorCount = 0;
5060 return;
5061 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08005062
5063 final int offset = getSelectionStart();
5064 final int line = mLayout.getLineForOffset(offset);
5065 final int top = mLayout.getLineTop(line);
5066 final int bottom = mLayout.getLineTop(line + 1);
5067
5068 mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
5069
5070 int middle = bottom;
5071 if (mCursorCount == 2) {
5072 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
5073 middle = (top + bottom) >> 1;
5074 }
5075
5076 updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
5077
5078 if (mCursorCount == 2) {
5079 updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
5080 }
5081 }
5082
5083 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
5084 if (mCursorDrawable[cursorIndex] == null)
5085 mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
5086
5087 if (mTempRect == null) mTempRect = new Rect();
5088
5089 mCursorDrawable[cursorIndex].getPadding(mTempRect);
5090 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
5091 horizontal = Math.max(0.5f, horizontal - 0.5f);
5092 final int left = (int) (horizontal) - mTempRect.left;
5093 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
5094 bottom + mTempRect.bottom);
5095 }
5096
5097 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
5098 final boolean translate = cursorOffsetVertical != 0;
5099 if (translate) canvas.translate(0, cursorOffsetVertical);
5100 for (int i = 0; i < mCursorCount; i++) {
5101 mCursorDrawable[i].draw(canvas);
5102 }
5103 if (translate) canvas.translate(0, -cursorOffsetVertical);
5104 }
5105
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005106 @Override
5107 public void getFocusedRect(Rect r) {
5108 if (mLayout == null) {
5109 super.getFocusedRect(r);
5110 return;
5111 }
5112
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005113 int selEnd = getSelectionEnd();
5114 if (selEnd < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005115 super.getFocusedRect(r);
5116 return;
5117 }
5118
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005119 int selStart = getSelectionStart();
5120 if (selStart < 0 || selStart >= selEnd) {
5121 int line = mLayout.getLineForOffset(selEnd);
5122 r.top = mLayout.getLineTop(line);
5123 r.bottom = mLayout.getLineBottom(line);
5124 r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
5125 r.right = r.left + 4;
5126 } else {
5127 int lineStart = mLayout.getLineForOffset(selStart);
5128 int lineEnd = mLayout.getLineForOffset(selEnd);
5129 r.top = mLayout.getLineTop(lineStart);
5130 r.bottom = mLayout.getLineBottom(lineEnd);
5131 if (lineStart == lineEnd) {
5132 r.left = (int) mLayout.getPrimaryHorizontal(selStart);
5133 r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
5134 } else {
5135 // Selection extends across multiple lines -- the focused
5136 // rect covers the entire width.
Gilles Debunnefd419b02011-08-25 11:53:26 -07005137 if (mHighlightPath == null) mHighlightPath = new Path();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07005138 if (mHighlightPathBogus) {
5139 mHighlightPath.reset();
5140 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
5141 mHighlightPathBogus = false;
5142 }
5143 synchronized (sTempRect) {
5144 mHighlightPath.computeBounds(sTempRect, true);
5145 r.left = (int)sTempRect.left-1;
5146 r.right = (int)sTempRect.right+1;
5147 }
5148 }
5149 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005150
5151 // Adjust for padding and gravity.
5152 int paddingLeft = getCompoundPaddingLeft();
5153 int paddingTop = getExtendedPaddingTop();
5154 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5155 paddingTop += getVerticalOffset(false);
5156 }
5157 r.offset(paddingLeft, paddingTop);
5158 }
5159
5160 /**
5161 * Return the number of lines of text, or 0 if the internal Layout has not
5162 * been built.
5163 */
5164 public int getLineCount() {
5165 return mLayout != null ? mLayout.getLineCount() : 0;
5166 }
5167
5168 /**
5169 * Return the baseline for the specified line (0...getLineCount() - 1)
5170 * If bounds is not null, return the top, left, right, bottom extents
5171 * of the specified line in it. If the internal Layout has not been built,
5172 * return 0 and set bounds to (0, 0, 0, 0)
5173 * @param line which line to examine (0..getLineCount() - 1)
5174 * @param bounds Optional. If not null, it returns the extent of the line
5175 * @return the Y-coordinate of the baseline
5176 */
5177 public int getLineBounds(int line, Rect bounds) {
5178 if (mLayout == null) {
5179 if (bounds != null) {
5180 bounds.set(0, 0, 0, 0);
5181 }
5182 return 0;
5183 }
5184 else {
5185 int baseline = mLayout.getLineBounds(line, bounds);
5186
5187 int voffset = getExtendedPaddingTop();
5188 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5189 voffset += getVerticalOffset(true);
5190 }
5191 if (bounds != null) {
5192 bounds.offset(getCompoundPaddingLeft(), voffset);
5193 }
5194 return baseline + voffset;
5195 }
5196 }
5197
5198 @Override
5199 public int getBaseline() {
5200 if (mLayout == null) {
5201 return super.getBaseline();
5202 }
5203
5204 int voffset = 0;
5205 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5206 voffset = getVerticalOffset(true);
5207 }
5208
5209 return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
5210 }
5211
Romain Guyf2fc4602011-07-19 15:20:03 -07005212 /**
5213 * @hide
5214 * @param offsetRequired
5215 */
5216 @Override
5217 protected int getFadeTop(boolean offsetRequired) {
Romain Guy59f13c7d2011-07-19 18:35:33 -07005218 if (mLayout == null) return 0;
5219
Romain Guyf2fc4602011-07-19 15:20:03 -07005220 int voffset = 0;
5221 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5222 voffset = getVerticalOffset(true);
5223 }
5224
5225 if (offsetRequired) voffset += getTopPaddingOffset();
5226
5227 return getExtendedPaddingTop() + voffset;
5228 }
5229
5230 /**
5231 * @hide
5232 * @param offsetRequired
5233 */
Gilles Debunne3784a7f2011-07-15 13:49:38 -07005234 @Override
Romain Guyf2fc4602011-07-19 15:20:03 -07005235 protected int getFadeHeight(boolean offsetRequired) {
5236 return mLayout != null ? mLayout.getHeight() : 0;
5237 }
5238
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005239 @Override
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005240 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
5241 if (keyCode == KeyEvent.KEYCODE_BACK) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005242 boolean isInSelectionMode = mSelectionActionMode != null;
5243
Gilles Debunne28294cc2011-08-24 12:02:05 -07005244 if (isInSelectionMode) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005245 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
5246 KeyEvent.DispatcherState state = getKeyDispatcherState();
5247 if (state != null) {
5248 state.startTracking(event, this);
5249 }
5250 return true;
5251 } else if (event.getAction() == KeyEvent.ACTION_UP) {
5252 KeyEvent.DispatcherState state = getKeyDispatcherState();
5253 if (state != null) {
5254 state.handleUpEvent(event);
5255 }
5256 if (event.isTracking() && !event.isCanceled()) {
Gilles Debunne4a7199a2011-07-11 14:58:27 -07005257 if (isInSelectionMode) {
5258 stopSelectionActionMode();
5259 return true;
5260 }
5261 }
5262 }
5263 }
5264 }
5265 return super.onKeyPreIme(keyCode, event);
5266 }
5267
5268 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005269 public boolean onKeyDown(int keyCode, KeyEvent event) {
5270 int which = doKeyDown(keyCode, event, null);
5271 if (which == 0) {
5272 // Go through default dispatching.
5273 return super.onKeyDown(keyCode, event);
5274 }
5275
5276 return true;
5277 }
5278
5279 @Override
5280 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005281 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005282
5283 int which = doKeyDown(keyCode, down, event);
5284 if (which == 0) {
5285 // Go through default dispatching.
5286 return super.onKeyMultiple(keyCode, repeatCount, event);
5287 }
5288 if (which == -1) {
5289 // Consumed the whole thing.
5290 return true;
5291 }
5292
5293 repeatCount--;
5294
5295 // We are going to dispatch the remaining events to either the input
5296 // or movement method. To do this, we will just send a repeated stream
5297 // of down and up events until we have done the complete repeatCount.
5298 // It would be nice if those interfaces had an onKeyMultiple() method,
5299 // but adding that is a more complicated change.
The Android Open Source Project10592532009-03-18 17:39:46 -07005300 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005301 if (which == 1) {
5302 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5303 while (--repeatCount > 0) {
5304 mInput.onKeyDown(this, (Editable)mText, keyCode, down);
5305 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
5306 }
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005307 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005308
5309 } else if (which == 2) {
5310 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5311 while (--repeatCount > 0) {
5312 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
5313 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
5314 }
5315 }
5316
5317 return true;
5318 }
5319
5320 /**
5321 * Returns true if pressing ENTER in this field advances focus instead
5322 * of inserting the character. This is true mostly in single-line fields,
5323 * but also in mail addresses and subjects which will display on multiple
5324 * lines but where it doesn't make sense to insert newlines.
5325 */
The Android Open Source Project4df24232009-03-05 14:34:35 -08005326 private boolean shouldAdvanceFocusOnEnter() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005327 if (mInput == null) {
5328 return false;
5329 }
5330
5331 if (mSingleLine) {
5332 return true;
5333 }
5334
5335 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5336 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Jeff Brown4e6319b2010-12-13 10:36:51 -08005337 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
5338 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005339 return true;
5340 }
5341 }
5342
5343 return false;
5344 }
5345
Jeff Brown4e6319b2010-12-13 10:36:51 -08005346 /**
5347 * Returns true if pressing TAB in this field advances focus instead
5348 * of inserting the character. Insert tabs only in multi-line editors.
5349 */
5350 private boolean shouldAdvanceFocusOnTab() {
5351 if (mInput != null && !mSingleLine) {
5352 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
5353 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
5354 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
5355 || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
5356 return false;
5357 }
5358 }
5359 }
5360 return true;
5361 }
5362
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005363 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
5364 if (!isEnabled()) {
5365 return 0;
5366 }
5367
5368 switch (keyCode) {
5369 case KeyEvent.KEYCODE_ENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005370 if (event.hasNoModifiers()) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005371 // When mInputContentType is set, we know that we are
5372 // running in a "modern" cupcake environment, so don't need
5373 // to worry about the application trying to capture
5374 // enter key events.
5375 if (mInputContentType != null) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005376 // If there is an action listener, given them a
5377 // chance to consume the event.
5378 if (mInputContentType.onEditorActionListener != null &&
5379 mInputContentType.onEditorActionListener.onEditorAction(
5380 this, EditorInfo.IME_NULL, event)) {
5381 mInputContentType.enterDown = true;
5382 // We are consuming the enter key for them.
5383 return -1;
5384 }
5385 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005386
The Android Open Source Project10592532009-03-18 17:39:46 -07005387 // If our editor should move focus when enter is pressed, or
5388 // this is a generated event from an IME action button, then
5389 // don't let it be inserted into the text.
Jeff Brown4e6319b2010-12-13 10:36:51 -08005390 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
The Android Open Source Project10592532009-03-18 17:39:46 -07005391 || shouldAdvanceFocusOnEnter()) {
Dianne Hackborn0500b3c2011-11-01 15:28:43 -07005392 if (hasOnClickListeners()) {
Leon Scroggins7014b122011-01-11 15:17:34 -05005393 return 0;
5394 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005395 return -1;
5396 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005397 }
The Android Open Source Project10592532009-03-18 17:39:46 -07005398 break;
5399
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005400 case KeyEvent.KEYCODE_DPAD_CENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005401 if (event.hasNoModifiers()) {
5402 if (shouldAdvanceFocusOnEnter()) {
5403 return 0;
5404 }
5405 }
5406 break;
5407
5408 case KeyEvent.KEYCODE_TAB:
5409 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
5410 if (shouldAdvanceFocusOnTab()) {
5411 return 0;
5412 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005413 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005414 break;
5415
5416 // Has to be done on key down (and not on key up) to correctly be intercepted.
5417 case KeyEvent.KEYCODE_BACK:
5418 if (mSelectionActionMode != null) {
5419 stopSelectionActionMode();
5420 return -1;
5421 }
5422 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005423 }
5424
5425 if (mInput != null) {
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005426 resetErrorChangedFlag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005427
5428 boolean doDown = true;
5429 if (otherEvent != null) {
5430 try {
5431 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005432 final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005433 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005434 doDown = false;
5435 if (handled) {
5436 return -1;
5437 }
5438 } catch (AbstractMethodError e) {
5439 // onKeyOther was added after 1.0, so if it isn't
5440 // implemented we need to try to dispatch as a regular down.
5441 } finally {
5442 endBatchEdit();
5443 }
5444 }
5445
5446 if (doDown) {
5447 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005448 final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005449 endBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08005450 hideErrorIfUnchanged();
5451 if (handled) return 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005452 }
5453 }
5454
5455 // bug 650865: sometimes we get a key event before a layout.
5456 // don't try to move around if we don't know the layout.
5457
5458 if (mMovement != null && mLayout != null) {
5459 boolean doDown = true;
5460 if (otherEvent != null) {
5461 try {
5462 boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
5463 otherEvent);
5464 doDown = false;
5465 if (handled) {
5466 return -1;
5467 }
5468 } catch (AbstractMethodError e) {
5469 // onKeyOther was added after 1.0, so if it isn't
5470 // implemented we need to try to dispatch as a regular down.
5471 }
5472 }
5473 if (doDown) {
5474 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
5475 return 2;
5476 }
5477 }
5478
5479 return 0;
5480 }
5481
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08005482 /**
5483 * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
5484 * can be recorded.
5485 * @hide
5486 */
5487 public void resetErrorChangedFlag() {
5488 /*
5489 * Keep track of what the error was before doing the input
5490 * so that if an input filter changed the error, we leave
5491 * that error showing. Otherwise, we take down whatever
5492 * error was showing when the user types something.
5493 */
5494 mErrorWasChanged = false;
5495 }
5496
5497 /**
5498 * @hide
5499 */
5500 public void hideErrorIfUnchanged() {
5501 if (mError != null && !mErrorWasChanged) {
5502 setError(null, null);
5503 }
5504 }
5505
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005506 @Override
5507 public boolean onKeyUp(int keyCode, KeyEvent event) {
5508 if (!isEnabled()) {
5509 return super.onKeyUp(keyCode, event);
5510 }
5511
5512 switch (keyCode) {
5513 case KeyEvent.KEYCODE_DPAD_CENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005514 if (event.hasNoModifiers()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005515 /*
5516 * If there is a click listener, just call through to
5517 * super, which will invoke it.
5518 *
Jeff Brown4e6319b2010-12-13 10:36:51 -08005519 * If there isn't a click listener, try to show the soft
5520 * input method. (It will also
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005521 * call performClick(), but that won't do anything in
5522 * this case.)
5523 */
Gilles Debunne06a8e9b2011-12-08 10:39:39 -08005524 if (!hasOnClickListeners()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005525 if (mMovement != null && mText instanceof Editable
5526 && mLayout != null && onCheckIsTextEditor()) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08005527 InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09005528 viewClicked(imm);
Gilles Debunne0f4109e2011-10-19 11:26:21 -07005529 if (imm != null && mSoftInputShownOnFocus) {
satok863fcd62011-06-21 17:38:02 +09005530 imm.showSoftInput(this, 0);
5531 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08005532 }
5533 }
5534 }
5535 return super.onKeyUp(keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005536
Jeff Brown4e6319b2010-12-13 10:36:51 -08005537 case KeyEvent.KEYCODE_ENTER:
Jeff Brown4e6319b2010-12-13 10:36:51 -08005538 if (event.hasNoModifiers()) {
5539 if (mInputContentType != null
5540 && mInputContentType.onEditorActionListener != null
5541 && mInputContentType.enterDown) {
5542 mInputContentType.enterDown = false;
5543 if (mInputContentType.onEditorActionListener.onEditorAction(
5544 this, EditorInfo.IME_NULL, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005545 return true;
5546 }
5547 }
5548
Jeff Brown4e6319b2010-12-13 10:36:51 -08005549 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
5550 || shouldAdvanceFocusOnEnter()) {
5551 /*
5552 * If there is a click listener, just call through to
5553 * super, which will invoke it.
5554 *
5555 * If there isn't a click listener, try to advance focus,
5556 * but still call through to super, which will reset the
5557 * pressed state and longpress state. (It will also
5558 * call performClick(), but that won't do anything in
5559 * this case.)
5560 */
Gilles Debunne06a8e9b2011-12-08 10:39:39 -08005561 if (!hasOnClickListeners()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005562 View v = focusSearch(FOCUS_DOWN);
5563
5564 if (v != null) {
5565 if (!v.requestFocus(FOCUS_DOWN)) {
5566 throw new IllegalStateException(
5567 "focus search returned a view " +
5568 "that wasn't able to take focus!");
5569 }
5570
5571 /*
5572 * Return true because we handled the key; super
5573 * will return false because there was no click
5574 * listener.
5575 */
5576 super.onKeyUp(keyCode, event);
5577 return true;
5578 } else if ((event.getFlags()
5579 & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
5580 // No target for next focus, but make sure the IME
5581 // if this came from it.
5582 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08005583 if (imm != null && imm.isActive(this)) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08005584 imm.hideSoftInputFromWindow(getWindowToken(), 0);
5585 }
5586 }
5587 }
5588 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005589 return super.onKeyUp(keyCode, event);
5590 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07005591 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005592 }
5593
5594 if (mInput != null)
5595 if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
5596 return true;
5597
5598 if (mMovement != null && mLayout != null)
5599 if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
5600 return true;
5601
5602 return super.onKeyUp(keyCode, event);
5603 }
5604
5605 @Override public boolean onCheckIsTextEditor() {
5606 return mInputType != EditorInfo.TYPE_NULL;
5607 }
Gilles Debunneb062e812011-09-27 14:58:37 -07005608
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005609 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
Janos Levai042856c2010-10-15 02:53:58 +03005610 if (onCheckIsTextEditor() && isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005611 if (mInputMethodState == null) {
5612 mInputMethodState = new InputMethodState();
5613 }
5614 outAttrs.inputType = mInputType;
5615 if (mInputContentType != null) {
5616 outAttrs.imeOptions = mInputContentType.imeOptions;
5617 outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5618 outAttrs.actionLabel = mInputContentType.imeActionLabel;
5619 outAttrs.actionId = mInputContentType.imeActionId;
5620 outAttrs.extras = mInputContentType.extras;
5621 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005622 outAttrs.imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005623 }
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005624 if (focusSearch(FOCUS_DOWN) != null) {
5625 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5626 }
5627 if (focusSearch(FOCUS_UP) != null) {
5628 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5629 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005630 if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5631 == EditorInfo.IME_ACTION_UNSPECIFIED) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005632 if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005633 // An action has not been set, but the enter key will move to
5634 // the next focus, so set the action to that.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005635 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005636 } else {
5637 // An action has not been set, and there is no focus to move
5638 // to, so let's just supply a "done" action.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005639 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005640 }
5641 if (!shouldAdvanceFocusOnEnter()) {
5642 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005643 }
5644 }
Gilles Debunne91a08cf2010-11-08 17:34:49 -08005645 if (isMultilineInputType(outAttrs.inputType)) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005646 // Multi-line text editors should always show an enter key.
5647 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5648 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005649 outAttrs.hintText = mHint;
5650 if (mText instanceof Editable) {
5651 InputConnection ic = new EditableInputConnection(this);
Gilles Debunne05336272010-07-09 20:13:45 -07005652 outAttrs.initialSelStart = getSelectionStart();
5653 outAttrs.initialSelEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005654 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5655 return ic;
5656 }
5657 }
5658 return null;
5659 }
5660
5661 /**
5662 * If this TextView contains editable content, extract a portion of it
5663 * based on the information in <var>request</var> in to <var>outText</var>.
5664 * @return Returns true if the text was successfully extracted, else false.
5665 */
5666 public boolean extractText(ExtractedTextRequest request,
5667 ExtractedText outText) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005668 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5669 EXTRACT_UNKNOWN, outText);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005670 }
5671
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005672 static final int EXTRACT_NOTHING = -2;
5673 static final int EXTRACT_UNKNOWN = -1;
5674
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005675 boolean extractTextInternal(ExtractedTextRequest request,
5676 int partialStartOffset, int partialEndOffset, int delta,
5677 ExtractedText outText) {
5678 final CharSequence content = mText;
5679 if (content != null) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005680 if (partialStartOffset != EXTRACT_NOTHING) {
5681 final int N = content.length();
5682 if (partialStartOffset < 0) {
5683 outText.partialStartOffset = outText.partialEndOffset = -1;
5684 partialStartOffset = 0;
5685 partialEndOffset = N;
5686 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01005687 // Now use the delta to determine the actual amount of text
5688 // we need.
5689 partialEndOffset += delta;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005690 // Adjust offsets to ensure we contain full spans.
5691 if (content instanceof Spanned) {
5692 Spanned spanned = (Spanned)content;
5693 Object[] spans = spanned.getSpans(partialStartOffset,
5694 partialEndOffset, ParcelableSpan.class);
5695 int i = spans.length;
5696 while (i > 0) {
5697 i--;
5698 int j = spanned.getSpanStart(spans[i]);
5699 if (j < partialStartOffset) partialStartOffset = j;
5700 j = spanned.getSpanEnd(spans[i]);
5701 if (j > partialEndOffset) partialEndOffset = j;
5702 }
5703 }
5704 outText.partialStartOffset = partialStartOffset;
Viktor Yakovel964be412010-02-17 08:35:57 +01005705 outText.partialEndOffset = partialEndOffset - delta;
5706
Eric Fischer32929412009-12-14 17:33:11 -08005707 if (partialStartOffset > N) {
5708 partialStartOffset = N;
5709 } else if (partialStartOffset < 0) {
5710 partialStartOffset = 0;
5711 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005712 if (partialEndOffset > N) {
5713 partialEndOffset = N;
5714 } else if (partialEndOffset < 0) {
5715 partialEndOffset = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005716 }
5717 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005718 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5719 outText.text = content.subSequence(partialStartOffset,
5720 partialEndOffset);
5721 } else {
5722 outText.text = TextUtils.substring(content, partialStartOffset,
5723 partialEndOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005724 }
Viktor Yakovel970a138c92010-02-12 16:00:41 +01005725 } else {
5726 outText.partialStartOffset = 0;
5727 outText.partialEndOffset = 0;
5728 outText.text = "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005729 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005730 outText.flags = 0;
5731 if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5732 outText.flags |= ExtractedText.FLAG_SELECTING;
5733 }
5734 if (mSingleLine) {
5735 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005736 }
5737 outText.startOffset = 0;
Gilles Debunne05336272010-07-09 20:13:45 -07005738 outText.selectionStart = getSelectionStart();
5739 outText.selectionEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005740 return true;
5741 }
5742 return false;
5743 }
5744
5745 boolean reportExtractedText() {
5746 final InputMethodState ims = mInputMethodState;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005747 if (ims != null) {
5748 final boolean contentChanged = ims.mContentChanged;
5749 if (contentChanged || ims.mSelectionModeChanged) {
5750 ims.mContentChanged = false;
5751 ims.mSelectionModeChanged = false;
5752 final ExtractedTextRequest req = mInputMethodState.mExtracting;
5753 if (req != null) {
5754 InputMethodManager imm = InputMethodManager.peekInstance();
5755 if (imm != null) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005756 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005757 + ims.mChangedStart + " end=" + ims.mChangedEnd
5758 + " delta=" + ims.mChangedDelta);
5759 if (ims.mChangedStart < 0 && !contentChanged) {
5760 ims.mChangedStart = EXTRACT_NOTHING;
5761 }
5762 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5763 ims.mChangedDelta, ims.mTmpExtracted)) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005764 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005765 + ims.mTmpExtracted.partialStartOffset
5766 + " end=" + ims.mTmpExtracted.partialEndOffset
5767 + ": " + ims.mTmpExtracted.text);
5768 imm.updateExtractedText(this, req.token,
5769 mInputMethodState.mTmpExtracted);
Viktor Yakovel964be412010-02-17 08:35:57 +01005770 ims.mChangedStart = EXTRACT_UNKNOWN;
5771 ims.mChangedEnd = EXTRACT_UNKNOWN;
5772 ims.mChangedDelta = 0;
5773 ims.mContentChanged = false;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005774 return true;
5775 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005776 }
5777 }
5778 }
5779 }
5780 return false;
5781 }
5782
5783 /**
5784 * This is used to remove all style-impacting spans from text before new
5785 * extracted text is being replaced into it, so that we don't have any
5786 * lingering spans applied during the replace.
5787 */
5788 static void removeParcelableSpans(Spannable spannable, int start, int end) {
5789 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5790 int i = spans.length;
5791 while (i > 0) {
5792 i--;
5793 spannable.removeSpan(spans[i]);
5794 }
5795 }
5796
5797 /**
5798 * Apply to this text view the given extracted text, as previously
5799 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5800 */
5801 public void setExtractedText(ExtractedText text) {
5802 Editable content = getEditableText();
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005803 if (text.text != null) {
5804 if (content == null) {
5805 setText(text.text, TextView.BufferType.EDITABLE);
5806 } else if (text.partialStartOffset < 0) {
5807 removeParcelableSpans(content, 0, content.length());
5808 content.replace(0, content.length(), text.text);
5809 } else {
5810 final int N = content.length();
5811 int start = text.partialStartOffset;
5812 if (start > N) start = N;
5813 int end = text.partialEndOffset;
5814 if (end > N) end = N;
5815 removeParcelableSpans(content, start, end);
5816 content.replace(start, end, text.text);
5817 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005818 }
5819
5820 // Now set the selection position... make sure it is in range, to
5821 // avoid crashes. If this is a partial update, it is possible that
5822 // the underlying text may have changed, causing us problems here.
5823 // Also we just don't want to trust clients to do the right thing.
5824 Spannable sp = (Spannable)getText();
5825 final int N = sp.length();
5826 int start = text.selectionStart;
5827 if (start < 0) start = 0;
5828 else if (start > N) start = N;
5829 int end = text.selectionEnd;
5830 if (end < 0) end = 0;
5831 else if (end > N) end = N;
5832 Selection.setSelection(sp, start, end);
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005833
5834 // Finally, update the selection mode.
5835 if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5836 MetaKeyKeyListener.startSelecting(this, sp);
5837 } else {
5838 MetaKeyKeyListener.stopSelecting(this, sp);
5839 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005840 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005841
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005842 /**
5843 * @hide
5844 */
5845 public void setExtracting(ExtractedTextRequest req) {
5846 if (mInputMethodState != null) {
5847 mInputMethodState.mExtracting = req;
5848 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005849 // This would stop a possible selection mode, but no such mode is started in case
5850 // extracted mode will start. Some text is selected though, and will trigger an action mode
5851 // in the extracted view.
Adam Powellba0a2c32010-09-28 17:41:23 -07005852 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005853 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -07005854
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005855 /**
5856 * Called by the framework in response to a text completion from
5857 * the current input method, provided by it calling
5858 * {@link InputConnection#commitCompletion
5859 * InputConnection.commitCompletion()}. The default implementation does
5860 * nothing; text views that are supporting auto-completion should override
5861 * this to do their desired behavior.
5862 *
5863 * @param text The auto complete text the user has selected.
5864 */
5865 public void onCommitCompletion(CompletionInfo text) {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08005866 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005867 }
5868
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005869 /**
5870 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5871 * a dictionnary) from the current input method, provided by it calling
5872 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5873 * implementation flashes the background of the corrected word to provide feedback to the user.
5874 *
5875 * @param info The auto correct info about the text that was corrected.
5876 */
5877 public void onCommitCorrection(CorrectionInfo info) {
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005878 if (mCorrectionHighlighter == null) {
5879 mCorrectionHighlighter = new CorrectionHighlighter();
5880 } else {
5881 mCorrectionHighlighter.invalidate(false);
5882 }
5883
5884 mCorrectionHighlighter.highlight(info);
5885 }
5886
5887 private class CorrectionHighlighter {
5888 private final Path mPath = new Path();
5889 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5890 private int mStart, mEnd;
5891 private long mFadingStartTime;
5892 private final static int FADE_OUT_DURATION = 400;
5893
5894 public CorrectionHighlighter() {
5895 mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5896 mPaint.setStyle(Paint.Style.FILL);
5897 }
5898
5899 public void highlight(CorrectionInfo info) {
5900 mStart = info.getOffset();
5901 mEnd = mStart + info.getNewText().length();
5902 mFadingStartTime = SystemClock.uptimeMillis();
5903
5904 if (mStart < 0 || mEnd < 0) {
5905 stopAnimation();
5906 }
5907 }
5908
5909 public void draw(Canvas canvas, int cursorOffsetVertical) {
5910 if (updatePath() && updatePaint()) {
5911 if (cursorOffsetVertical != 0) {
5912 canvas.translate(0, cursorOffsetVertical);
5913 }
5914
5915 canvas.drawPath(mPath, mPaint);
5916
5917 if (cursorOffsetVertical != 0) {
5918 canvas.translate(0, -cursorOffsetVertical);
5919 }
Gilles Debunne8615ac92011-11-29 15:25:03 -08005920 invalidate(true); // TODO invalidate cursor region only
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005921 } else {
5922 stopAnimation();
Gilles Debunne8615ac92011-11-29 15:25:03 -08005923 invalidate(false); // TODO invalidate cursor region only
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005924 }
5925 }
5926
5927 private boolean updatePaint() {
5928 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5929 if (duration > FADE_OUT_DURATION) return false;
5930
5931 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5932 final int highlightColorAlpha = Color.alpha(mHighlightColor);
5933 final int color = (mHighlightColor & 0x00FFFFFF) +
5934 ((int) (highlightColorAlpha * coef) << 24);
5935 mPaint.setColor(color);
5936 return true;
5937 }
5938
5939 private boolean updatePath() {
5940 final Layout layout = TextView.this.mLayout;
5941 if (layout == null) return false;
5942
5943 // Update in case text is edited while the animation is run
5944 final int length = mText.length();
5945 int start = Math.min(length, mStart);
5946 int end = Math.min(length, mEnd);
5947
5948 mPath.reset();
5949 TextView.this.mLayout.getSelectionPath(start, end, mPath);
5950 return true;
5951 }
5952
5953 private void invalidate(boolean delayed) {
5954 if (TextView.this.mLayout == null) return;
5955
5956 synchronized (sTempRect) {
5957 mPath.computeBounds(sTempRect, false);
5958
5959 int left = getCompoundPaddingLeft();
5960 int top = getExtendedPaddingTop() + getVerticalOffset(true);
5961
5962 if (delayed) {
5963 TextView.this.postInvalidateDelayed(16, // 60 Hz update
5964 left + (int) sTempRect.left, top + (int) sTempRect.top,
5965 left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5966 } else {
5967 TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5968 (int) sTempRect.right, (int) sTempRect.bottom);
5969 }
5970 }
5971 }
5972
5973 private void stopAnimation() {
5974 TextView.this.mCorrectionHighlighter = null;
5975 }
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005976 }
5977
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005978 public void beginBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005979 mInBatchEditControllers = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005980 final InputMethodState ims = mInputMethodState;
5981 if (ims != null) {
5982 int nesting = ++ims.mBatchEditNesting;
5983 if (nesting == 1) {
5984 ims.mCursorChanged = false;
5985 ims.mChangedDelta = 0;
5986 if (ims.mContentChanged) {
5987 // We already have a pending change from somewhere else,
5988 // so turn this into a full update.
5989 ims.mChangedStart = 0;
5990 ims.mChangedEnd = mText.length();
5991 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005992 ims.mChangedStart = EXTRACT_UNKNOWN;
5993 ims.mChangedEnd = EXTRACT_UNKNOWN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005994 ims.mContentChanged = false;
5995 }
5996 onBeginBatchEdit();
5997 }
5998 }
5999 }
6000
6001 public void endBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07006002 mInBatchEditControllers = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006003 final InputMethodState ims = mInputMethodState;
6004 if (ims != null) {
6005 int nesting = --ims.mBatchEditNesting;
6006 if (nesting == 0) {
6007 finishBatchEdit(ims);
6008 }
6009 }
6010 }
6011
6012 void ensureEndedBatchEdit() {
6013 final InputMethodState ims = mInputMethodState;
6014 if (ims != null && ims.mBatchEditNesting != 0) {
6015 ims.mBatchEditNesting = 0;
6016 finishBatchEdit(ims);
6017 }
6018 }
6019
6020 void finishBatchEdit(final InputMethodState ims) {
6021 onEndBatchEdit();
6022
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07006023 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006024 updateAfterEdit();
6025 reportExtractedText();
6026 } else if (ims.mCursorChanged) {
6027 // Cheezy way to get us to report the current cursor location.
6028 invalidateCursor();
6029 }
6030 }
6031
6032 void updateAfterEdit() {
6033 invalidate();
Gilles Debunne05336272010-07-09 20:13:45 -07006034 int curs = getSelectionStart();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006035
Gilles Debunne3d010062011-02-18 14:16:41 -08006036 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006037 registerForPreDraw();
6038 }
6039
6040 if (curs >= 0) {
6041 mHighlightPathBogus = true;
Gilles Debunne3d010062011-02-18 14:16:41 -08006042 makeBlink();
Gilles Debunne8202cd32011-07-15 09:56:30 -07006043 bringPointIntoView(curs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006044 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006045
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006046 checkForResize();
6047 }
6048
6049 /**
6050 * Called by the framework in response to a request to begin a batch
6051 * of edit operations through a call to link {@link #beginBatchEdit()}.
6052 */
6053 public void onBeginBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08006054 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006055 }
6056
6057 /**
6058 * Called by the framework in response to a request to end a batch
6059 * of edit operations through a call to link {@link #endBatchEdit}.
6060 */
6061 public void onEndBatchEdit() {
Gilles Debunned40cc3c2011-03-11 16:59:32 -08006062 // intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006063 }
6064
6065 /**
6066 * Called by the framework in response to a private command from the
6067 * current method, provided by it calling
6068 * {@link InputConnection#performPrivateCommand
6069 * InputConnection.performPrivateCommand()}.
6070 *
6071 * @param action The action name of the command.
6072 * @param data Any additional data for the command. This may be null.
6073 * @return Return true if you handled the command, else false.
6074 */
6075 public boolean onPrivateIMECommand(String action, Bundle data) {
6076 return false;
6077 }
6078
6079 private void nullLayouts() {
6080 if (mLayout instanceof BoringLayout && mSavedLayout == null) {
6081 mSavedLayout = (BoringLayout) mLayout;
6082 }
6083 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
6084 mSavedHintLayout = (BoringLayout) mHintLayout;
6085 }
6086
Adam Powell282e3772011-08-30 16:51:11 -07006087 mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
Gilles Debunne77f18b02010-10-22 14:28:25 -07006088
Fabrice Di Megliod4c3b8e2011-11-09 18:04:07 -08006089 mBoring = mHintBoring = null;
6090
Gilles Debunne77f18b02010-10-22 14:28:25 -07006091 // Since it depends on the value of mLayout
6092 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006093 }
6094
6095 /**
6096 * Make a new Layout based on the already-measured size of the view,
6097 * on the assumption that it was measured correctly at some point.
6098 */
6099 private void assumeLayout() {
6100 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6101
6102 if (width < 1) {
6103 width = 0;
6104 }
6105
6106 int physicalWidth = width;
6107
6108 if (mHorizontallyScrolling) {
Jeff Brown033a0012011-11-11 15:30:16 -08006109 width = VERY_WIDE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006110 }
6111
6112 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
6113 physicalWidth, false);
6114 }
6115
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006116 @Override
Fabrice Di Meglio7f86c802011-07-01 15:09:24 -07006117 protected void resetResolvedLayoutDirection() {
6118 super.resetResolvedLayoutDirection();
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006119
6120 if (mLayoutAlignment != null &&
6121 (mTextAlign == TextAlign.VIEW_START ||
6122 mTextAlign == TextAlign.VIEW_END)) {
6123 mLayoutAlignment = null;
6124 }
6125 }
6126
6127 private Layout.Alignment getLayoutAlignment() {
6128 if (mLayoutAlignment == null) {
6129 Layout.Alignment alignment;
6130 TextAlign textAlign = mTextAlign;
6131 switch (textAlign) {
6132 case INHERIT:
6133 // fall through to gravity temporarily
6134 // intention is to inherit value through view hierarchy.
6135 case GRAVITY:
6136 switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
6137 case Gravity.START:
6138 alignment = Layout.Alignment.ALIGN_NORMAL;
6139 break;
6140 case Gravity.END:
6141 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6142 break;
6143 case Gravity.LEFT:
6144 alignment = Layout.Alignment.ALIGN_LEFT;
6145 break;
6146 case Gravity.RIGHT:
6147 alignment = Layout.Alignment.ALIGN_RIGHT;
6148 break;
6149 case Gravity.CENTER_HORIZONTAL:
6150 alignment = Layout.Alignment.ALIGN_CENTER;
6151 break;
6152 default:
6153 alignment = Layout.Alignment.ALIGN_NORMAL;
6154 break;
6155 }
6156 break;
6157 case TEXT_START:
6158 alignment = Layout.Alignment.ALIGN_NORMAL;
6159 break;
6160 case TEXT_END:
6161 alignment = Layout.Alignment.ALIGN_OPPOSITE;
6162 break;
6163 case CENTER:
6164 alignment = Layout.Alignment.ALIGN_CENTER;
6165 break;
6166 case VIEW_START:
6167 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6168 Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
6169 break;
6170 case VIEW_END:
6171 alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
6172 Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
6173 break;
6174 default:
6175 alignment = Layout.Alignment.ALIGN_NORMAL;
6176 break;
6177 }
6178 mLayoutAlignment = alignment;
6179 }
6180 return mLayoutAlignment;
6181 }
6182
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006183 /**
6184 * The width passed in is now the desired layout width,
6185 * not the full view width with padding.
6186 * {@hide}
6187 */
Gilles Debunne287d6c62011-10-05 18:22:11 -07006188 protected void makeNewLayout(int wantWidth, int hintWidth,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006189 BoringLayout.Metrics boring,
6190 BoringLayout.Metrics hintBoring,
6191 int ellipsisWidth, boolean bringIntoView) {
6192 stopMarquee();
6193
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006194 // Update "old" cached values
6195 mOldMaximum = mMaximum;
6196 mOldMaxMode = mMaxMode;
6197
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006198 mHighlightPathBogus = true;
6199
Gilles Debunne287d6c62011-10-05 18:22:11 -07006200 if (wantWidth < 0) {
6201 wantWidth = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006202 }
6203 if (hintWidth < 0) {
6204 hintWidth = 0;
6205 }
6206
Doug Feltc0ccf0c2011-06-23 16:13:18 -07006207 Layout.Alignment alignment = getLayoutAlignment();
Romain Guy4dc4f732009-06-19 15:16:40 -07006208 boolean shouldEllipsize = mEllipsize != null && mInput == null;
Adam Powell282e3772011-08-30 16:51:11 -07006209 final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
6210 mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
6211 TruncateAt effectiveEllipsize = mEllipsize;
6212 if (mEllipsize == TruncateAt.MARQUEE &&
6213 mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
Fabrice Di Megliocb332642011-09-23 19:08:04 -07006214 effectiveEllipsize = TruncateAt.END_SMALL;
Adam Powell282e3772011-08-30 16:51:11 -07006215 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006216
Doug Feltcb3791202011-07-07 11:57:48 -07006217 if (mTextDir == null) {
6218 resolveTextDirection();
6219 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006220
Gilles Debunne287d6c62011-10-05 18:22:11 -07006221 mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
Adam Powell282e3772011-08-30 16:51:11 -07006222 effectiveEllipsize, effectiveEllipsize == mEllipsize);
6223 if (switchEllipsize) {
6224 TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
6225 TruncateAt.END : TruncateAt.MARQUEE;
Gilles Debunne287d6c62011-10-05 18:22:11 -07006226 mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
Adam Powell282e3772011-08-30 16:51:11 -07006227 shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006228 }
6229
Romain Guy4dc4f732009-06-19 15:16:40 -07006230 shouldEllipsize = mEllipsize != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006231 mHintLayout = null;
6232
6233 if (mHint != null) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006234 if (shouldEllipsize) hintWidth = wantWidth;
Romain Guy4dc4f732009-06-19 15:16:40 -07006235
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006236 if (hintBoring == UNKNOWN_BORING) {
Doug Feltcb3791202011-07-07 11:57:48 -07006237 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006238 mHintBoring);
6239 if (hintBoring != null) {
6240 mHintBoring = hintBoring;
6241 }
6242 }
6243
6244 if (hintBoring != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006245 if (hintBoring.width <= hintWidth &&
6246 (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006247 if (mSavedHintLayout != null) {
6248 mHintLayout = mSavedHintLayout.
6249 replaceOrMake(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006250 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6251 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006252 } else {
6253 mHintLayout = BoringLayout.make(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07006254 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6255 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006256 }
6257
6258 mSavedHintLayout = (BoringLayout) mHintLayout;
Romain Guy4dc4f732009-06-19 15:16:40 -07006259 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
6260 if (mSavedHintLayout != null) {
6261 mHintLayout = mSavedHintLayout.
6262 replaceOrMake(mHint, mTextPaint,
6263 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6264 hintBoring, mIncludePad, mEllipsize,
6265 ellipsisWidth);
6266 } else {
6267 mHintLayout = BoringLayout.make(mHint, mTextPaint,
6268 hintWidth, alignment, mSpacingMult, mSpacingAdd,
6269 hintBoring, mIncludePad, mEllipsize,
6270 ellipsisWidth);
6271 }
6272 } else if (shouldEllipsize) {
6273 mHintLayout = new StaticLayout(mHint,
6274 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006275 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006276 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006277 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006278 } else {
6279 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006280 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006281 mIncludePad);
6282 }
Romain Guy4dc4f732009-06-19 15:16:40 -07006283 } else if (shouldEllipsize) {
6284 mHintLayout = new StaticLayout(mHint,
6285 0, mHint.length(),
Doug Feltcb3791202011-07-07 11:57:48 -07006286 mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07006287 mSpacingAdd, mIncludePad, mEllipsize,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006288 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006289 } else {
6290 mHintLayout = new StaticLayout(mHint, mTextPaint,
Doug Feltcb3791202011-07-07 11:57:48 -07006291 hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006292 mIncludePad);
6293 }
6294 }
6295
6296 if (bringIntoView) {
6297 registerForPreDraw();
6298 }
6299
6300 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
Romain Guy939151f2009-04-08 14:22:40 -07006301 if (!compressText(ellipsisWidth)) {
6302 final int height = mLayoutParams.height;
6303 // If the size of the view does not depend on the size of the text, try to
6304 // start the marquee immediately
Romain Guy980a9382010-01-08 15:06:28 -08006305 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
Romain Guy939151f2009-04-08 14:22:40 -07006306 startMarquee();
6307 } else {
6308 // Defer the start of the marquee until we know our width (see setFrame())
6309 mRestartMarquee = true;
6310 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006311 }
6312 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07006313
6314 // CursorControllers need a non-null mLayout
6315 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006316 }
6317
Gilles Debunne287d6c62011-10-05 18:22:11 -07006318 private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Adam Powell282e3772011-08-30 16:51:11 -07006319 Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
6320 boolean useSaved) {
6321 Layout result = null;
6322 if (mText instanceof Spannable) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006323 result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
Adam Powell282e3772011-08-30 16:51:11 -07006324 alignment, mTextDir, mSpacingMult,
6325 mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
Fabrice Di Meglioad0b0512011-10-04 17:21:26 -07006326 ellipsisWidth);
Adam Powell282e3772011-08-30 16:51:11 -07006327 } else {
6328 if (boring == UNKNOWN_BORING) {
6329 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
6330 if (boring != null) {
6331 mBoring = boring;
6332 }
6333 }
6334
6335 if (boring != null) {
Gilles Debunne287d6c62011-10-05 18:22:11 -07006336 if (boring.width <= wantWidth &&
Adam Powell282e3772011-08-30 16:51:11 -07006337 (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
6338 if (useSaved && mSavedLayout != null) {
6339 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006340 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006341 boring, mIncludePad);
6342 } else {
6343 result = BoringLayout.make(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006344 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006345 boring, mIncludePad);
6346 }
6347
6348 if (useSaved) {
6349 mSavedLayout = (BoringLayout) result;
6350 }
Gilles Debunne287d6c62011-10-05 18:22:11 -07006351 } else if (shouldEllipsize && boring.width <= wantWidth) {
Adam Powell282e3772011-08-30 16:51:11 -07006352 if (useSaved && mSavedLayout != null) {
6353 result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006354 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006355 boring, mIncludePad, effectiveEllipsize,
6356 ellipsisWidth);
6357 } else {
6358 result = BoringLayout.make(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006359 wantWidth, alignment, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006360 boring, mIncludePad, effectiveEllipsize,
6361 ellipsisWidth);
6362 }
6363 } else if (shouldEllipsize) {
6364 result = new StaticLayout(mTransformed,
6365 0, mTransformed.length(),
Gilles Debunne287d6c62011-10-05 18:22:11 -07006366 mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
Adam Powell282e3772011-08-30 16:51:11 -07006367 mSpacingAdd, mIncludePad, effectiveEllipsize,
6368 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6369 } else {
6370 result = new StaticLayout(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006371 wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006372 mIncludePad);
6373 }
6374 } else if (shouldEllipsize) {
6375 result = new StaticLayout(mTransformed,
6376 0, mTransformed.length(),
Gilles Debunne287d6c62011-10-05 18:22:11 -07006377 mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
Adam Powell282e3772011-08-30 16:51:11 -07006378 mSpacingAdd, mIncludePad, effectiveEllipsize,
6379 ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
6380 } else {
6381 result = new StaticLayout(mTransformed, mTextPaint,
Gilles Debunne287d6c62011-10-05 18:22:11 -07006382 wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
Adam Powell282e3772011-08-30 16:51:11 -07006383 mIncludePad);
6384 }
6385 }
6386 return result;
6387 }
6388
Romain Guy939151f2009-04-08 14:22:40 -07006389 private boolean compressText(float width) {
Romain Guy2bffd262010-09-12 17:40:02 -07006390 if (isHardwareAccelerated()) return false;
6391
Romain Guy3373ed62009-05-04 14:13:32 -07006392 // Only compress the text if it hasn't been compressed by the previous pass
6393 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
6394 mTextPaint.getTextScaleX() == 1.0f) {
Romain Guy939151f2009-04-08 14:22:40 -07006395 final float textWidth = mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07006396 final float overflow = (textWidth + 1.0f - width) / width;
Romain Guy939151f2009-04-08 14:22:40 -07006397 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
6398 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
6399 post(new Runnable() {
6400 public void run() {
6401 requestLayout();
6402 }
6403 });
6404 return true;
6405 }
6406 }
6407
6408 return false;
6409 }
6410
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006411 private static int desired(Layout layout) {
6412 int n = layout.getLineCount();
6413 CharSequence text = layout.getText();
6414 float max = 0;
6415
6416 // if any line was wrapped, we can't use it.
6417 // but it's ok for the last line not to have a newline
6418
6419 for (int i = 0; i < n - 1; i++) {
6420 if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
6421 return -1;
6422 }
6423
6424 for (int i = 0; i < n; i++) {
6425 max = Math.max(max, layout.getLineWidth(i));
6426 }
6427
6428 return (int) FloatMath.ceil(max);
6429 }
6430
6431 /**
6432 * Set whether the TextView includes extra top and bottom padding to make
6433 * room for accents that go above the normal ascent and descent.
6434 * The default is true.
6435 *
6436 * @attr ref android.R.styleable#TextView_includeFontPadding
6437 */
6438 public void setIncludeFontPadding(boolean includepad) {
Gilles Debunne22378292011-08-12 10:38:52 -07006439 if (mIncludePad != includepad) {
6440 mIncludePad = includepad;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006441
Gilles Debunne22378292011-08-12 10:38:52 -07006442 if (mLayout != null) {
6443 nullLayouts();
6444 requestLayout();
6445 invalidate();
6446 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006447 }
6448 }
6449
Romain Guy4dc4f732009-06-19 15:16:40 -07006450 private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006451
6452 @Override
6453 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
6454 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
6455 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
6456 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
6457 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
6458
6459 int width;
6460 int height;
6461
6462 BoringLayout.Metrics boring = UNKNOWN_BORING;
6463 BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
6464
Doug Feltcb3791202011-07-07 11:57:48 -07006465 if (mTextDir == null) {
6466 resolveTextDirection();
6467 }
6468
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006469 int des = -1;
6470 boolean fromexisting = false;
6471
6472 if (widthMode == MeasureSpec.EXACTLY) {
6473 // Parent has told us how big to be. So be it.
6474 width = widthSize;
6475 } else {
6476 if (mLayout != null && mEllipsize == null) {
6477 des = desired(mLayout);
6478 }
6479
6480 if (des < 0) {
Doug Feltcb3791202011-07-07 11:57:48 -07006481 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006482 if (boring != null) {
6483 mBoring = boring;
6484 }
6485 } else {
6486 fromexisting = true;
6487 }
6488
6489 if (boring == null || boring == UNKNOWN_BORING) {
6490 if (des < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006491 des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006492 }
6493
6494 width = des;
6495 } else {
6496 width = boring.width;
6497 }
6498
6499 final Drawables dr = mDrawables;
6500 if (dr != null) {
6501 width = Math.max(width, dr.mDrawableWidthTop);
6502 width = Math.max(width, dr.mDrawableWidthBottom);
6503 }
6504
6505 if (mHint != null) {
6506 int hintDes = -1;
6507 int hintWidth;
6508
Romain Guy4dc4f732009-06-19 15:16:40 -07006509 if (mHintLayout != null && mEllipsize == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006510 hintDes = desired(mHintLayout);
6511 }
6512
6513 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006514 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006515 if (hintBoring != null) {
6516 mHintBoring = hintBoring;
6517 }
6518 }
6519
6520 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
6521 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006522 hintDes = (int) FloatMath.ceil(
6523 Layout.getDesiredWidth(mHint, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006524 }
6525
6526 hintWidth = hintDes;
6527 } else {
6528 hintWidth = hintBoring.width;
6529 }
6530
6531 if (hintWidth > width) {
6532 width = hintWidth;
6533 }
6534 }
6535
6536 width += getCompoundPaddingLeft() + getCompoundPaddingRight();
6537
6538 if (mMaxWidthMode == EMS) {
6539 width = Math.min(width, mMaxWidth * getLineHeight());
6540 } else {
6541 width = Math.min(width, mMaxWidth);
6542 }
6543
6544 if (mMinWidthMode == EMS) {
6545 width = Math.max(width, mMinWidth * getLineHeight());
6546 } else {
6547 width = Math.max(width, mMinWidth);
6548 }
6549
6550 // Check against our minimum width
6551 width = Math.max(width, getSuggestedMinimumWidth());
6552
6553 if (widthMode == MeasureSpec.AT_MOST) {
6554 width = Math.min(widthSize, width);
6555 }
6556 }
6557
6558 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
6559 int unpaddedWidth = want;
Gilles Debunne9a80a652011-01-31 12:56:07 -08006560
Jeff Brown033a0012011-11-11 15:30:16 -08006561 if (mHorizontallyScrolling) want = VERY_WIDE;
Gilles Debunne9a80a652011-01-31 12:56:07 -08006562
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006563 int hintWant = want;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006564 int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006565
6566 if (mLayout == null) {
6567 makeNewLayout(want, hintWant, boring, hintBoring,
Romain Guy4dc4f732009-06-19 15:16:40 -07006568 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006569 } else {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07006570 final boolean layoutChanged = (mLayout.getWidth() != want) ||
6571 (hintWidth != hintWant) ||
6572 (mLayout.getEllipsizedWidth() !=
6573 width - getCompoundPaddingLeft() - getCompoundPaddingRight());
6574
6575 final boolean widthChanged = (mHint == null) &&
6576 (mEllipsize == null) &&
6577 (want > mLayout.getWidth()) &&
6578 (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
6579
6580 final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
6581
6582 if (layoutChanged || maximumChanged) {
6583 if (!maximumChanged && widthChanged) {
6584 mLayout.increaseWidthTo(want);
6585 } else {
6586 makeNewLayout(want, hintWant, boring, hintBoring,
6587 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
6588 }
6589 } else {
6590 // Nothing has changed
6591 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006592 }
6593
6594 if (heightMode == MeasureSpec.EXACTLY) {
6595 // Parent has told us how big to be. So be it.
6596 height = heightSize;
6597 mDesiredHeightAtMeasure = -1;
6598 } else {
6599 int desired = getDesiredHeight();
6600
6601 height = desired;
6602 mDesiredHeightAtMeasure = desired;
6603
6604 if (heightMode == MeasureSpec.AT_MOST) {
Christoffer Gurell1d05c7c2009-10-12 15:53:39 +02006605 height = Math.min(desired, heightSize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006606 }
6607 }
6608
Romain Guy4dc4f732009-06-19 15:16:40 -07006609 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006610 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
Romain Guy4dc4f732009-06-19 15:16:40 -07006611 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006612 }
6613
6614 /*
6615 * We didn't let makeNewLayout() register to bring the cursor into view,
6616 * so do it here if there is any possibility that it is needed.
6617 */
6618 if (mMovement != null ||
6619 mLayout.getWidth() > unpaddedWidth ||
6620 mLayout.getHeight() > unpaddedHeight) {
6621 registerForPreDraw();
6622 } else {
6623 scrollTo(0, 0);
6624 }
6625
6626 setMeasuredDimension(width, height);
6627 }
6628
6629 private int getDesiredHeight() {
Romain Guy4dc4f732009-06-19 15:16:40 -07006630 return Math.max(
6631 getDesiredHeight(mLayout, true),
6632 getDesiredHeight(mHintLayout, mEllipsize != null));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006633 }
6634
6635 private int getDesiredHeight(Layout layout, boolean cap) {
6636 if (layout == null) {
6637 return 0;
6638 }
6639
6640 int linecount = layout.getLineCount();
6641 int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
6642 int desired = layout.getLineTop(linecount);
6643
6644 final Drawables dr = mDrawables;
6645 if (dr != null) {
6646 desired = Math.max(desired, dr.mDrawableHeightLeft);
6647 desired = Math.max(desired, dr.mDrawableHeightRight);
6648 }
6649
6650 desired += pad;
6651
6652 if (mMaxMode == LINES) {
6653 /*
6654 * Don't cap the hint to a certain number of lines.
6655 * (Do cap it, though, if we have a maximum pixel height.)
6656 */
6657 if (cap) {
6658 if (linecount > mMaximum) {
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08006659 desired = layout.getLineTop(mMaximum);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006660
6661 if (dr != null) {
6662 desired = Math.max(desired, dr.mDrawableHeightLeft);
6663 desired = Math.max(desired, dr.mDrawableHeightRight);
6664 }
6665
6666 desired += pad;
6667 linecount = mMaximum;
6668 }
6669 }
6670 } else {
6671 desired = Math.min(desired, mMaximum);
6672 }
6673
6674 if (mMinMode == LINES) {
6675 if (linecount < mMinimum) {
6676 desired += getLineHeight() * (mMinimum - linecount);
6677 }
6678 } else {
6679 desired = Math.max(desired, mMinimum);
6680 }
6681
6682 // Check against our minimum height
6683 desired = Math.max(desired, getSuggestedMinimumHeight());
6684
6685 return desired;
6686 }
6687
6688 /**
6689 * Check whether a change to the existing text layout requires a
6690 * new view layout.
6691 */
6692 private void checkForResize() {
6693 boolean sizeChanged = false;
6694
6695 if (mLayout != null) {
6696 // Check if our width changed
6697 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
6698 sizeChanged = true;
6699 invalidate();
6700 }
6701
6702 // Check if our height changed
6703 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6704 int desiredHeight = getDesiredHeight();
6705
6706 if (desiredHeight != this.getHeight()) {
6707 sizeChanged = true;
6708 }
Romain Guy980a9382010-01-08 15:06:28 -08006709 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006710 if (mDesiredHeightAtMeasure >= 0) {
6711 int desiredHeight = getDesiredHeight();
6712
6713 if (desiredHeight != mDesiredHeightAtMeasure) {
6714 sizeChanged = true;
6715 }
6716 }
6717 }
6718 }
6719
6720 if (sizeChanged) {
6721 requestLayout();
6722 // caller will have already invalidated
6723 }
6724 }
6725
6726 /**
6727 * Check whether entirely new text requires a new view layout
6728 * or merely a new text layout.
6729 */
6730 private void checkForRelayout() {
6731 // If we have a fixed width, we can just swap in a new text layout
6732 // if the text height stays the same or if the view height is fixed.
6733
6734 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6735 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6736 (mHint == null || mHintLayout != null) &&
6737 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6738 // Static width, so try making a new text layout.
6739
6740 int oldht = mLayout.getHeight();
6741 int want = mLayout.getWidth();
6742 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6743
6744 /*
6745 * No need to bring the text into view, since the size is not
6746 * changing (unless we do the requestLayout(), in which case it
6747 * will happen at measure).
6748 */
6749 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
Romain Guye1e0dc82009-11-03 17:21:04 -08006750 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6751 false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006752
Romain Guye1e0dc82009-11-03 17:21:04 -08006753 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6754 // In a fixed-height view, so use our new text layout.
6755 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
Romain Guy980a9382010-01-08 15:06:28 -08006756 mLayoutParams.height != LayoutParams.MATCH_PARENT) {
Romain Guye1e0dc82009-11-03 17:21:04 -08006757 invalidate();
6758 return;
6759 }
6760
6761 // Dynamic height, but height has stayed the same,
6762 // so use our new text layout.
6763 if (mLayout.getHeight() == oldht &&
6764 (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6765 invalidate();
6766 return;
6767 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006768 }
6769
6770 // We lose: the height has changed and we have a dynamic height.
6771 // Request a new view layout using our new text layout.
6772 requestLayout();
6773 invalidate();
6774 } else {
6775 // Dynamic width, so we have no choice but to request a new
6776 // view layout with a new text layout.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006777 nullLayouts();
6778 requestLayout();
6779 invalidate();
6780 }
6781 }
6782
6783 /**
6784 * Returns true if anything changed.
6785 */
6786 private boolean bringTextIntoView() {
6787 int line = 0;
6788 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6789 line = mLayout.getLineCount() - 1;
6790 }
6791
6792 Layout.Alignment a = mLayout.getParagraphAlignment(line);
6793 int dir = mLayout.getParagraphDirection(line);
6794 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6795 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6796 int ht = mLayout.getHeight();
6797
6798 int scrollx, scrolly;
6799
Doug Felt25b9f422011-07-11 13:48:37 -07006800 // Convert to left, center, or right alignment.
6801 if (a == Layout.Alignment.ALIGN_NORMAL) {
6802 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT :
6803 Layout.Alignment.ALIGN_RIGHT;
6804 } else if (a == Layout.Alignment.ALIGN_OPPOSITE){
6805 a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT :
6806 Layout.Alignment.ALIGN_LEFT;
6807 }
6808
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006809 if (a == Layout.Alignment.ALIGN_CENTER) {
6810 /*
6811 * Keep centered if possible, or, if it is too wide to fit,
6812 * keep leading edge in view.
6813 */
6814
6815 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6816 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6817
6818 if (right - left < hspace) {
6819 scrollx = (right + left) / 2 - hspace / 2;
6820 } else {
6821 if (dir < 0) {
6822 scrollx = right - hspace;
6823 } else {
6824 scrollx = left;
6825 }
6826 }
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006827 } else if (a == Layout.Alignment.ALIGN_RIGHT) {
Doug Felt25b9f422011-07-11 13:48:37 -07006828 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6829 scrollx = right - hspace;
Fabrice Di Megliod2b5d1c2011-07-13 19:38:17 -07006830 } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
6831 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006832 }
6833
6834 if (ht < vspace) {
6835 scrolly = 0;
6836 } else {
6837 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6838 scrolly = ht - vspace;
6839 } else {
6840 scrolly = 0;
6841 }
6842 }
6843
6844 if (scrollx != mScrollX || scrolly != mScrollY) {
6845 scrollTo(scrollx, scrolly);
6846 return true;
6847 } else {
6848 return false;
6849 }
6850 }
6851
6852 /**
6853 * Move the point, specified by the offset, into the view if it is needed.
6854 * This has to be called after layout. Returns true if anything changed.
6855 */
6856 public boolean bringPointIntoView(int offset) {
6857 boolean changed = false;
6858
Gilles Debunne176ee3d2011-07-16 13:28:41 -07006859 if (mLayout == null) return changed;
6860
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006861 int line = mLayout.getLineForOffset(offset);
6862
6863 // FIXME: Is it okay to truncate this, or should we round?
6864 final int x = (int)mLayout.getPrimaryHorizontal(offset);
6865 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006866 final int bottom = mLayout.getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006867
6868 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6869 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6870 int ht = mLayout.getHeight();
6871
6872 int grav;
6873
6874 switch (mLayout.getParagraphAlignment(line)) {
Doug Felt25b9f422011-07-11 13:48:37 -07006875 case ALIGN_LEFT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006876 grav = 1;
6877 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006878 case ALIGN_RIGHT:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006879 grav = -1;
6880 break;
Doug Felt25b9f422011-07-11 13:48:37 -07006881 case ALIGN_NORMAL:
6882 grav = mLayout.getParagraphDirection(line);
6883 break;
6884 case ALIGN_OPPOSITE:
6885 grav = -mLayout.getParagraphDirection(line);
6886 break;
6887 case ALIGN_CENTER:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006888 default:
6889 grav = 0;
Doug Felt25b9f422011-07-11 13:48:37 -07006890 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006891 }
6892
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006893 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6894 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6895
6896 int hslack = (bottom - top) / 2;
6897 int vslack = hslack;
6898
6899 if (vslack > vspace / 4)
6900 vslack = vspace / 4;
6901 if (hslack > hspace / 4)
6902 hslack = hspace / 4;
6903
6904 int hs = mScrollX;
6905 int vs = mScrollY;
6906
6907 if (top - vs < vslack)
6908 vs = top - vslack;
6909 if (bottom - vs > vspace - vslack)
6910 vs = bottom - (vspace - vslack);
6911 if (ht - vs < vspace)
6912 vs = ht - vspace;
6913 if (0 - vs > 0)
6914 vs = 0;
6915
6916 if (grav != 0) {
6917 if (x - hs < hslack) {
6918 hs = x - hslack;
6919 }
6920 if (x - hs > hspace - hslack) {
6921 hs = x - (hspace - hslack);
6922 }
6923 }
6924
6925 if (grav < 0) {
6926 if (left - hs > 0)
6927 hs = left;
6928 if (right - hs < hspace)
6929 hs = right - hspace;
6930 } else if (grav > 0) {
6931 if (right - hs < hspace)
6932 hs = right - hspace;
6933 if (left - hs > 0)
6934 hs = left;
6935 } else /* grav == 0 */ {
6936 if (right - left <= hspace) {
6937 /*
6938 * If the entire text fits, center it exactly.
6939 */
6940 hs = left - (hspace - (right - left)) / 2;
6941 } else if (x > right - hslack) {
6942 /*
6943 * If we are near the right edge, keep the right edge
6944 * at the edge of the view.
6945 */
6946 hs = right - hspace;
6947 } else if (x < left + hslack) {
6948 /*
6949 * If we are near the left edge, keep the left edge
6950 * at the edge of the view.
6951 */
6952 hs = left;
6953 } else if (left > hs) {
6954 /*
6955 * Is there whitespace visible at the left? Fix it if so.
6956 */
6957 hs = left;
6958 } else if (right < hs + hspace) {
6959 /*
6960 * Is there whitespace visible at the right? Fix it if so.
6961 */
6962 hs = right - hspace;
6963 } else {
6964 /*
6965 * Otherwise, float as needed.
6966 */
6967 if (x - hs < hslack) {
6968 hs = x - hslack;
6969 }
6970 if (x - hs > hspace - hslack) {
6971 hs = x - (hspace - hslack);
6972 }
6973 }
6974 }
6975
6976 if (hs != mScrollX || vs != mScrollY) {
6977 if (mScroller == null) {
6978 scrollTo(hs, vs);
6979 } else {
6980 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6981 int dx = hs - mScrollX;
6982 int dy = vs - mScrollY;
6983
6984 if (duration > ANIMATED_SCROLL_GAP) {
6985 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
Mike Cleronf116bf82009-09-27 19:14:12 -07006986 awakenScrollBars(mScroller.getDuration());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006987 invalidate();
6988 } else {
6989 if (!mScroller.isFinished()) {
6990 mScroller.abortAnimation();
6991 }
6992
6993 scrollBy(dx, dy);
6994 }
6995
6996 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6997 }
6998
6999 changed = true;
7000 }
7001
7002 if (isFocused()) {
Gilles Debunne716dbf62011-03-07 18:12:10 -08007003 // This offsets because getInterestingRect() is in terms of viewport coordinates, but
7004 // requestRectangleOnScreen() is in terms of content coordinates.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007005
Gilles Debunne716dbf62011-03-07 18:12:10 -08007006 if (mTempRect == null) mTempRect = new Rect();
Dianne Hackborn70a3f672011-08-08 14:32:41 -07007007 // The offsets here are to ensure the rectangle we are using is
7008 // within our view bounds, in case the cursor is on the far left
7009 // or right. If it isn't withing the bounds, then this request
7010 // will be ignored.
7011 mTempRect.set(x - 2, top, x + 2, bottom);
Gilles Debunne716dbf62011-03-07 18:12:10 -08007012 getInterestingRect(mTempRect, line);
7013 mTempRect.offset(mScrollX, mScrollY);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007014
Gilles Debunne716dbf62011-03-07 18:12:10 -08007015 if (requestRectangleOnScreen(mTempRect)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007016 changed = true;
7017 }
7018 }
7019
7020 return changed;
7021 }
7022
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007023 /**
7024 * Move the cursor, if needed, so that it is at an offset that is visible
7025 * to the user. This will not move the cursor if it represents more than
7026 * one character (a selection range). This will only work if the
7027 * TextView contains spannable text; otherwise it will do nothing.
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07007028 *
Gilles Debunne57f4e5b2010-06-21 16:21:51 -07007029 * @return True if the cursor was actually moved, false otherwise.
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007030 */
7031 public boolean moveCursorToVisibleOffset() {
7032 if (!(mText instanceof Spannable)) {
7033 return false;
7034 }
Gilles Debunne05336272010-07-09 20:13:45 -07007035 int start = getSelectionStart();
7036 int end = getSelectionEnd();
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007037 if (start != end) {
7038 return false;
7039 }
7040
7041 // First: make sure the line is visible on screen:
7042
7043 int line = mLayout.getLineForOffset(start);
7044
7045 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007046 final int bottom = mLayout.getLineTop(line + 1);
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007047 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
7048 int vslack = (bottom - top) / 2;
7049 if (vslack > vspace / 4)
7050 vslack = vspace / 4;
7051 final int vs = mScrollY;
7052
7053 if (top < (vs+vslack)) {
7054 line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
7055 } else if (bottom > (vspace+vs-vslack)) {
7056 line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
7057 }
7058
7059 // Next: make sure the character is visible on screen:
7060
7061 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
7062 final int hs = mScrollX;
7063 final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
7064 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
7065
Doug Feltc982f602010-05-25 11:51:40 -07007066 // line might contain bidirectional text
7067 final int lowChar = leftChar < rightChar ? leftChar : rightChar;
7068 final int highChar = leftChar > rightChar ? leftChar : rightChar;
7069
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007070 int newStart = start;
Doug Feltc982f602010-05-25 11:51:40 -07007071 if (newStart < lowChar) {
7072 newStart = lowChar;
7073 } else if (newStart > highChar) {
7074 newStart = highChar;
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07007075 }
7076
7077 if (newStart != start) {
7078 Selection.setSelection((Spannable)mText, newStart);
7079 return true;
7080 }
7081
7082 return false;
7083 }
7084
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007085 @Override
7086 public void computeScroll() {
7087 if (mScroller != null) {
7088 if (mScroller.computeScrollOffset()) {
7089 mScrollX = mScroller.getCurrX();
7090 mScrollY = mScroller.getCurrY();
Romain Guy0fd89bf2011-01-26 15:41:30 -08007091 invalidateParentCaches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007092 postInvalidate(); // So we draw again
7093 }
7094 }
7095 }
7096
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007097 private void getInterestingRect(Rect r, int line) {
7098 convertFromViewportToContentCoordinates(r);
7099
7100 // Rectangle can can be expanded on first and last line to take
7101 // padding into account.
7102 // TODO Take left/right padding into account too?
7103 if (line == 0) r.top -= getExtendedPaddingTop();
7104 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
7105 }
7106
7107 private void convertFromViewportToContentCoordinates(Rect r) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007108 final int horizontalOffset = viewportToContentHorizontalOffset();
7109 r.left += horizontalOffset;
7110 r.right += horizontalOffset;
7111
7112 final int verticalOffset = viewportToContentVerticalOffset();
7113 r.top += verticalOffset;
7114 r.bottom += verticalOffset;
7115 }
7116
7117 private int viewportToContentHorizontalOffset() {
7118 return getCompoundPaddingLeft() - mScrollX;
7119 }
7120
7121 private int viewportToContentVerticalOffset() {
7122 int offset = getExtendedPaddingTop() - mScrollY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007123 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007124 offset += getVerticalOffset(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007125 }
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07007126 return offset;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007127 }
7128
7129 @Override
7130 public void debug(int depth) {
7131 super.debug(depth);
7132
7133 String output = debugIndent(depth);
7134 output += "frame={" + mLeft + ", " + mTop + ", " + mRight
7135 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
7136 + "} ";
7137
7138 if (mText != null) {
7139
7140 output += "mText=\"" + mText + "\" ";
7141 if (mLayout != null) {
7142 output += "mLayout width=" + mLayout.getWidth()
7143 + " height=" + mLayout.getHeight();
7144 }
7145 } else {
7146 output += "mText=NULL";
7147 }
7148 Log.d(VIEW_LOG_TAG, output);
7149 }
7150
7151 /**
7152 * Convenience for {@link Selection#getSelectionStart}.
7153 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007154 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007155 public int getSelectionStart() {
7156 return Selection.getSelectionStart(getText());
7157 }
7158
7159 /**
7160 * Convenience for {@link Selection#getSelectionEnd}.
7161 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07007162 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007163 public int getSelectionEnd() {
7164 return Selection.getSelectionEnd(getText());
7165 }
7166
7167 /**
7168 * Return true iff there is a selection inside this text view.
7169 */
7170 public boolean hasSelection() {
Gilles Debunne03789e82010-09-07 19:07:17 -07007171 final int selectionStart = getSelectionStart();
7172 final int selectionEnd = getSelectionEnd();
7173
7174 return selectionStart >= 0 && selectionStart != selectionEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007175 }
7176
7177 /**
7178 * Sets the properties of this field (lines, horizontally scrolling,
7179 * transformation method) to be for a single-line input.
7180 *
7181 * @attr ref android.R.styleable#TextView_singleLine
7182 */
7183 public void setSingleLine() {
7184 setSingleLine(true);
7185 }
7186
7187 /**
Adam Powell7f8f79a2011-07-07 18:35:54 -07007188 * Sets the properties of this field to transform input to ALL CAPS
7189 * display. This may use a "small caps" formatting if available.
7190 * This setting will be ignored if this field is editable or selectable.
7191 *
7192 * This call replaces the current transformation method. Disabling this
7193 * will not necessarily restore the previous behavior from before this
7194 * was enabled.
7195 *
7196 * @see #setTransformationMethod(TransformationMethod)
7197 * @attr ref android.R.styleable#TextView_textAllCaps
7198 */
7199 public void setAllCaps(boolean allCaps) {
7200 if (allCaps) {
7201 setTransformationMethod(new AllCapsTransformationMethod(getContext()));
7202 } else {
7203 setTransformationMethod(null);
7204 }
7205 }
7206
7207 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007208 * If true, sets the properties of this field (number of lines, horizontally scrolling,
7209 * transformation method) to be for a single-line input; if false, restores these to the default
7210 * conditions.
7211 *
7212 * Note that the default conditions are not necessarily those that were in effect prior this
7213 * method, and you may want to reset these properties to your custom values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007214 *
7215 * @attr ref android.R.styleable#TextView_singleLine
7216 */
7217 @android.view.RemotableViewMethod
7218 public void setSingleLine(boolean singleLine) {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007219 // Could be used, but may break backward compatibility.
7220 // if (mSingleLine == singleLine) return;
Gilles Debunned7483bf2010-11-10 10:47:45 -08007221 setInputTypeSingleLine(singleLine);
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007222 applySingleLine(singleLine, true, true);
Gilles Debunned7483bf2010-11-10 10:47:45 -08007223 }
7224
7225 /**
7226 * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
7227 * @param singleLine
7228 */
7229 private void setInputTypeSingleLine(boolean singleLine) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007230 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007231 if (singleLine) {
7232 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7233 } else {
7234 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
7235 }
7236 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007237 }
7238
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007239 private void applySingleLine(boolean singleLine, boolean applyTransformation,
7240 boolean changeMaxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007241 mSingleLine = singleLine;
7242 if (singleLine) {
7243 setLines(1);
7244 setHorizontallyScrolling(true);
7245 if (applyTransformation) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08007246 setTransformationMethod(SingleLineTransformationMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007247 }
7248 } else {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08007249 if (changeMaxLines) {
7250 setMaxLines(Integer.MAX_VALUE);
7251 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007252 setHorizontallyScrolling(false);
7253 if (applyTransformation) {
7254 setTransformationMethod(null);
7255 }
7256 }
7257 }
Gilles Debunneb2316962010-12-21 17:32:43 -08007258
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007259 /**
7260 * Causes words in the text that are longer than the view is wide
7261 * to be ellipsized instead of broken in the middle. You may also
7262 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
Kenny Roote855d132009-06-11 11:00:42 -05007263 * to constrain the text to a single line. Use <code>null</code>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007264 * to turn off ellipsizing.
7265 *
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007266 * If {@link #setMaxLines} has been used to set two or more lines,
Gilles Debunne6435a562011-08-04 21:22:30 -07007267 * {@link android.text.TextUtils.TruncateAt#END} and
7268 * {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported
7269 * (other ellipsizing types will not do anything).
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07007270 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007271 * @attr ref android.R.styleable#TextView_ellipsize
7272 */
7273 public void setEllipsize(TextUtils.TruncateAt where) {
Gilles Debunne22378292011-08-12 10:38:52 -07007274 // TruncateAt is an enum. != comparison is ok between these singleton objects.
7275 if (mEllipsize != where) {
7276 mEllipsize = where;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007277
Gilles Debunne22378292011-08-12 10:38:52 -07007278 if (mLayout != null) {
7279 nullLayouts();
7280 requestLayout();
7281 invalidate();
7282 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007283 }
7284 }
7285
7286 /**
7287 * Sets how many times to repeat the marquee animation. Only applied if the
7288 * TextView has marquee enabled. Set to -1 to repeat indefinitely.
7289 *
7290 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
7291 */
7292 public void setMarqueeRepeatLimit(int marqueeLimit) {
7293 mMarqueeRepeatLimit = marqueeLimit;
7294 }
7295
7296 /**
7297 * Returns where, if anywhere, words that are longer than the view
7298 * is wide should be ellipsized.
7299 */
7300 @ViewDebug.ExportedProperty
7301 public TextUtils.TruncateAt getEllipsize() {
7302 return mEllipsize;
7303 }
7304
7305 /**
7306 * Set the TextView so that when it takes focus, all the text is
7307 * selected.
7308 *
7309 * @attr ref android.R.styleable#TextView_selectAllOnFocus
7310 */
7311 @android.view.RemotableViewMethod
7312 public void setSelectAllOnFocus(boolean selectAllOnFocus) {
7313 mSelectAllOnFocus = selectAllOnFocus;
7314
7315 if (selectAllOnFocus && !(mText instanceof Spannable)) {
7316 setText(mText, BufferType.SPANNABLE);
7317 }
7318 }
7319
7320 /**
7321 * Set whether the cursor is visible. The default is true.
7322 *
7323 * @attr ref android.R.styleable#TextView_cursorVisible
7324 */
7325 @android.view.RemotableViewMethod
7326 public void setCursorVisible(boolean visible) {
Gilles Debunne3d010062011-02-18 14:16:41 -08007327 if (mCursorVisible != visible) {
7328 mCursorVisible = visible;
7329 invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007330
Gilles Debunne3d010062011-02-18 14:16:41 -08007331 makeBlink();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007332
Gilles Debunne3d010062011-02-18 14:16:41 -08007333 // InsertionPointCursorController depends on mCursorVisible
7334 prepareCursorControllers();
7335 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007336 }
7337
Gilles Debunne98dbfd42011-01-24 12:54:10 -08007338 private boolean isCursorVisible() {
7339 return mCursorVisible && isTextEditable();
7340 }
7341
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007342 private boolean canMarquee() {
7343 int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
Adam Powell282e3772011-08-30 16:51:11 -07007344 return width > 0 && (mLayout.getLineWidth(0) > width ||
7345 (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null &&
7346 mSavedMarqueeModeLayout.getLineWidth(0) > width));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007347 }
7348
7349 private void startMarquee() {
Romain Guy4dc4f732009-06-19 15:16:40 -07007350 // Do not ellipsize EditText
7351 if (mInput != null) return;
7352
Romain Guy939151f2009-04-08 14:22:40 -07007353 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
7354 return;
7355 }
7356
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007357 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
7358 getLineCount() == 1 && canMarquee()) {
Romain Guy939151f2009-04-08 14:22:40 -07007359
Adam Powell282e3772011-08-30 16:51:11 -07007360 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
7361 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
7362 final Layout tmp = mLayout;
7363 mLayout = mSavedMarqueeModeLayout;
7364 mSavedMarqueeModeLayout = tmp;
7365 setHorizontalFadingEdgeEnabled(true);
7366 requestLayout();
7367 invalidate();
7368 }
7369
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007370 if (mMarquee == null) mMarquee = new Marquee(this);
7371 mMarquee.start(mMarqueeRepeatLimit);
7372 }
7373 }
7374
7375 private void stopMarquee() {
7376 if (mMarquee != null && !mMarquee.isStopped()) {
7377 mMarquee.stop();
7378 }
Adam Powell282e3772011-08-30 16:51:11 -07007379
7380 if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
7381 mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
7382 final Layout tmp = mSavedMarqueeModeLayout;
7383 mSavedMarqueeModeLayout = mLayout;
7384 mLayout = tmp;
7385 setHorizontalFadingEdgeEnabled(false);
7386 requestLayout();
7387 invalidate();
7388 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007389 }
7390
7391 private void startStopMarquee(boolean start) {
7392 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7393 if (start) {
7394 startMarquee();
7395 } else {
7396 stopMarquee();
7397 }
7398 }
7399 }
7400
7401 private static final class Marquee extends Handler {
7402 // TODO: Add an option to configure this
Romain Guy939151f2009-04-08 14:22:40 -07007403 private static final float MARQUEE_DELTA_MAX = 0.07f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007404 private static final int MARQUEE_DELAY = 1200;
7405 private static final int MARQUEE_RESTART_DELAY = 1200;
7406 private static final int MARQUEE_RESOLUTION = 1000 / 30;
7407 private static final int MARQUEE_PIXELS_PER_SECOND = 30;
7408
7409 private static final byte MARQUEE_STOPPED = 0x0;
7410 private static final byte MARQUEE_STARTING = 0x1;
7411 private static final byte MARQUEE_RUNNING = 0x2;
7412
7413 private static final int MESSAGE_START = 0x1;
7414 private static final int MESSAGE_TICK = 0x2;
7415 private static final int MESSAGE_RESTART = 0x3;
7416
7417 private final WeakReference<TextView> mView;
7418
7419 private byte mStatus = MARQUEE_STOPPED;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007420 private final float mScrollUnit;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007421 private float mMaxScroll;
Romain Guyc2303192009-04-03 17:37:18 -07007422 float mMaxFadeScroll;
7423 private float mGhostStart;
7424 private float mGhostOffset;
7425 private float mFadeStop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007426 private int mRepeatLimit;
7427
7428 float mScroll;
7429
7430 Marquee(TextView v) {
7431 final float density = v.getContext().getResources().getDisplayMetrics().density;
Gilles Debunnee15b3582010-06-16 15:17:21 -07007432 mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007433 mView = new WeakReference<TextView>(v);
7434 }
7435
7436 @Override
7437 public void handleMessage(Message msg) {
7438 switch (msg.what) {
7439 case MESSAGE_START:
7440 mStatus = MARQUEE_RUNNING;
7441 tick();
7442 break;
7443 case MESSAGE_TICK:
7444 tick();
7445 break;
7446 case MESSAGE_RESTART:
7447 if (mStatus == MARQUEE_RUNNING) {
7448 if (mRepeatLimit >= 0) {
7449 mRepeatLimit--;
7450 }
7451 start(mRepeatLimit);
7452 }
7453 break;
7454 }
7455 }
7456
7457 void tick() {
7458 if (mStatus != MARQUEE_RUNNING) {
7459 return;
7460 }
7461
7462 removeMessages(MESSAGE_TICK);
7463
7464 final TextView textView = mView.get();
7465 if (textView != null && (textView.isFocused() || textView.isSelected())) {
7466 mScroll += mScrollUnit;
7467 if (mScroll > mMaxScroll) {
7468 mScroll = mMaxScroll;
7469 sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
7470 } else {
7471 sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
7472 }
7473 textView.invalidate();
7474 }
7475 }
7476
7477 void stop() {
7478 mStatus = MARQUEE_STOPPED;
7479 removeMessages(MESSAGE_START);
7480 removeMessages(MESSAGE_RESTART);
7481 removeMessages(MESSAGE_TICK);
7482 resetScroll();
7483 }
7484
7485 private void resetScroll() {
7486 mScroll = 0.0f;
7487 final TextView textView = mView.get();
7488 if (textView != null) textView.invalidate();
7489 }
7490
7491 void start(int repeatLimit) {
7492 if (repeatLimit == 0) {
7493 stop();
7494 return;
7495 }
7496 mRepeatLimit = repeatLimit;
7497 final TextView textView = mView.get();
7498 if (textView != null && textView.mLayout != null) {
7499 mStatus = MARQUEE_STARTING;
7500 mScroll = 0.0f;
Romain Guyc2303192009-04-03 17:37:18 -07007501 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
7502 textView.getCompoundPaddingRight();
7503 final float lineWidth = textView.mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07007504 final float gap = textWidth / 3.0f;
7505 mGhostStart = lineWidth - textWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007506 mMaxScroll = mGhostStart + textWidth;
Romain Guy3373ed62009-05-04 14:13:32 -07007507 mGhostOffset = lineWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07007508 mFadeStop = lineWidth + textWidth / 6.0f;
7509 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
7510
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007511 textView.invalidate();
7512 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
7513 }
7514 }
7515
Romain Guyc2303192009-04-03 17:37:18 -07007516 float getGhostOffset() {
7517 return mGhostOffset;
7518 }
7519
7520 boolean shouldDrawLeftFade() {
7521 return mScroll <= mFadeStop;
7522 }
7523
7524 boolean shouldDrawGhost() {
7525 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
7526 }
7527
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007528 boolean isRunning() {
7529 return mStatus == MARQUEE_RUNNING;
7530 }
7531
7532 boolean isStopped() {
7533 return mStatus == MARQUEE_STOPPED;
7534 }
7535 }
7536
7537 /**
Gilles Debunne4469e602011-03-09 14:38:04 -08007538 * This method is called when the text is changed, in case any subclasses
7539 * would like to know.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007540 *
Gilles Debunne4469e602011-03-09 14:38:04 -08007541 * Within <code>text</code>, the <code>lengthAfter</code> characters
7542 * beginning at <code>start</code> have just replaced old text that had
7543 * length <code>lengthBefore</code>. It is an error to attempt to make
7544 * changes to <code>text</code> from this callback.
7545 *
7546 * @param text The text the TextView is displaying
7547 * @param start The offset of the start of the range of the text that was
7548 * modified
7549 * @param lengthBefore The length of the former text that has been replaced
7550 * @param lengthAfter The length of the replacement modified text
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007551 */
Gilles Debunne4469e602011-03-09 14:38:04 -08007552 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
Gilles Debunne6435a562011-08-04 21:22:30 -07007553 // intentionally empty, template pattern method can be overridden by subclasses
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007554 }
7555
7556 /**
7557 * This method is called when the selection has changed, in case any
7558 * subclasses would like to know.
7559 *
7560 * @param selStart The new selection start location.
7561 * @param selEnd The new selection end location.
7562 */
7563 protected void onSelectionChanged(int selStart, int selEnd) {
Svetoslav Ganova0156172011-06-26 17:55:44 -07007564 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007565 }
Svetoslav Ganova0156172011-06-26 17:55:44 -07007566
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007567 /**
7568 * Adds a TextWatcher to the list of those whose methods are called
7569 * whenever this TextView's text changes.
7570 * <p>
7571 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
7572 * not called after {@link #setText} calls. Now, doing {@link #setText}
7573 * if there are any text changed listeners forces the buffer type to
7574 * Editable if it would not otherwise be and does call this method.
7575 */
7576 public void addTextChangedListener(TextWatcher watcher) {
7577 if (mListeners == null) {
7578 mListeners = new ArrayList<TextWatcher>();
7579 }
7580
7581 mListeners.add(watcher);
7582 }
7583
7584 /**
7585 * Removes the specified TextWatcher from the list of those whose
7586 * methods are called
7587 * whenever this TextView's text changes.
7588 */
7589 public void removeTextChangedListener(TextWatcher watcher) {
7590 if (mListeners != null) {
7591 int i = mListeners.indexOf(watcher);
7592
7593 if (i >= 0) {
7594 mListeners.remove(i);
7595 }
7596 }
7597 }
7598
Gilles Debunne6435a562011-08-04 21:22:30 -07007599 private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007600 if (mListeners != null) {
7601 final ArrayList<TextWatcher> list = mListeners;
7602 final int count = list.size();
7603 for (int i = 0; i < count; i++) {
7604 list.get(i).beforeTextChanged(text, start, before, after);
7605 }
7606 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007607
7608 // The spans that are inside or intersect the modified region no longer make sense
7609 removeIntersectingSpans(start, start + before, SpellCheckSpan.class);
7610 removeIntersectingSpans(start, start + before, SuggestionSpan.class);
7611 }
7612
7613 // Removes all spans that are inside or actually overlap the start..end range
7614 private <T> void removeIntersectingSpans(int start, int end, Class<T> type) {
7615 if (!(mText instanceof Editable)) return;
7616 Editable text = (Editable) mText;
7617
7618 T[] spans = text.getSpans(start, end, type);
7619 final int length = spans.length;
7620 for (int i = 0; i < length; i++) {
7621 final int s = text.getSpanStart(spans[i]);
7622 final int e = text.getSpanEnd(spans[i]);
Gilles Debunneb062e812011-09-27 14:58:37 -07007623 // Spans that are adjacent to the edited region will be handled in
7624 // updateSpellCheckSpans. Result depends on what will be added (space or text)
Gilles Debunne6435a562011-08-04 21:22:30 -07007625 if (e == start || s == end) break;
7626 text.removeSpan(spans[i]);
7627 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007628 }
7629
7630 /**
7631 * Not private so it can be called from an inner class without going
7632 * through a thunk.
7633 */
Gilles Debunne6435a562011-08-04 21:22:30 -07007634 void sendOnTextChanged(CharSequence text, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007635 if (mListeners != null) {
7636 final ArrayList<TextWatcher> list = mListeners;
7637 final int count = list.size();
7638 for (int i = 0; i < count; i++) {
7639 list.get(i).onTextChanged(text, start, before, after);
7640 }
7641 }
Gilles Debunne1a22db22011-11-20 22:13:21 +01007642
Gilles Debunnec115fa02011-12-07 13:38:31 -08007643 updateSpellCheckSpans(start, start + after, false);
Gilles Debunne1a22db22011-11-20 22:13:21 +01007644
7645 // Hide the controllers as soon as text is modified (typing, procedural...)
7646 // We do not hide the span controllers, since they can be added when a new text is
7647 // inserted into the text view (voice IME).
7648 hideCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007649 }
7650
7651 /**
7652 * Not private so it can be called from an inner class without going
7653 * through a thunk.
7654 */
7655 void sendAfterTextChanged(Editable text) {
7656 if (mListeners != null) {
7657 final ArrayList<TextWatcher> list = mListeners;
7658 final int count = list.size();
7659 for (int i = 0; i < count; i++) {
7660 list.get(i).afterTextChanged(text);
7661 }
7662 }
7663 }
7664
7665 /**
7666 * Not private so it can be called from an inner class without going
7667 * through a thunk.
7668 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007669 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007670 final InputMethodState ims = mInputMethodState;
7671 if (ims == null || ims.mBatchEditNesting == 0) {
7672 updateAfterEdit();
7673 }
7674 if (ims != null) {
7675 ims.mContentChanged = true;
7676 if (ims.mChangedStart < 0) {
7677 ims.mChangedStart = start;
7678 ims.mChangedEnd = start+before;
7679 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01007680 ims.mChangedStart = Math.min(ims.mChangedStart, start);
7681 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007682 }
7683 ims.mChangedDelta += after-before;
7684 }
Gilles Debunne186aaf92011-09-16 14:26:12 -07007685
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007686 sendOnTextChanged(buffer, start, before, after);
7687 onTextChanged(buffer, start, before, after);
7688 }
7689
7690 /**
7691 * Not private so it can be called from an inner class without going
7692 * through a thunk.
7693 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08007694 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007695 // XXX Make the start and end move together if this ends up
7696 // spending too much time invalidating.
7697
7698 boolean selChanged = false;
7699 int newSelStart=-1, newSelEnd=-1;
7700
7701 final InputMethodState ims = mInputMethodState;
7702
7703 if (what == Selection.SELECTION_END) {
7704 mHighlightPathBogus = true;
7705 selChanged = true;
7706 newSelEnd = newStart;
7707
7708 if (!isFocused()) {
7709 mSelectionMoved = true;
7710 }
7711
7712 if (oldStart >= 0 || newStart >= 0) {
7713 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
7714 registerForPreDraw();
Gilles Debunne3d010062011-02-18 14:16:41 -08007715 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007716 }
7717 }
7718
7719 if (what == Selection.SELECTION_START) {
7720 mHighlightPathBogus = true;
7721 selChanged = true;
7722 newSelStart = newStart;
7723
7724 if (!isFocused()) {
7725 mSelectionMoved = true;
7726 }
7727
7728 if (oldStart >= 0 || newStart >= 0) {
7729 int end = Selection.getSelectionEnd(buf);
7730 invalidateCursor(end, oldStart, newStart);
7731 }
7732 }
7733
7734 if (selChanged) {
7735 if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
7736 if (newSelStart < 0) {
7737 newSelStart = Selection.getSelectionStart(buf);
7738 }
7739 if (newSelEnd < 0) {
7740 newSelEnd = Selection.getSelectionEnd(buf);
7741 }
7742 onSelectionChanged(newSelStart, newSelEnd);
7743 }
7744 }
Gilles Debunne8615ac92011-11-29 15:25:03 -08007745
7746 if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007747 if (ims == null || ims.mBatchEditNesting == 0) {
7748 invalidate();
7749 mHighlightPathBogus = true;
7750 checkForResize();
7751 } else {
7752 ims.mContentChanged = true;
7753 }
7754 }
7755
7756 if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7757 mHighlightPathBogus = true;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007758 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7759 ims.mSelectionModeChanged = true;
7760 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007761
7762 if (Selection.getSelectionStart(buf) >= 0) {
7763 if (ims == null || ims.mBatchEditNesting == 0) {
7764 invalidateCursor();
7765 } else {
7766 ims.mCursorChanged = true;
7767 }
7768 }
7769 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007770
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007771 if (what instanceof ParcelableSpan) {
7772 // If this is a span that can be sent to a remote process,
7773 // the current extract editor would be interested in it.
7774 if (ims != null && ims.mExtracting != null) {
7775 if (ims.mBatchEditNesting != 0) {
7776 if (oldStart >= 0) {
7777 if (ims.mChangedStart > oldStart) {
7778 ims.mChangedStart = oldStart;
7779 }
7780 if (ims.mChangedStart > oldEnd) {
7781 ims.mChangedStart = oldEnd;
7782 }
7783 }
7784 if (newStart >= 0) {
7785 if (ims.mChangedStart > newStart) {
7786 ims.mChangedStart = newStart;
7787 }
7788 if (ims.mChangedStart > newEnd) {
7789 ims.mChangedStart = newEnd;
7790 }
7791 }
7792 } else {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007793 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007794 + oldStart + "-" + oldEnd + ","
7795 + newStart + "-" + newEnd + what);
7796 ims.mContentChanged = true;
7797 }
7798 }
7799 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007800
Gilles Debunnec115fa02011-12-07 13:38:31 -08007801 if (mSpellChecker != null && newStart < 0 && what instanceof SpellCheckSpan) {
7802 mSpellChecker.removeSpellCheckSpan((SpellCheckSpan) what);
Gilles Debunne6435a562011-08-04 21:22:30 -07007803 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007804 }
7805
Gilles Debunne6435a562011-08-04 21:22:30 -07007806 /**
7807 * Create new SpellCheckSpans on the modified region.
7808 */
Gilles Debunnec115fa02011-12-07 13:38:31 -08007809 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Gilles Debunne770f0fa2011-12-13 14:47:19 -08007810 if (isTextEditable() && isSuggestionsEnabled() && !(this instanceof ExtractEditText)) {
Gilles Debunnec115fa02011-12-07 13:38:31 -08007811 if (mSpellChecker == null && createSpellChecker) {
7812 mSpellChecker = new SpellChecker(this);
7813 }
7814 if (mSpellChecker != null) {
7815 mSpellChecker.spellCheck(start, end);
7816 }
Gilles Debunne6435a562011-08-04 21:22:30 -07007817 }
7818 }
7819
Luca Zanoline6d36822011-08-30 18:04:34 +01007820 /**
7821 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
7822 * pop-up should be displayed.
7823 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007824 private class EasyEditSpanController {
Luca Zanoline6d36822011-08-30 18:04:34 +01007825
Luca Zanolin1564fc72011-09-07 00:01:28 +01007826 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
Luca Zanoline6d36822011-08-30 18:04:34 +01007827
Luca Zanolin1564fc72011-09-07 00:01:28 +01007828 private EasyEditPopupWindow mPopupWindow;
7829
7830 private EasyEditSpan mEasyEditSpan;
7831
7832 private Runnable mHidePopup;
Luca Zanoline6d36822011-08-30 18:04:34 +01007833
7834 private void hide() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007835 if (mPopupWindow != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007836 mPopupWindow.hide();
Luca Zanolin1564fc72011-09-07 00:01:28 +01007837 TextView.this.removeCallbacks(mHidePopup);
Luca Zanoline6d36822011-08-30 18:04:34 +01007838 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007839 removeSpans(mText);
7840 mEasyEditSpan = null;
Luca Zanoline6d36822011-08-30 18:04:34 +01007841 }
7842
7843 /**
7844 * Monitors the changes in the text.
7845 *
7846 * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
7847 * as the notifications are not sent when a spannable (with spans) is inserted.
7848 */
7849 public void onTextChange(CharSequence buffer) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007850 adjustSpans(mText);
7851
7852 if (getWindowVisibility() != View.VISIBLE) {
7853 // The window is not visible yet, ignore the text change.
7854 return;
Luca Zanoline6d36822011-08-30 18:04:34 +01007855 }
7856
Luca Zanolin1564fc72011-09-07 00:01:28 +01007857 if (mLayout == null) {
7858 // The view has not been layout yet, ignore the text change
7859 return;
7860 }
7861
7862 InputMethodManager imm = InputMethodManager.peekInstance();
7863 if (!(TextView.this instanceof ExtractEditText)
7864 && imm != null && imm.isFullscreenMode()) {
7865 // The input is in extract mode. We do not have to handle the easy edit in the
7866 // original TextView, as the ExtractEditText will do
7867 return;
7868 }
7869
7870 // Remove the current easy edit span, as the text changed, and remove the pop-up
7871 // (if any)
7872 if (mEasyEditSpan != null) {
7873 if (mText instanceof Spannable) {
7874 ((Spannable) mText).removeSpan(mEasyEditSpan);
7875 }
7876 mEasyEditSpan = null;
7877 }
7878 if (mPopupWindow != null && mPopupWindow.isShowing()) {
7879 mPopupWindow.hide();
7880 }
7881
7882 // Display the new easy edit span (if any).
Luca Zanoline6d36822011-08-30 18:04:34 +01007883 if (buffer instanceof Spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007884 mEasyEditSpan = getSpan((Spanned) buffer);
7885 if (mEasyEditSpan != null) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007886 if (mPopupWindow == null) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007887 mPopupWindow = new EasyEditPopupWindow();
7888 mHidePopup = new Runnable() {
7889 @Override
7890 public void run() {
7891 hide();
7892 }
7893 };
Luca Zanoline6d36822011-08-30 18:04:34 +01007894 }
Luca Zanolin1564fc72011-09-07 00:01:28 +01007895 mPopupWindow.show(mEasyEditSpan);
7896 TextView.this.removeCallbacks(mHidePopup);
7897 TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
7898 }
7899 }
7900 }
7901
7902 /**
7903 * Adjusts the spans by removing all of them except the last one.
7904 */
7905 private void adjustSpans(CharSequence buffer) {
7906 // This method enforces that only one easy edit span is attached to the text.
7907 // A better way to enforce this would be to listen for onSpanAdded, but this method
7908 // cannot be used in this scenario as no notification is triggered when a text with
7909 // spans is inserted into a text.
7910 if (buffer instanceof Spannable) {
7911 Spannable spannable = (Spannable) buffer;
7912 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7913 EasyEditSpan.class);
7914 for (int i = 0; i < spans.length - 1; i++) {
7915 spannable.removeSpan(spans[i]);
7916 }
7917 }
7918 }
7919
7920 /**
7921 * Removes all the {@link EasyEditSpan} currently attached.
7922 */
7923 private void removeSpans(CharSequence buffer) {
7924 if (buffer instanceof Spannable) {
7925 Spannable spannable = (Spannable) buffer;
7926 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7927 EasyEditSpan.class);
7928 for (int i = 0; i < spans.length; i++) {
7929 spannable.removeSpan(spans[i]);
Luca Zanoline6d36822011-08-30 18:04:34 +01007930 }
7931 }
7932 }
7933
7934 private EasyEditSpan getSpan(Spanned spanned) {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007935 EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
Luca Zanoline6d36822011-08-30 18:04:34 +01007936 EasyEditSpan.class);
Luca Zanolin1564fc72011-09-07 00:01:28 +01007937 if (easyEditSpans.length == 0) {
Luca Zanoline6d36822011-08-30 18:04:34 +01007938 return null;
7939 } else {
Luca Zanolin1564fc72011-09-07 00:01:28 +01007940 return easyEditSpans[0];
Luca Zanoline6d36822011-08-30 18:04:34 +01007941 }
7942 }
7943 }
7944
7945 /**
7946 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Luca Zanolin1564fc72011-09-07 00:01:28 +01007947 * by {@link EasyEditSpanController}.
Luca Zanoline6d36822011-08-30 18:04:34 +01007948 */
Luca Zanolin1564fc72011-09-07 00:01:28 +01007949 private class EasyEditPopupWindow extends PinnedPopupWindow
Luca Zanoline6d36822011-08-30 18:04:34 +01007950 implements OnClickListener {
7951 private static final int POPUP_TEXT_LAYOUT =
7952 com.android.internal.R.layout.text_edit_action_popup_text;
7953 private TextView mDeleteTextView;
Luca Zanolin1564fc72011-09-07 00:01:28 +01007954 private EasyEditSpan mEasyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007955
7956 @Override
7957 protected void createPopupWindow() {
7958 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
7959 com.android.internal.R.attr.textSelectHandleWindowStyle);
7960 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
7961 mPopupWindow.setClippingEnabled(true);
7962 }
7963
7964 @Override
7965 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07007966 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
7967 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
7968 mContentView = linearLayout;
Luca Zanoline6d36822011-08-30 18:04:34 +01007969 mContentView.setBackgroundResource(
7970 com.android.internal.R.drawable.text_edit_side_paste_window);
7971
7972 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
7973 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
7974
7975 LayoutParams wrapContent = new LayoutParams(
7976 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
7977
7978 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
7979 mDeleteTextView.setLayoutParams(wrapContent);
7980 mDeleteTextView.setText(com.android.internal.R.string.delete);
7981 mDeleteTextView.setOnClickListener(this);
7982 mContentView.addView(mDeleteTextView);
7983 }
7984
Luca Zanolin1564fc72011-09-07 00:01:28 +01007985 public void show(EasyEditSpan easyEditSpan) {
7986 mEasyEditSpan = easyEditSpan;
Luca Zanoline6d36822011-08-30 18:04:34 +01007987 super.show();
7988 }
7989
7990 @Override
7991 public void onClick(View view) {
7992 if (view == mDeleteTextView) {
Gilles Debunne39ba6d92011-11-09 05:26:26 +01007993 Editable editable = (Editable) mText;
7994 int start = editable.getSpanStart(mEasyEditSpan);
7995 int end = editable.getSpanEnd(mEasyEditSpan);
7996 if (start >= 0 && end >= 0) {
7997 deleteText_internal(start, end);
7998 }
Luca Zanoline6d36822011-08-30 18:04:34 +01007999 }
8000 }
8001
8002 @Override
8003 protected int getTextOffset() {
8004 // Place the pop-up at the end of the span
8005 Editable editable = (Editable) mText;
Luca Zanolin1564fc72011-09-07 00:01:28 +01008006 return editable.getSpanEnd(mEasyEditSpan);
Luca Zanoline6d36822011-08-30 18:04:34 +01008007 }
8008
8009 @Override
8010 protected int getVerticalLocalPosition(int line) {
8011 return mLayout.getLineBottom(line);
8012 }
8013
8014 @Override
8015 protected int clipVertically(int positionY) {
8016 // As we display the pop-up below the span, no vertical clipping is required.
8017 return positionY;
8018 }
8019 }
8020
Gilles Debunne6435a562011-08-04 21:22:30 -07008021 private class ChangeWatcher implements TextWatcher, SpanWatcher {
svetoslavganov75986cf2009-05-14 22:28:01 -07008022
8023 private CharSequence mBeforeText;
8024
Luca Zanolin1564fc72011-09-07 00:01:28 +01008025 private EasyEditSpanController mEasyEditSpanController;
Luca Zanoline6d36822011-08-30 18:04:34 +01008026
8027 private ChangeWatcher() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008028 mEasyEditSpanController = new EasyEditSpanController();
Luca Zanoline6d36822011-08-30 18:04:34 +01008029 }
8030
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008031 public void beforeTextChanged(CharSequence buffer, int start,
8032 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008033 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008034 + " before=" + before + " after=" + after + ": " + buffer);
svetoslavganov75986cf2009-05-14 22:28:01 -07008035
Amith Yamasani91ccdb52010-01-14 18:56:02 -08008036 if (AccessibilityManager.getInstance(mContext).isEnabled()
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008037 && !isPasswordInputType(mInputType)
8038 && !hasPasswordTransformationMethod()) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008039 mBeforeText = buffer.toString();
8040 }
8041
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008042 TextView.this.sendBeforeTextChanged(buffer, start, before, after);
8043 }
8044
8045 public void onTextChanged(CharSequence buffer, int start,
8046 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008047 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008048 + " before=" + before + " after=" + after + ": " + buffer);
8049 TextView.this.handleTextChanged(buffer, start, before, after);
svetoslavganov75986cf2009-05-14 22:28:01 -07008050
Luca Zanolin1564fc72011-09-07 00:01:28 +01008051 mEasyEditSpanController.onTextChange(buffer);
Luca Zanoline6d36822011-08-30 18:04:34 +01008052
svetoslavganov75986cf2009-05-14 22:28:01 -07008053 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
Gilles Debunne6435a562011-08-04 21:22:30 -07008054 (isFocused() || isSelected() && isShown())) {
svetoslavganov75986cf2009-05-14 22:28:01 -07008055 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
8056 mBeforeText = null;
8057 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008058 }
8059
8060 public void afterTextChanged(Editable buffer) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008061 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008062 TextView.this.sendAfterTextChanged(buffer);
8063
Gilles Debunne6435a562011-08-04 21:22:30 -07008064 if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008065 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
8066 }
8067 }
8068
8069 public void onSpanChanged(Spannable buf,
8070 Object what, int s, int e, int st, int en) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008071 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008072 + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
8073 TextView.this.spanChange(buf, what, s, st, e, en);
8074 }
8075
8076 public void onSpanAdded(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008077 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008078 + " what=" + what + ": " + buf);
8079 TextView.this.spanChange(buf, what, -1, s, -1, e);
8080 }
8081
8082 public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008083 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008084 + " what=" + what + ": " + buf);
8085 TextView.this.spanChange(buf, what, s, -1, e, -1);
8086 }
Luca Zanoline6d36822011-08-30 18:04:34 +01008087
8088 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +01008089 mEasyEditSpanController.hide();
Luca Zanoline6d36822011-08-30 18:04:34 +01008090 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008091 }
8092
Romain Guydcc490f2010-02-24 17:59:35 -08008093 /**
8094 * @hide
8095 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008096 @Override
Romain Guya440b002010-02-24 15:57:54 -08008097 public void dispatchFinishTemporaryDetach() {
8098 mDispatchTemporaryDetach = true;
8099 super.dispatchFinishTemporaryDetach();
8100 mDispatchTemporaryDetach = false;
8101 }
8102
8103 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008104 public void onStartTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008105 super.onStartTemporaryDetach();
8106 // Only track when onStartTemporaryDetach() is called directly,
8107 // usually because this instance is an editable field in a list
8108 if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
Gilles Debunne4b2274f2011-02-25 15:18:03 -08008109
8110 // Because of View recycling in ListView, there is no easy way to know when a TextView with
8111 // selection becomes visible again. Until a better solution is found, stop text selection
8112 // mode (if any) as soon as this TextView is recycled.
Gilles Debunne4a7199a2011-07-11 14:58:27 -07008113 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008114 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008115
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008116 @Override
8117 public void onFinishTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08008118 super.onFinishTemporaryDetach();
8119 // Only track when onStartTemporaryDetach() is called directly,
8120 // usually because this instance is an editable field in a list
8121 if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008122 }
Gilles Debunne3784a7f2011-07-15 13:49:38 -07008123
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008124 @Override
8125 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
8126 if (mTemporaryDetach) {
8127 // If we are temporarily in the detach state, then do nothing.
8128 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8129 return;
8130 }
8131
8132 mShowCursor = SystemClock.uptimeMillis();
8133
8134 ensureEndedBatchEdit();
Gilles Debunne03789e82010-09-07 19:07:17 -07008135
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008136 if (focused) {
8137 int selStart = getSelectionStart();
8138 int selEnd = getSelectionEnd();
8139
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08008140 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
8141 // mode for these, unless there was a specific selection already started.
8142 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
8143 selEnd == mText.length();
8144 mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
8145
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008146 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008147 // If a tap was used to give focus to that view, move cursor at tap position.
Gilles Debunne64e54a62010-09-07 19:07:17 -07008148 // Has to be done before onTakeFocus, which can be overloaded.
Gilles Debunne380b6042010-10-08 16:12:11 -07008149 final int lastTapPosition = getLastTapPosition();
8150 if (lastTapPosition >= 0) {
8151 Selection.setSelection((Spannable) mText, lastTapPosition);
8152 }
Gilles Debunne2703a422010-08-23 15:14:03 -07008153
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008154 if (mMovement != null) {
8155 mMovement.onTakeFocus(this, (Spannable) mText, direction);
8156 }
8157
Gilles Debunne64e54a62010-09-07 19:07:17 -07008158 // The DecorView does not have focus when the 'Done' ExtractEditText button is
Joe Onoratoc6cc0f82011-04-12 11:53:13 -07008159 // pressed. Since it is the ViewAncestor's mView, it requests focus before
Gilles Debunne64e54a62010-09-07 19:07:17 -07008160 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
8161 // This special case ensure that we keep current selection in that case.
8162 // It would be better to know why the DecorView does not have focus at that time.
Gilles Debunne47fa8e82010-09-07 18:50:07 -07008163 if (((this instanceof ExtractEditText) || mSelectionMoved) &&
8164 selStart >= 0 && selEnd >= 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008165 /*
8166 * Someone intentionally set the selection, so let them
8167 * do whatever it is that they wanted to do instead of
8168 * the default on-focus behavior. We reset the selection
8169 * here instead of just skipping the onTakeFocus() call
8170 * because some movement methods do something other than
8171 * just setting the selection in theirs and we still
8172 * need to go through that path.
8173 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008174 Selection.setSelection((Spannable) mText, selStart, selEnd);
8175 }
Gilles Debunnef170a342010-11-11 11:08:59 -08008176
8177 if (mSelectAllOnFocus) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008178 selectAll();
Gilles Debunnef170a342010-11-11 11:08:59 -08008179 }
8180
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008181 mTouchFocusSelected = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008182 }
8183
8184 mFrozenWithFocus = false;
8185 mSelectionMoved = false;
8186
8187 if (mText instanceof Spannable) {
8188 Spannable sp = (Spannable) mText;
8189 MetaKeyKeyListener.resetMetaState(sp);
8190 }
8191
8192 makeBlink();
8193
8194 if (mError != null) {
8195 showError();
8196 }
8197 } else {
8198 if (mError != null) {
8199 hideError();
8200 }
8201 // Don't leave us in the middle of a batch edit.
8202 onEndBatchEdit();
Gilles Debunne05336272010-07-09 20:13:45 -07008203
Gilles Debunne64e54a62010-09-07 19:07:17 -07008204 if (this instanceof ExtractEditText) {
8205 // terminateTextSelectionMode removes selection, which we want to keep when
8206 // ExtractEditText goes out of focus.
8207 final int selStart = getSelectionStart();
8208 final int selEnd = getSelectionEnd();
Gilles Debunneb7012e842011-02-24 15:40:38 -08008209 hideControllers();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008210 Selection.setSelection((Spannable) mText, selStart, selEnd);
8211 } else {
Gilles Debunneb7012e842011-02-24 15:40:38 -08008212 hideControllers();
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008213 downgradeEasyCorrectionSpans();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008214 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008215
Gilles Debunnee587d832010-11-23 20:20:11 -08008216 // No need to create the controller
Gilles Debunne380b6042010-10-08 16:12:11 -07008217 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008218 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunne380b6042010-10-08 16:12:11 -07008219 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008220 }
8221
8222 startStopMarquee(focused);
8223
8224 if (mTransformation != null) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008225 mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008226 }
8227
8228 super.onFocusChanged(focused, direction, previouslyFocusedRect);
8229 }
8230
Gilles Debunne380b6042010-10-08 16:12:11 -07008231 private int getLastTapPosition() {
Gilles Debunnee587d832010-11-23 20:20:11 -08008232 // No need to create the controller at that point, no last tap position saved
Gilles Debunne528c64882010-10-08 11:56:13 -07008233 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008234 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
Gilles Debunne380b6042010-10-08 16:12:11 -07008235 if (lastTapPosition >= 0) {
Gilles Debunne528c64882010-10-08 11:56:13 -07008236 // Safety check, should not be possible.
Gilles Debunne380b6042010-10-08 16:12:11 -07008237 if (lastTapPosition > mText.length()) {
8238 Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
Gilles Debunne528c64882010-10-08 11:56:13 -07008239 + mText.length() + ")");
Gilles Debunne380b6042010-10-08 16:12:11 -07008240 lastTapPosition = mText.length();
Gilles Debunne528c64882010-10-08 11:56:13 -07008241 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008242 return lastTapPosition;
Gilles Debunne528c64882010-10-08 11:56:13 -07008243 }
8244 }
Gilles Debunne380b6042010-10-08 16:12:11 -07008245
8246 return -1;
Gilles Debunne528c64882010-10-08 11:56:13 -07008247 }
8248
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008249 @Override
8250 public void onWindowFocusChanged(boolean hasWindowFocus) {
8251 super.onWindowFocusChanged(hasWindowFocus);
8252
8253 if (hasWindowFocus) {
8254 if (mBlink != null) {
8255 mBlink.uncancel();
Gilles Debunne3d010062011-02-18 14:16:41 -08008256 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008257 }
8258 } else {
8259 if (mBlink != null) {
8260 mBlink.cancel();
8261 }
8262 // Don't leave us in the middle of a batch edit.
8263 onEndBatchEdit();
8264 if (mInputContentType != null) {
8265 mInputContentType.enterDown = false;
8266 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008267
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008268 hideControllers();
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07008269 if (mSuggestionsPopupWindow != null) {
8270 mSuggestionsPopupWindow.onParentLostFocus();
8271 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008272 }
8273
8274 startStopMarquee(hasWindowFocus);
8275 }
8276
Adam Powellba0a2c32010-09-28 17:41:23 -07008277 @Override
8278 protected void onVisibilityChanged(View changedView, int visibility) {
8279 super.onVisibilityChanged(changedView, visibility);
8280 if (visibility != VISIBLE) {
Gilles Debunnee507a9e2010-10-10 12:44:18 -07008281 hideControllers();
Adam Powellba0a2c32010-09-28 17:41:23 -07008282 }
8283 }
8284
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008285 /**
8286 * Use {@link BaseInputConnection#removeComposingSpans
8287 * BaseInputConnection.removeComposingSpans()} to remove any IME composing
8288 * state from this text view.
8289 */
8290 public void clearComposingText() {
8291 if (mText instanceof Spannable) {
8292 BaseInputConnection.removeComposingSpans((Spannable)mText);
8293 }
8294 }
8295
8296 @Override
8297 public void setSelected(boolean selected) {
8298 boolean wasSelected = isSelected();
8299
8300 super.setSelected(selected);
8301
8302 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8303 if (selected) {
8304 startMarquee();
8305 } else {
8306 stopMarquee();
8307 }
8308 }
8309 }
8310
8311 @Override
8312 public boolean onTouchEvent(MotionEvent event) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008313 final int action = event.getActionMasked();
Adam Powell965b9692010-10-21 18:44:32 -07008314
Adam Powell965b9692010-10-21 18:44:32 -07008315 if (hasSelectionController()) {
8316 getSelectionController().onTouchEvent(event);
Gilles Debunne5347c582010-10-27 14:22:35 -07008317 }
Adam Powellb08013c2010-09-16 16:28:11 -07008318
Gilles Debunne5347c582010-10-27 14:22:35 -07008319 if (action == MotionEvent.ACTION_DOWN) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07008320 mLastDownPositionX = event.getX();
8321 mLastDownPositionY = event.getY();
Gilles Debunne9948ad72010-11-24 14:00:46 -08008322
The Android Open Source Project4df24232009-03-05 14:34:35 -08008323 // Reset this state; it will be re-set if super.onTouchEvent
8324 // causes focus to move to the view.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008325 mTouchFocusSelected = false;
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008326 mIgnoreActionUpEvent = false;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008327 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008328
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008329 final boolean superResult = super.onTouchEvent(event);
8330
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008331 /*
8332 * Don't handle the release after a long press, because it will
8333 * move the selection away from whatever the menu action was
8334 * trying to affect.
8335 */
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008336 if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
8337 mDiscardNextActionUp = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008338 return superResult;
8339 }
8340
Gilles Debunne70a63122011-09-01 13:27:33 -07008341 final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
8342 !shouldIgnoreActionUpEvent() && isFocused();
Gilles Debunnec3e85a72011-01-21 08:46:06 -08008343
Gilles Debunne70a63122011-09-01 13:27:33 -07008344 if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
Janos Levai042856c2010-10-15 02:53:58 +03008345 && mText instanceof Spannable && mLayout != null) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008346 boolean handled = false;
8347
The Android Open Source Projectba87e3e2009-03-13 13:04:22 -07008348 if (mMovement != null) {
8349 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
8350 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008351
Gilles Debunne70a63122011-09-01 13:27:33 -07008352 if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable) {
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008353 // The LinkMovementMethod which should handle taps on links has not been installed
Gilles Debunne70a63122011-09-01 13:27:33 -07008354 // on non editable text that support text selection.
8355 // We reproduce its behavior here to open links for these.
Gilles Debunnef3895ed2010-12-21 12:53:58 -08008356 ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
8357 getSelectionEnd(), ClickableSpan.class);
8358
8359 if (links.length != 0) {
8360 links[0].onClick(this);
8361 handled = true;
8362 }
8363 }
8364
Gilles Debunne70a63122011-09-01 13:27:33 -07008365 if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008366 // Show the IME, except when selecting in read-only text.
satok863fcd62011-06-21 17:38:02 +09008367 final InputMethodManager imm = InputMethodManager.peekInstance();
satoka67a3cf2011-09-07 17:14:03 +09008368 viewClicked(imm);
Gilles Debunne0f4109e2011-10-19 11:26:21 -07008369 if (!mTextIsSelectable && mSoftInputShownOnFocus) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008370 handled |= imm != null && imm.showSoftInput(this, 0);
Adam Powell879fb6b2010-09-20 11:23:56 -07008371 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07008372
Gilles Debunne180bb1b2011-03-10 11:14:00 -08008373 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
Gilles Debunne70a63122011-09-01 13:27:33 -07008374 hideControllers();
8375 if (!selectAllGotFocus && mText.length() > 0) {
Gilles Debunneb062e812011-09-27 14:58:37 -07008376 if (mSpellChecker != null) {
8377 // When the cursor moves, the word that was typed may need spell check
8378 mSpellChecker.onSelectionChanged();
8379 }
Gilles Debunne61ddbba2011-11-09 09:48:40 +01008380 if (!extractedTextModeWillBeStarted()) {
8381 if (isCursorInsideEasyCorrectionSpan()) {
8382 showSuggestions();
8383 } else if (hasInsertionController()) {
8384 getInsertionController().show();
8385 }
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08008386 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008387 }
Gilles Debunne6435a562011-08-04 21:22:30 -07008388
8389 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008390 }
8391
The Android Open Source Project4df24232009-03-05 14:34:35 -08008392 if (handled) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008393 return true;
8394 }
8395 }
8396
8397 return superResult;
8398 }
8399
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008400 /**
Gilles Debunne6435a562011-08-04 21:22:30 -07008401 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
8402 */
8403 private boolean isCursorInsideSuggestionSpan() {
8404 if (!(mText instanceof Spannable)) return false;
8405
8406 SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
8407 getSelectionEnd(), SuggestionSpan.class);
8408 return (suggestionSpans.length > 0);
8409 }
8410
8411 /**
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008412 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
8413 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
8414 */
8415 private boolean isCursorInsideEasyCorrectionSpan() {
Gilles Debunne6435a562011-08-04 21:22:30 -07008416 Spannable spannable = (Spannable) mText;
Luca Zanolin7d1c55f2011-08-16 14:59:26 +01008417 SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
8418 getSelectionEnd(), SuggestionSpan.class);
8419 for (int i = 0; i < suggestionSpans.length; i++) {
8420 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
8421 return true;
8422 }
8423 }
8424 return false;
8425 }
8426
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008427 /**
8428 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
8429 * span.
8430 */
8431 private void downgradeEasyCorrectionSpans() {
8432 if (mText instanceof Spannable) {
8433 Spannable spannable = (Spannable) mText;
8434 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8435 spannable.length(), SuggestionSpan.class);
8436 for (int i = 0; i < suggestionSpans.length; i++) {
8437 int flags = suggestionSpans[i].getFlags();
8438 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8439 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
Gilles Debunne186aaf92011-09-16 14:26:12 -07008440 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
Luca Zanolinfe5e9832011-09-02 19:41:42 +01008441 suggestionSpans[i].setFlags(flags);
8442 }
8443 }
8444 }
8445 }
8446
Jeff Brown8f345672011-02-26 13:29:53 -08008447 @Override
8448 public boolean onGenericMotionEvent(MotionEvent event) {
8449 if (mMovement != null && mText instanceof Spannable && mLayout != null) {
8450 try {
8451 if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
8452 return true;
8453 }
8454 } catch (AbstractMethodError ex) {
8455 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
8456 // Ignore its absence in case third party applications implemented the
8457 // interface directly.
8458 }
8459 }
8460 return super.onGenericMotionEvent(event);
8461 }
8462
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008463 private void prepareCursorControllers() {
Adam Powell8c8293b2010-10-12 14:45:12 -07008464 boolean windowSupportsHandles = false;
8465
8466 ViewGroup.LayoutParams params = getRootView().getLayoutParams();
8467 if (params instanceof WindowManager.LayoutParams) {
8468 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
8469 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
8470 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
8471 }
8472
Gilles Debunne98dbfd42011-01-24 12:54:10 -08008473 mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
Adam Powell965b9692010-10-21 18:44:32 -07008474 mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
8475 mLayout != null;
8476
8477 if (!mInsertionControllerEnabled) {
Gilles Debunnef48e83b2010-12-06 18:36:08 -08008478 hideInsertionPointCursorController();
Adam Powell65a1de92011-01-30 15:47:29 -08008479 if (mInsertionPointCursorController != null) {
8480 mInsertionPointCursorController.onDetached();
8481 mInsertionPointCursorController = null;
8482 }
Gilles Debunne05336272010-07-09 20:13:45 -07008483 }
8484
Adam Powell965b9692010-10-21 18:44:32 -07008485 if (!mSelectionControllerEnabled) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008486 stopSelectionActionMode();
Adam Powell65a1de92011-01-30 15:47:29 -08008487 if (mSelectionModifierCursorController != null) {
8488 mSelectionModifierCursorController.onDetached();
8489 mSelectionModifierCursorController = null;
8490 }
Gilles Debunne05336272010-07-09 20:13:45 -07008491 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008492 }
8493
8494 /**
Gilles Debunne86b9c782010-11-11 10:43:48 -08008495 * @return True iff this TextView contains a text that can be edited, or if this is
8496 * a selectable TextView.
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008497 */
8498 private boolean isTextEditable() {
Gilles Debunnef076eeb2010-11-29 11:32:53 -08008499 return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008500 }
8501
The Android Open Source Project4df24232009-03-05 14:34:35 -08008502 /**
8503 * Returns true, only while processing a touch gesture, if the initial
8504 * 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 -07008505 * its selection changed. Only valid while processing the touch gesture
8506 * of interest.
The Android Open Source Project4df24232009-03-05 14:34:35 -08008507 */
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008508 public boolean didTouchFocusSelect() {
8509 return mTouchFocusSelected;
The Android Open Source Project4df24232009-03-05 14:34:35 -08008510 }
8511
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008512 @Override
8513 public void cancelLongPress() {
8514 super.cancelLongPress();
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008515 mIgnoreActionUpEvent = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008516 }
Gilles Debunne70a63122011-09-01 13:27:33 -07008517
8518 /**
8519 * This method is only valid during a touch event.
8520 *
8521 * @return true when the ACTION_UP event should be ignored, false otherwise.
8522 *
8523 * @hide
8524 */
8525 public boolean shouldIgnoreActionUpEvent() {
8526 return mIgnoreActionUpEvent;
8527 }
8528
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008529 @Override
8530 public boolean onTrackballEvent(MotionEvent event) {
8531 if (mMovement != null && mText instanceof Spannable &&
8532 mLayout != null) {
8533 if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
8534 return true;
8535 }
8536 }
8537
8538 return super.onTrackballEvent(event);
8539 }
8540
8541 public void setScroller(Scroller s) {
8542 mScroller = s;
8543 }
8544
8545 private static class Blink extends Handler implements Runnable {
Gilles Debunnee15b3582010-06-16 15:17:21 -07008546 private final WeakReference<TextView> mView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008547 private boolean mCancelled;
8548
8549 public Blink(TextView v) {
8550 mView = new WeakReference<TextView>(v);
8551 }
8552
8553 public void run() {
8554 if (mCancelled) {
8555 return;
8556 }
8557
8558 removeCallbacks(Blink.this);
8559
8560 TextView tv = mView.get();
8561
Gilles Debunne3d010062011-02-18 14:16:41 -08008562 if (tv != null && tv.shouldBlink()) {
8563 if (tv.mLayout != null) {
8564 tv.invalidateCursorPath();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008565 }
Gilles Debunne3d010062011-02-18 14:16:41 -08008566
8567 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008568 }
8569 }
8570
8571 void cancel() {
8572 if (!mCancelled) {
8573 removeCallbacks(Blink.this);
8574 mCancelled = true;
8575 }
8576 }
8577
8578 void uncancel() {
8579 mCancelled = false;
8580 }
8581 }
8582
Gilles Debunne3d010062011-02-18 14:16:41 -08008583 /**
8584 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
8585 */
8586 private boolean shouldBlink() {
Gilles Debunne545c4d42011-11-29 10:37:15 -08008587 if (!isCursorVisible() || !isFocused()) return false;
Gilles Debunne3d010062011-02-18 14:16:41 -08008588
8589 final int start = getSelectionStart();
8590 if (start < 0) return false;
8591
8592 final int end = getSelectionEnd();
8593 if (end < 0) return false;
8594
8595 return start == end;
8596 }
8597
8598 private void makeBlink() {
Gilles Debunne545c4d42011-11-29 10:37:15 -08008599 if (shouldBlink()) {
8600 mShowCursor = SystemClock.uptimeMillis();
8601 if (mBlink == null) mBlink = new Blink(this);
8602 mBlink.removeCallbacks(mBlink);
8603 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
Gilles Debunne3d010062011-02-18 14:16:41 -08008604 } else {
8605 if (mBlink != null) mBlink.removeCallbacks(mBlink);
8606 }
8607 }
8608
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008609 @Override
8610 protected float getLeftFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008611 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008612 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8613 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008614 if (mMarquee != null && !mMarquee.isStopped()) {
8615 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008616 if (marquee.shouldDrawLeftFade()) {
8617 return marquee.mScroll / getHorizontalFadingEdgeLength();
8618 } else {
8619 return 0.0f;
8620 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008621 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008622 final int layoutDirection = getResolvedLayoutDirection();
8623 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008624 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008625 case Gravity.LEFT:
8626 return 0.0f;
8627 case Gravity.RIGHT:
8628 return (mLayout.getLineRight(0) - (mRight - mLeft) -
8629 getCompoundPaddingLeft() - getCompoundPaddingRight() -
8630 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
8631 case Gravity.CENTER_HORIZONTAL:
8632 return 0.0f;
8633 }
8634 }
8635 }
8636 return super.getLeftFadingEdgeStrength();
8637 }
8638
8639 @Override
8640 protected float getRightFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07008641 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
Adam Powell282e3772011-08-30 16:51:11 -07008642 if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8643 mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008644 if (mMarquee != null && !mMarquee.isStopped()) {
8645 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07008646 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008647 } else if (getLineCount() == 1) {
Fabrice Di Meglioc0053222011-06-13 12:16:51 -07008648 final int layoutDirection = getResolvedLayoutDirection();
8649 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
Fabrice Di Meglio6a036402011-05-23 14:43:23 -07008650 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008651 case Gravity.LEFT:
Romain Guy076dc9f2009-06-24 17:17:51 -07008652 final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
8653 getCompoundPaddingRight();
8654 final float lineWidth = mLayout.getLineWidth(0);
8655 return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008656 case Gravity.RIGHT:
8657 return 0.0f;
8658 case Gravity.CENTER_HORIZONTAL:
Gilles Debunne44c14732010-10-19 11:56:59 -07008659 case Gravity.FILL_HORIZONTAL:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008660 return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
8661 getCompoundPaddingLeft() - getCompoundPaddingRight())) /
8662 getHorizontalFadingEdgeLength();
8663 }
8664 }
8665 }
8666 return super.getRightFadingEdgeStrength();
8667 }
8668
8669 @Override
8670 protected int computeHorizontalScrollRange() {
Romain Guydac5f9f2010-07-08 11:40:54 -07008671 if (mLayout != null) {
8672 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
8673 (int) mLayout.getLineWidth(0) : mLayout.getWidth();
8674 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008675
8676 return super.computeHorizontalScrollRange();
8677 }
8678
8679 @Override
8680 protected int computeVerticalScrollRange() {
8681 if (mLayout != null)
8682 return mLayout.getHeight();
8683
8684 return super.computeVerticalScrollRange();
8685 }
8686
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07008687 @Override
8688 protected int computeVerticalScrollExtent() {
8689 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
8690 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008691
8692 @Override
Svetoslav Ganovea515ae2011-09-14 18:15:32 -07008693 public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
8694 super.findViewsWithText(outViews, searched, flags);
8695 if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
8696 && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
8697 String searchedLowerCase = searched.toString().toLowerCase();
8698 String textLowerCase = mText.toString().toLowerCase();
8699 if (textLowerCase.contains(searchedLowerCase)) {
8700 outViews.add(this);
8701 }
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07008702 }
8703 }
8704
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008705 public enum BufferType {
8706 NORMAL, SPANNABLE, EDITABLE,
8707 }
8708
8709 /**
8710 * Returns the TextView_textColor attribute from the
8711 * Resources.StyledAttributes, if set, or the TextAppearance_textColor
8712 * from the TextView_textAppearance attribute, if TextView_textColor
8713 * was not set directly.
8714 */
8715 public static ColorStateList getTextColors(Context context, TypedArray attrs) {
8716 ColorStateList colors;
8717 colors = attrs.getColorStateList(com.android.internal.R.styleable.
8718 TextView_textColor);
8719
8720 if (colors == null) {
8721 int ap = attrs.getResourceId(com.android.internal.R.styleable.
8722 TextView_textAppearance, -1);
8723 if (ap != -1) {
8724 TypedArray appearance;
8725 appearance = context.obtainStyledAttributes(ap,
8726 com.android.internal.R.styleable.TextAppearance);
8727 colors = appearance.getColorStateList(com.android.internal.R.styleable.
8728 TextAppearance_textColor);
8729 appearance.recycle();
8730 }
8731 }
8732
8733 return colors;
8734 }
8735
8736 /**
8737 * Returns the default color from the TextView_textColor attribute
8738 * from the AttributeSet, if set, or the default color from the
8739 * TextAppearance_textColor from the TextView_textAppearance attribute,
8740 * if TextView_textColor was not set directly.
8741 */
8742 public static int getTextColor(Context context,
8743 TypedArray attrs,
8744 int def) {
8745 ColorStateList colors = getTextColors(context, attrs);
8746
8747 if (colors == null) {
8748 return def;
8749 } else {
8750 return colors.getDefaultColor();
8751 }
8752 }
8753
8754 @Override
8755 public boolean onKeyShortcut(int keyCode, KeyEvent event) {
Jeff Brownc1df9072010-12-21 16:38:50 -08008756 final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
8757 if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
8758 switch (keyCode) {
8759 case KeyEvent.KEYCODE_A:
8760 if (canSelectText()) {
8761 return onTextContextMenuItem(ID_SELECT_ALL);
8762 }
8763 break;
8764 case KeyEvent.KEYCODE_X:
8765 if (canCut()) {
8766 return onTextContextMenuItem(ID_CUT);
8767 }
8768 break;
8769 case KeyEvent.KEYCODE_C:
8770 if (canCopy()) {
8771 return onTextContextMenuItem(ID_COPY);
8772 }
8773 break;
8774 case KeyEvent.KEYCODE_V:
8775 if (canPaste()) {
8776 return onTextContextMenuItem(ID_PASTE);
8777 }
8778 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008779 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008780 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008781 return super.onKeyShortcut(keyCode, event);
8782 }
8783
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008784 /**
8785 * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
8786 * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
8787 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
8788 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008789 private boolean canSelectText() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008790 return hasSelectionController() && mText.length() != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008791 }
8792
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008793 /**
8794 * Test based on the <i>intrinsic</i> charateristics of the TextView.
8795 * The text must be spannable and the movement method must allow for arbitary selection.
8796 *
8797 * See also {@link #canSelectText()}.
8798 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07008799 private boolean textCanBeSelected() {
Gilles Debunne05336272010-07-09 20:13:45 -07008800 // prepareCursorController() relies on this method.
8801 // If you change this condition, make sure prepareCursorController is called anywhere
8802 // the value of this condition might be changed.
Gilles Debunnebb588da2011-07-11 18:26:19 -07008803 if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
8804 return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008805 }
8806
8807 private boolean canCut() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008808 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008809 return false;
8810 }
8811
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008812 if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
8813 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008814 }
8815
8816 return false;
8817 }
8818
8819 private boolean canCopy() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07008820 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008821 return false;
8822 }
8823
Gilles Debunne03789e82010-09-07 19:07:17 -07008824 if (mText.length() > 0 && hasSelection()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008825 return true;
8826 }
8827
8828 return false;
8829 }
8830
8831 private boolean canPaste() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008832 return (mText instanceof Editable &&
8833 mInput != null &&
8834 getSelectionStart() >= 0 &&
8835 getSelectionEnd() >= 0 &&
8836 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008837 hasPrimaryClip());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008838 }
8839
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008840 private static long packRangeInLong(int start, int end) {
Gilles Debunne05336272010-07-09 20:13:45 -07008841 return (((long) start) << 32) | end;
8842 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008843
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008844 private static int extractRangeStartFromLong(long range) {
8845 return (int) (range >>> 32);
8846 }
8847
8848 private static int extractRangeEndFromLong(long range) {
8849 return (int) (range & 0x00000000FFFFFFFFL);
8850 }
Gilles Debunnecbfbb522010-10-07 16:57:31 -07008851
Gilles Debunnec59269f2011-04-22 11:46:09 -07008852 private boolean selectAll() {
8853 final int length = mText.length();
8854 Selection.setSelection((Spannable) mText, 0, length);
8855 return length > 0;
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008856 }
8857
Gilles Debunnec59269f2011-04-22 11:46:09 -07008858 /**
8859 * Adjusts selection to the word under last touch offset.
8860 * Return true if the operation was successfully performed.
8861 */
8862 private boolean selectCurrentWord() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08008863 if (!canSelectText()) {
Gilles Debunnec59269f2011-04-22 11:46:09 -07008864 return false;
Gilles Debunne6da7e932010-12-07 14:28:14 -08008865 }
8866
Gilles Debunne710a9102010-11-23 16:50:28 -08008867 if (hasPasswordTransformationMethod()) {
Gilles Debunne87380bc2011-01-04 13:24:54 -08008868 // Always select all on a password field.
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008869 // Cut/copy menu entries are not available for passwords, but being able to select all
8870 // is however useful to delete or paste to replace the entire content.
Gilles Debunnec59269f2011-04-22 11:46:09 -07008871 return selectAll();
8872 }
8873
8874 int klass = mInputType & InputType.TYPE_MASK_CLASS;
8875 int variation = mInputType & InputType.TYPE_MASK_VARIATION;
8876
8877 // Specific text field types: select the entire text for these
8878 if (klass == InputType.TYPE_CLASS_NUMBER ||
8879 klass == InputType.TYPE_CLASS_PHONE ||
8880 klass == InputType.TYPE_CLASS_DATETIME ||
8881 variation == InputType.TYPE_TEXT_VARIATION_URI ||
8882 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
8883 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
8884 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
8885 return selectAll();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008886 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008887
Gilles Debunne87380bc2011-01-04 13:24:54 -08008888 long lastTouchOffsets = getLastTouchOffsets();
8889 final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
8890 final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008891
Gilles Debunnebb588da2011-07-11 18:26:19 -07008892 // Safety check in case standard touch event handling has been bypassed
8893 if (minOffset < 0 || minOffset >= mText.length()) return false;
8894 if (maxOffset < 0 || maxOffset >= mText.length()) return false;
8895
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008896 int selectionStart, selectionEnd;
8897
8898 // If a URLSpan (web address, email, phone...) is found at that position, select it.
8899 URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008900 if (urlSpans.length >= 1) {
8901 URLSpan urlSpan = urlSpans[0];
8902 selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
8903 selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008904 } else {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008905 final WordIterator wordIterator = getWordIterator();
8906 wordIterator.setCharSequence(mText, minOffset, maxOffset);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008907
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008908 selectionStart = wordIterator.getBeginning(minOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008909 if (selectionStart == BreakIterator.DONE) return false;
8910
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008911 selectionEnd = wordIterator.getEnd(maxOffset);
Gilles Debunnec59269f2011-04-22 11:46:09 -07008912 if (selectionEnd == BreakIterator.DONE) return false;
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008913
8914 if (selectionStart == selectionEnd) {
8915 // Possible when the word iterator does not properly handle the text's language
8916 long range = getCharRange(selectionStart);
8917 selectionStart = extractRangeStartFromLong(range);
8918 selectionEnd = extractRangeEndFromLong(range);
8919 }
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008920 }
8921
8922 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008923 return selectionEnd > selectionStart;
8924 }
8925
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008926 /**
8927 * This is a temporary method. Future versions may support multi-locale text.
8928 *
satok05f24702011-11-02 19:29:35 +09008929 * @return The locale that should be used for a word iterator and a spell checker
8930 * in this TextView, based on the current spell checker settings,
8931 * the current IME's locale, or the system default locale.
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008932 * @hide
8933 */
satok05f24702011-11-02 19:29:35 +09008934 public Locale getTextServicesLocale() {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008935 Locale locale = Locale.getDefault();
satok05f24702011-11-02 19:29:35 +09008936 final TextServicesManager textServicesManager = (TextServicesManager)
8937 mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
8938 final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true);
8939 if (subtype != null) {
8940 locale = new Locale(subtype.getLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008941 }
8942 return locale;
8943 }
8944
8945 void onLocaleChanged() {
8946 // Will be re-created on demand in getWordIterator with the proper new locale
8947 mWordIterator = null;
8948 }
8949
8950 /**
8951 * @hide
8952 */
8953 public WordIterator getWordIterator() {
8954 if (mWordIterator == null) {
satok05f24702011-11-02 19:29:35 +09008955 mWordIterator = new WordIterator(getTextServicesLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -07008956 }
8957 return mWordIterator;
8958 }
8959
Gilles Debunne8f3105f2011-10-03 18:28:59 -07008960 private long getCharRange(int offset) {
8961 final int textLength = mText.length();
8962 if (offset + 1 < textLength) {
8963 final char currentChar = mText.charAt(offset);
8964 final char nextChar = mText.charAt(offset + 1);
8965 if (Character.isSurrogatePair(currentChar, nextChar)) {
8966 return packRangeInLong(offset, offset + 2);
8967 }
8968 }
8969 if (offset < textLength) {
8970 return packRangeInLong(offset, offset + 1);
8971 }
8972 if (offset - 2 >= 0) {
8973 final char previousChar = mText.charAt(offset - 1);
8974 final char previousPreviousChar = mText.charAt(offset - 2);
8975 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
8976 return packRangeInLong(offset - 2, offset);
8977 }
8978 }
8979 if (offset - 1 >= 0) {
8980 return packRangeInLong(offset - 1, offset);
8981 }
8982 return packRangeInLong(offset, offset);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008983 }
8984
8985 private long getLastTouchOffsets() {
Gilles Debunne771d64b2011-11-10 10:12:13 +01008986 SelectionModifierCursorController selectionController = getSelectionController();
8987 final int minOffset = selectionController.getMinTouchOffset();
8988 final int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008989 return packRangeInLong(minOffset, maxOffset);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008990 }
Gilles Debunnedf4ee432010-08-25 19:13:48 -07008991
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008992 @Override
Svetoslav Ganov736c2752011-04-22 18:30:36 -07008993 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganov887e1a12011-04-29 15:09:28 -07008994 super.onPopulateAccessibilityEvent(event);
8995
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08008996 final boolean isPassword = hasPasswordTransformationMethod();
svetoslavganov75986cf2009-05-14 22:28:01 -07008997 if (!isPassword) {
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07008998 CharSequence text = getTextForAccessibility();
Svetoslav Ganovd37848a2011-09-20 14:03:55 -07008999 if (!TextUtils.isEmpty(text)) {
svetoslavganov75986cf2009-05-14 22:28:01 -07009000 event.getText().add(text);
9001 }
svetoslavganov75986cf2009-05-14 22:28:01 -07009002 }
svetoslavganov75986cf2009-05-14 22:28:01 -07009003 }
9004
Svetoslav Ganov30401322011-05-12 18:53:45 -07009005 @Override
9006 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
9007 super.onInitializeAccessibilityEvent(event);
9008
9009 final boolean isPassword = hasPasswordTransformationMethod();
9010 event.setPassword(isPassword);
Svetoslav Ganova0156172011-06-26 17:55:44 -07009011
9012 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
9013 event.setFromIndex(Selection.getSelectionStart(mText));
9014 event.setToIndex(Selection.getSelectionEnd(mText));
9015 event.setItemCount(mText.length());
9016 }
Svetoslav Ganov30401322011-05-12 18:53:45 -07009017 }
9018
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07009019 @Override
9020 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
9021 super.onInitializeAccessibilityNodeInfo(info);
9022
9023 final boolean isPassword = hasPasswordTransformationMethod();
9024 if (!isPassword) {
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07009025 info.setText(getTextForAccessibility());
Svetoslav Ganov8643aa02011-04-20 12:12:33 -07009026 }
9027 info.setPassword(isPassword);
9028 }
9029
Svetoslav Ganov4e03f592011-07-29 22:17:14 -07009030 @Override
9031 public void sendAccessibilityEvent(int eventType) {
9032 // Do not send scroll events since first they are not interesting for
9033 // accessibility and second such events a generated too frequently.
9034 // For details see the implementation of bringTextIntoView().
9035 if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
9036 return;
9037 }
9038 super.sendAccessibilityEvent(eventType);
9039 }
9040
Svetoslav Ganovab5a40572011-09-15 11:30:01 -07009041 /**
9042 * Gets the text reported for accessibility purposes. It is the
9043 * text if not empty or the hint.
9044 *
9045 * @return The accessibility text.
9046 */
9047 private CharSequence getTextForAccessibility() {
9048 CharSequence text = getText();
9049 if (TextUtils.isEmpty(text)) {
9050 text = getHint();
9051 }
9052 return text;
9053 }
9054
svetoslavganov75986cf2009-05-14 22:28:01 -07009055 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
9056 int fromIndex, int removedCount, int addedCount) {
9057 AccessibilityEvent event =
9058 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
9059 event.setFromIndex(fromIndex);
9060 event.setRemovedCount(removedCount);
9061 event.setAddedCount(addedCount);
9062 event.setBeforeText(beforeText);
9063 sendAccessibilityEventUnchecked(event);
9064 }
9065
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009066 /**
9067 * Returns whether this text view is a current input method target. The
9068 * default implementation just checks with {@link InputMethodManager}.
9069 */
9070 public boolean isInputMethodTarget() {
9071 InputMethodManager imm = InputMethodManager.peekInstance();
9072 return imm != null && imm.isActive(this);
9073 }
9074
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009075 // Selection context mode
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009076 private static final int ID_SELECT_ALL = android.R.id.selectAll;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009077 private static final int ID_CUT = android.R.id.cut;
9078 private static final int ID_COPY = android.R.id.copy;
9079 private static final int ID_PASTE = android.R.id.paste;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009080
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009081 /**
9082 * Called when a context menu option for the text view is selected. Currently
Gilles Debunne07194e52011-11-02 14:18:44 -07009083 * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
9084 * {@link android.R.id#copy} or {@link android.R.id#paste}.
Gilles Debunnec59269f2011-04-22 11:46:09 -07009085 *
9086 * @return true if the context menu item action was performed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009087 */
9088 public boolean onTextContextMenuItem(int id) {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009089 int min = 0;
9090 int max = mText.length();
Gilles Debunne64e54a62010-09-07 19:07:17 -07009091
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009092 if (isFocused()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07009093 final int selStart = getSelectionStart();
9094 final int selEnd = getSelectionEnd();
9095
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07009096 min = Math.max(0, Math.min(selStart, selEnd));
9097 max = Math.max(0, Math.max(selStart, selEnd));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009098 }
9099
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009100 switch (id) {
Jeff Brownc1df9072010-12-21 16:38:50 -08009101 case ID_SELECT_ALL:
Gilles Debunne299733e2011-02-07 17:11:41 -08009102 // This does not enter text selection mode. Text is highlighted, so that it can be
Gilles Debunnec59269f2011-04-22 11:46:09 -07009103 // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
Jeff Brownc1df9072010-12-21 16:38:50 -08009104 selectAll();
Jeff Brownc1df9072010-12-21 16:38:50 -08009105 return true;
9106
9107 case ID_PASTE:
9108 paste(min, max);
9109 return true;
9110
9111 case ID_CUT:
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009112 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009113 deleteText_internal(min, max);
Jeff Brownc1df9072010-12-21 16:38:50 -08009114 stopSelectionActionMode();
9115 return true;
9116
9117 case ID_COPY:
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009118 setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08009119 stopSelectionActionMode();
9120 return true;
9121 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009122 return false;
9123 }
9124
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009125 private CharSequence getTransformedText(int start, int end) {
9126 return removeSuggestionSpans(mTransformed.subSequence(start, end));
9127 }
9128
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009129 /**
9130 * Prepare text so that there are not zero or two spaces at beginning and end of region defined
9131 * by [min, max] when replacing this region by paste.
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009132 * 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 -08009133 * make sure we do not add an extra one from the paste content.
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009134 */
9135 private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009136 if (paste.length() > 0) {
9137 if (min > 0) {
9138 final char charBefore = mTransformed.charAt(min - 1);
9139 final char charAfter = paste.charAt(0);
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009140
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009141 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9142 // Two spaces at beginning of paste: remove one
9143 final int originalLength = mText.length();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009144 deleteText_internal(min - 1, min);
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009145 // Due to filters, there is no guarantee that exactly one character was
9146 // removed: count instead.
9147 final int delta = mText.length() - originalLength;
9148 min += delta;
9149 max += delta;
9150 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9151 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9152 // No space at beginning of paste: add one
9153 final int originalLength = mText.length();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009154 replaceText_internal(min, min, " ");
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009155 // Taking possible filters into account as above.
9156 final int delta = mText.length() - originalLength;
9157 min += delta;
9158 max += delta;
9159 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009160 }
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009161
9162 if (max < mText.length()) {
9163 final char charBefore = paste.charAt(paste.length() - 1);
9164 final char charAfter = mTransformed.charAt(max);
9165
9166 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9167 // Two spaces at end of paste: remove one
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009168 deleteText_internal(max, max + 1);
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009169 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9170 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9171 // No space at end of paste: add one
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009172 replaceText_internal(max, max, " ");
Gilles Debunnec0752ee2010-12-22 17:50:42 -08009173 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009174 }
9175 }
Gilles Debunne4ae0f292010-11-29 14:56:39 -08009176
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009177 return packRangeInLong(min, max);
9178 }
9179
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009180 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
9181 TextView shadowView = (TextView) inflate(mContext,
Gilles Debunnef170a342010-11-11 11:08:59 -08009182 com.android.internal.R.layout.text_drag_thumbnail, null);
9183
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009184 if (shadowView == null) {
Gilles Debunnef170a342010-11-11 11:08:59 -08009185 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
9186 }
9187
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009188 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
9189 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
Gilles Debunnef170a342010-11-11 11:08:59 -08009190 }
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009191 shadowView.setText(text);
9192 shadowView.setTextColor(getTextColors());
Gilles Debunnef170a342010-11-11 11:08:59 -08009193
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009194 shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
9195 shadowView.setGravity(Gravity.CENTER);
Gilles Debunnef170a342010-11-11 11:08:59 -08009196
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009197 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
Gilles Debunnef170a342010-11-11 11:08:59 -08009198 ViewGroup.LayoutParams.WRAP_CONTENT));
9199
9200 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009201 shadowView.measure(size, size);
Gilles Debunnef170a342010-11-11 11:08:59 -08009202
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009203 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
9204 shadowView.invalidate();
9205 return new DragShadowBuilder(shadowView);
Gilles Debunnef170a342010-11-11 11:08:59 -08009206 }
9207
Gilles Debunneaaa84792010-12-03 11:10:14 -08009208 private static class DragLocalState {
9209 public TextView sourceTextView;
9210 public int start, end;
9211
9212 public DragLocalState(TextView sourceTextView, int start, int end) {
9213 this.sourceTextView = sourceTextView;
9214 this.start = start;
9215 this.end = end;
9216 }
9217 }
9218
Gilles Debunnee15b3582010-06-16 15:17:21 -07009219 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009220 public boolean performLongClick() {
Gilles Debunnee28454a2011-09-07 18:03:44 -07009221 boolean handled = false;
9222 boolean vibrate = true;
9223
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009224 if (super.performLongClick()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009225 mDiscardNextActionUp = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009226 handled = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009227 }
Gilles Debunnef170a342010-11-11 11:08:59 -08009228
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009229 // Long press in empty space moves cursor and shows the Paste affordance if available.
Gilles Debunnee28454a2011-09-07 18:03:44 -07009230 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009231 mInsertionControllerEnabled) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -07009232 final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
Gilles Debunned1dc72a2010-11-30 10:16:35 -08009233 stopSelectionActionMode();
Gilles Debunne75beb332011-04-29 11:40:22 -07009234 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne6435a562011-08-04 21:22:30 -07009235 getInsertionController().showWithActionPopup();
Gilles Debunne299733e2011-02-07 17:11:41 -08009236 handled = true;
Gilles Debunnee28454a2011-09-07 18:03:44 -07009237 vibrate = false;
Gilles Debunne9948ad72010-11-24 14:00:46 -08009238 }
9239
Gilles Debunne299733e2011-02-07 17:11:41 -08009240 if (!handled && mSelectionActionMode != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009241 if (touchPositionIsInSelection()) {
9242 // Start a drag
9243 final int start = getSelectionStart();
9244 final int end = getSelectionEnd();
Gilles Debunnecf68fee2011-09-29 10:55:36 -07009245 CharSequence selectedText = getTransformedText(start, end);
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009246 ClipData data = ClipData.newPlainText(null, selectedText);
Gilles Debunneaaa84792010-12-03 11:10:14 -08009247 DragLocalState localState = new DragLocalState(this, start, end);
Christopher Tate02d2b3b2011-01-10 20:43:53 -08009248 startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009249 stopSelectionActionMode();
9250 } else {
Gilles Debunnef682a772011-08-31 15:49:10 -07009251 getSelectionController().hide();
Gilles Debunne2037b822011-04-22 13:07:33 -07009252 selectCurrentWord();
Gilles Debunne57324c72011-08-29 14:42:15 -07009253 getSelectionController().show();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009254 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009255 handled = true;
Gilles Debunnef170a342010-11-11 11:08:59 -08009256 }
9257
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08009258 // Start a new selection
Gilles Debunnee28454a2011-09-07 18:03:44 -07009259 if (!handled) {
Gilles Debunne8f3105f2011-10-03 18:28:59 -07009260 vibrate = handled = startSelectionActionMode();
Gilles Debunnee28454a2011-09-07 18:03:44 -07009261 }
9262
9263 if (vibrate) {
9264 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
9265 }
Gilles Debunne299733e2011-02-07 17:11:41 -08009266
9267 if (handled) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009268 mDiscardNextActionUp = true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009269 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009270
Gilles Debunne299733e2011-02-07 17:11:41 -08009271 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009272 }
9273
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009274 private boolean touchPositionIsInSelection() {
9275 int selectionStart = getSelectionStart();
9276 int selectionEnd = getSelectionEnd();
Gilles Debunne05336272010-07-09 20:13:45 -07009277
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009278 if (selectionStart == selectionEnd) {
9279 return false;
9280 }
Gilles Debunne05336272010-07-09 20:13:45 -07009281
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009282 if (selectionStart > selectionEnd) {
9283 int tmp = selectionStart;
9284 selectionStart = selectionEnd;
9285 selectionEnd = tmp;
Gilles Debunne05336272010-07-09 20:13:45 -07009286 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009287 }
Gilles Debunne05336272010-07-09 20:13:45 -07009288
Gilles Debunnee587d832010-11-23 20:20:11 -08009289 SelectionModifierCursorController selectionController = getSelectionController();
9290 int minOffset = selectionController.getMinTouchOffset();
9291 int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne05336272010-07-09 20:13:45 -07009292
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009293 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
9294 }
9295
Gilles Debunne21078e42011-08-02 10:22:35 -07009296 private PositionListener getPositionListener() {
9297 if (mPositionListener == null) {
9298 mPositionListener = new PositionListener();
9299 }
9300 return mPositionListener;
9301 }
9302
9303 private interface TextViewPositionListener {
Gilles Debunnef682a772011-08-31 15:49:10 -07009304 public void updatePosition(int parentPositionX, int parentPositionY,
9305 boolean parentPositionChanged, boolean parentScrolled);
Gilles Debunne21078e42011-08-02 10:22:35 -07009306 }
9307
9308 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
Luca Zanolin1564fc72011-09-07 00:01:28 +01009309 // 3 handles
9310 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
9311 private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
Gilles Debunne21078e42011-08-02 10:22:35 -07009312 private TextViewPositionListener[] mPositionListeners =
9313 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
9314 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
9315 private boolean mPositionHasChanged = true;
9316 // Absolute position of the TextView with respect to its parent window
9317 private int mPositionX, mPositionY;
9318 private int mNumberOfListeners;
Gilles Debunnef682a772011-08-31 15:49:10 -07009319 private boolean mScrollHasChanged;
Gilles Debunne21078e42011-08-02 10:22:35 -07009320
9321 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
9322 if (mNumberOfListeners == 0) {
9323 updatePosition();
9324 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9325 vto.addOnPreDrawListener(this);
9326 }
9327
9328 int emptySlotIndex = -1;
9329 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9330 TextViewPositionListener listener = mPositionListeners[i];
9331 if (listener == positionListener) {
9332 return;
9333 } else if (emptySlotIndex < 0 && listener == null) {
9334 emptySlotIndex = i;
9335 }
9336 }
9337
9338 mPositionListeners[emptySlotIndex] = positionListener;
9339 mCanMove[emptySlotIndex] = canMove;
9340 mNumberOfListeners++;
9341 }
9342
9343 public void removeSubscriber(TextViewPositionListener positionListener) {
9344 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9345 if (mPositionListeners[i] == positionListener) {
9346 mPositionListeners[i] = null;
9347 mNumberOfListeners--;
9348 break;
9349 }
9350 }
9351
9352 if (mNumberOfListeners == 0) {
9353 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9354 vto.removeOnPreDrawListener(this);
9355 }
9356 }
9357
9358 public int getPositionX() {
9359 return mPositionX;
9360 }
9361
9362 public int getPositionY() {
9363 return mPositionY;
9364 }
9365
9366 @Override
9367 public boolean onPreDraw() {
9368 updatePosition();
9369
9370 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009371 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
Gilles Debunne21078e42011-08-02 10:22:35 -07009372 TextViewPositionListener positionListener = mPositionListeners[i];
9373 if (positionListener != null) {
9374 positionListener.updatePosition(mPositionX, mPositionY,
Gilles Debunnef682a772011-08-31 15:49:10 -07009375 mPositionHasChanged, mScrollHasChanged);
Gilles Debunne21078e42011-08-02 10:22:35 -07009376 }
9377 }
9378 }
9379
Gilles Debunnef682a772011-08-31 15:49:10 -07009380 mScrollHasChanged = false;
Gilles Debunne21078e42011-08-02 10:22:35 -07009381 return true;
9382 }
9383
9384 private void updatePosition() {
9385 TextView.this.getLocationInWindow(mTempCoords);
9386
9387 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
9388
9389 mPositionX = mTempCoords[0];
9390 mPositionY = mTempCoords[1];
9391 }
9392
Gilles Debunnef682a772011-08-31 15:49:10 -07009393 public void onScrollChanged() {
9394 mScrollHasChanged = true;
9395 }
9396 }
9397
Gilles Debunne335c4e62011-12-01 15:36:08 -08009398 private boolean isPositionVisible(int positionX, int positionY) {
Gilles Debunne64901d42011-11-25 10:23:38 +01009399 synchronized (sTmpPosition) {
9400 final float[] position = sTmpPosition;
9401 position[0] = positionX;
9402 position[1] = positionY;
9403 View view = this;
9404
9405 while (view != null) {
9406 if (view != this) {
9407 // Local scroll is already taken into account in positionX/Y
9408 position[0] -= view.getScrollX();
9409 position[1] -= view.getScrollY();
9410 }
9411
9412 if (position[0] < 0 || position[1] < 0 ||
9413 position[0] > view.getWidth() || position[1] > view.getHeight()) {
9414 return false;
9415 }
9416
9417 if (!view.getMatrix().isIdentity()) {
9418 view.getMatrix().mapPoints(position);
9419 }
9420
9421 position[0] += view.getLeft();
9422 position[1] += view.getTop();
9423
9424 final ViewParent parent = view.getParent();
9425 if (parent instanceof View) {
9426 view = (View) parent;
9427 } else {
9428 // We've reached the ViewRoot, stop iterating
9429 view = null;
9430 }
9431 }
9432 }
9433
9434 // We've been able to walk up the view hierarchy and the position was never clipped
9435 return true;
9436 }
9437
Gilles Debunne335c4e62011-12-01 15:36:08 -08009438 private boolean isOffsetVisible(int offset) {
Gilles Debunne64901d42011-11-25 10:23:38 +01009439 final int line = mLayout.getLineForOffset(offset);
9440 final int lineBottom = mLayout.getLineBottom(line);
9441 final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
9442 return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
9443 lineBottom + viewportToContentVerticalOffset());
9444 }
9445
Gilles Debunnef682a772011-08-31 15:49:10 -07009446 @Override
9447 protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
9448 super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
9449 if (mPositionListener != null) {
9450 mPositionListener.onScrollChanged();
9451 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009452 }
9453
9454 private abstract class PinnedPopupWindow implements TextViewPositionListener {
9455 protected PopupWindow mPopupWindow;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009456 protected ViewGroup mContentView;
Gilles Debunne21078e42011-08-02 10:22:35 -07009457 int mPositionX, mPositionY;
9458
9459 protected abstract void createPopupWindow();
9460 protected abstract void initContentView();
9461 protected abstract int getTextOffset();
9462 protected abstract int getVerticalLocalPosition(int line);
9463 protected abstract int clipVertically(int positionY);
9464
9465 public PinnedPopupWindow() {
9466 createPopupWindow();
9467
9468 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9469 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
9470 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
9471
Gilles Debunne0eea6682011-08-29 13:30:31 -07009472 initContentView();
9473
9474 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9475 ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunne21078e42011-08-02 10:22:35 -07009476 mContentView.setLayoutParams(wrapContent);
9477
Gilles Debunne21078e42011-08-02 10:22:35 -07009478 mPopupWindow.setContentView(mContentView);
9479 }
9480
9481 public void show() {
Gilles Debunnef682a772011-08-31 15:49:10 -07009482 TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
Gilles Debunne21078e42011-08-02 10:22:35 -07009483
9484 computeLocalPosition();
9485
9486 final PositionListener positionListener = TextView.this.getPositionListener();
9487 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
9488 }
Gilles Debunne0eea6682011-08-29 13:30:31 -07009489
9490 protected void measureContent() {
9491 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9492 mContentView.measure(
9493 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
9494 View.MeasureSpec.AT_MOST),
9495 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
9496 View.MeasureSpec.AT_MOST));
9497 }
Gilles Debunne21078e42011-08-02 10:22:35 -07009498
Gilles Debunne0eea6682011-08-29 13:30:31 -07009499 /* The popup window will be horizontally centered on the getTextOffset() and vertically
9500 * positioned according to viewportToContentHorizontalOffset.
9501 *
9502 * This method assumes that mContentView has properly been measured from its content. */
Gilles Debunne21078e42011-08-02 10:22:35 -07009503 private void computeLocalPosition() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009504 measureContent();
Gilles Debunne21078e42011-08-02 10:22:35 -07009505 final int width = mContentView.getMeasuredWidth();
Gilles Debunne0eea6682011-08-29 13:30:31 -07009506 final int offset = getTextOffset();
Gilles Debunne21078e42011-08-02 10:22:35 -07009507 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
9508 mPositionX += viewportToContentHorizontalOffset();
9509
9510 final int line = mLayout.getLineForOffset(offset);
9511 mPositionY = getVerticalLocalPosition(line);
9512 mPositionY += viewportToContentVerticalOffset();
9513 }
9514
9515 private void updatePosition(int parentPositionX, int parentPositionY) {
9516 int positionX = parentPositionX + mPositionX;
9517 int positionY = parentPositionY + mPositionY;
9518
9519 positionY = clipVertically(positionY);
9520
9521 // Horizontal clipping
9522 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9523 final int width = mContentView.getMeasuredWidth();
9524 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
9525 positionX = Math.max(0, positionX);
9526
9527 if (isShowing()) {
9528 mPopupWindow.update(positionX, positionY, -1, -1);
9529 } else {
9530 mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
9531 positionX, positionY);
9532 }
9533 }
9534
9535 public void hide() {
9536 mPopupWindow.dismiss();
9537 TextView.this.getPositionListener().removeSubscriber(this);
9538 }
9539
9540 @Override
Gilles Debunnef682a772011-08-31 15:49:10 -07009541 public void updatePosition(int parentPositionX, int parentPositionY,
9542 boolean parentPositionChanged, boolean parentScrolled) {
9543 // Either parentPositionChanged or parentScrolled is true, check if still visible
Gilles Debunne64901d42011-11-25 10:23:38 +01009544 if (isShowing() && isOffsetVisible(getTextOffset())) {
Gilles Debunnef682a772011-08-31 15:49:10 -07009545 if (parentScrolled) computeLocalPosition();
Gilles Debunne21078e42011-08-02 10:22:35 -07009546 updatePosition(parentPositionX, parentPositionY);
9547 } else {
9548 hide();
9549 }
9550 }
9551
9552 public boolean isShowing() {
9553 return mPopupWindow.isShowing();
9554 }
9555 }
9556
Gilles Debunne0eea6682011-08-29 13:30:31 -07009557 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunne6435a562011-08-04 21:22:30 -07009558 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009559 private static final int ADD_TO_DICTIONARY = -1;
9560 private static final int DELETE_TEXT = -2;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009561 private SuggestionInfo[] mSuggestionInfos;
9562 private int mNumberOfSuggestions;
Gilles Debunne28294cc2011-08-24 12:02:05 -07009563 private boolean mCursorWasVisibleBeforeSuggestions;
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009564 private boolean mIsShowingUp = false;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009565 private SuggestionAdapter mSuggestionsAdapter;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009566 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
9567 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
9568
Gilles Debunne28294cc2011-08-24 12:02:05 -07009569 private class CustomPopupWindow extends PopupWindow {
9570 public CustomPopupWindow(Context context, int defStyle) {
9571 super(context, null, defStyle);
9572 }
9573
9574 @Override
9575 public void dismiss() {
9576 super.dismiss();
9577
Fabrice Di Meglio03e4d642011-09-06 19:06:06 -07009578 TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
9579
Gilles Debunne186aaf92011-09-16 14:26:12 -07009580 // Safe cast since show() checks that mText is an Editable
9581 ((Spannable) mText).removeSpan(mSuggestionRangeSpan);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009582
9583 setCursorVisible(mCursorWasVisibleBeforeSuggestions);
9584 if (hasInsertionController()) {
9585 getInsertionController().show();
9586 }
9587 }
9588 }
9589
9590 public SuggestionsPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009591 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009592 mSuggestionSpanComparator = new SuggestionSpanComparator();
9593 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
Gilles Debunne28294cc2011-08-24 12:02:05 -07009594 }
Gilles Debunne69340442011-03-31 13:37:51 -07009595
Gilles Debunne21078e42011-08-02 10:22:35 -07009596 @Override
9597 protected void createPopupWindow() {
Gilles Debunne28294cc2011-08-24 12:02:05 -07009598 mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
Gilles Debunne21078e42011-08-02 10:22:35 -07009599 com.android.internal.R.attr.textSuggestionsWindowStyle);
Gilles Debunne28885242011-07-25 18:56:09 -07009600 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Gilles Debunne28294cc2011-08-24 12:02:05 -07009601 mPopupWindow.setFocusable(true);
Gilles Debunne21078e42011-08-02 10:22:35 -07009602 mPopupWindow.setClippingEnabled(false);
9603 }
Gilles Debunne69340442011-03-31 13:37:51 -07009604
Gilles Debunne21078e42011-08-02 10:22:35 -07009605 @Override
9606 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009607 ListView listView = new ListView(TextView.this.getContext());
9608 mSuggestionsAdapter = new SuggestionAdapter();
9609 listView.setAdapter(mSuggestionsAdapter);
9610 listView.setOnItemClickListener(this);
9611 mContentView = listView;
Gilles Debunne28885242011-07-25 18:56:09 -07009612
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009613 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
9614 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
9615 for (int i = 0; i < mSuggestionInfos.length; i++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009616 mSuggestionInfos[i] = new SuggestionInfo();
Gilles Debunne28885242011-07-25 18:56:09 -07009617 }
Gilles Debunne69340442011-03-31 13:37:51 -07009618 }
9619
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009620 public boolean isShowingUp() {
9621 return mIsShowingUp;
9622 }
9623
9624 public void onParentLostFocus() {
9625 mIsShowingUp = false;
9626 }
9627
Gilles Debunne214a8622011-04-26 15:44:37 -07009628 private class SuggestionInfo {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009629 int suggestionStart, suggestionEnd; // range of actual suggestion within text
Gilles Debunneee511cc2011-05-05 14:57:50 -07009630 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009631 int suggestionIndex; // the index of this suggestion inside suggestionSpan
Gilles Debunne0eea6682011-08-29 13:30:31 -07009632 SpannableStringBuilder text = new SpannableStringBuilder();
Luca Zanolin2346e012011-09-06 22:56:47 +01009633 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
9634 android.R.style.TextAppearance_SuggestionHighlight);
Gilles Debunne0eea6682011-08-29 13:30:31 -07009635 }
9636
9637 private class SuggestionAdapter extends BaseAdapter {
9638 private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
9639 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
9640
9641 @Override
9642 public int getCount() {
9643 return mNumberOfSuggestions;
9644 }
9645
9646 @Override
9647 public Object getItem(int position) {
9648 return mSuggestionInfos[position];
9649 }
9650
9651 @Override
9652 public long getItemId(int position) {
9653 return position;
9654 }
9655
9656 @Override
9657 public View getView(int position, View convertView, ViewGroup parent) {
9658 TextView textView = (TextView) convertView;
9659
9660 if (textView == null) {
9661 textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
9662 false);
9663 }
9664
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009665 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
9666 textView.setText(suggestionInfo.text);
9667
9668 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9669 textView.setCompoundDrawablesWithIntrinsicBounds(
9670 com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
9671 } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9672 textView.setCompoundDrawablesWithIntrinsicBounds(
9673 com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
9674 } else {
9675 textView.setCompoundDrawables(null, null, null, null);
9676 }
9677
Gilles Debunne0eea6682011-08-29 13:30:31 -07009678 return textView;
9679 }
Gilles Debunne214a8622011-04-26 15:44:37 -07009680 }
9681
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009682 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
9683 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
9684 final int flag1 = span1.getFlags();
9685 final int flag2 = span2.getFlags();
9686 if (flag1 != flag2) {
9687 // The order here should match what is used in updateDrawState
9688 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9689 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9690 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9691 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9692 if (easy1 && !misspelled1) return -1;
9693 if (easy2 && !misspelled2) return 1;
9694 if (misspelled1) return -1;
9695 if (misspelled2) return 1;
9696 }
9697
9698 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
9699 }
9700 }
9701
Luca Zanoline3f89c02011-08-01 09:55:17 +01009702 /**
9703 * Returns the suggestion spans that cover the current cursor position. The suggestion
9704 * spans are sorted according to the length of text that they are attached to.
9705 */
9706 private SuggestionSpan[] getSuggestionSpans() {
9707 int pos = TextView.this.getSelectionStart();
9708 Spannable spannable = (Spannable) TextView.this.mText;
9709 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
9710
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009711 mSpansLengths.clear();
Luca Zanoline3f89c02011-08-01 09:55:17 +01009712 for (SuggestionSpan suggestionSpan : suggestionSpans) {
9713 int start = spannable.getSpanStart(suggestionSpan);
9714 int end = spannable.getSpanEnd(suggestionSpan);
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009715 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
Luca Zanoline3f89c02011-08-01 09:55:17 +01009716 }
9717
Gilles Debunnec9fd9782011-09-09 09:20:12 -07009718 // The suggestions are sorted according to their types (easy correction first, then
9719 // misspelled) and to the length of the text that they cover (shorter first).
9720 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
Luca Zanoline3f89c02011-08-01 09:55:17 +01009721 return suggestionSpans;
9722 }
9723
Gilles Debunne21078e42011-08-02 10:22:35 -07009724 @Override
Gilles Debunne69340442011-03-31 13:37:51 -07009725 public void show() {
9726 if (!(mText instanceof Editable)) return;
9727
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009728 updateSuggestions();
9729 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9730 setCursorVisible(false);
Gilles Debunne26c8b3a2011-10-12 14:06:58 -07009731 mIsShowingUp = true;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009732 super.show();
Gilles Debunne21078e42011-08-02 10:22:35 -07009733 }
9734
9735 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009736 protected void measureContent() {
9737 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9738 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
9739 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
9740 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
9741 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
9742
9743 int width = 0;
9744 View view = null;
9745 for (int i = 0; i < mNumberOfSuggestions; i++) {
9746 view = mSuggestionsAdapter.getView(i, view, mContentView);
Luca Zanolin58707d62011-09-28 09:27:49 +01009747 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
Gilles Debunne0eea6682011-08-29 13:30:31 -07009748 view.measure(horizontalMeasure, verticalMeasure);
9749 width = Math.max(width, view.getMeasuredWidth());
9750 }
9751
9752 // Enforce the width based on actual text widths
9753 mContentView.measure(
9754 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
9755 verticalMeasure);
9756
9757 Drawable popupBackground = mPopupWindow.getBackground();
9758 if (popupBackground != null) {
9759 if (mTempRect == null) mTempRect = new Rect();
9760 popupBackground.getPadding(mTempRect);
9761 width += mTempRect.left + mTempRect.right;
9762 }
9763 mPopupWindow.setWidth(width);
9764 }
9765
9766 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -07009767 protected int getTextOffset() {
9768 return getSelectionStart();
9769 }
9770
9771 @Override
9772 protected int getVerticalLocalPosition(int line) {
9773 return mLayout.getLineBottom(line);
9774 }
9775
9776 @Override
9777 protected int clipVertically(int positionY) {
9778 final int height = mContentView.getMeasuredHeight();
9779 final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9780 return Math.min(positionY, displayMetrics.heightPixels - height);
9781 }
9782
9783 @Override
9784 public void hide() {
9785 super.hide();
Gilles Debunne21078e42011-08-02 10:22:35 -07009786 }
9787
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009788 private void updateSuggestions() {
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009789 Spannable spannable = (Spannable) TextView.this.mText;
Luca Zanoline3f89c02011-08-01 09:55:17 +01009790 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
9791
satokb3fc1a52011-04-06 18:28:55 +09009792 final int nbSpans = suggestionSpans.length;
Gilles Debunne69340442011-03-31 13:37:51 -07009793
Gilles Debunne0eea6682011-08-29 13:30:31 -07009794 mNumberOfSuggestions = 0;
Gilles Debunne214a8622011-04-26 15:44:37 -07009795 int spanUnionStart = mText.length();
9796 int spanUnionEnd = 0;
9797
Gilles Debunnee90bed12011-08-30 14:28:27 -07009798 SuggestionSpan misspelledSpan = null;
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009799 int underlineColor = 0;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009800
Gilles Debunne69340442011-03-31 13:37:51 -07009801 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
satokb3fc1a52011-04-06 18:28:55 +09009802 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
9803 final int spanStart = spannable.getSpanStart(suggestionSpan);
9804 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009805 spanUnionStart = Math.min(spanStart, spanUnionStart);
9806 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
Gilles Debunne69340442011-03-31 13:37:51 -07009807
Gilles Debunnee90bed12011-08-30 14:28:27 -07009808 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
9809 misspelledSpan = suggestionSpan;
9810 }
9811
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009812 // The first span dictates the background color of the highlighted text
9813 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
9814
satokb3fc1a52011-04-06 18:28:55 +09009815 String[] suggestions = suggestionSpan.getSuggestions();
Gilles Debunne69340442011-03-31 13:37:51 -07009816 int nbSuggestions = suggestions.length;
9817 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
Gilles Debunne0eea6682011-08-29 13:30:31 -07009818 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunneee511cc2011-05-05 14:57:50 -07009819 suggestionInfo.suggestionSpan = suggestionSpan;
9820 suggestionInfo.suggestionIndex = suggestionIndex;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009821 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9822 suggestions[suggestionIndex]);
Gilles Debunne69340442011-03-31 13:37:51 -07009823
Gilles Debunne0eea6682011-08-29 13:30:31 -07009824 mNumberOfSuggestions++;
9825 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
Gilles Debunne214a8622011-04-26 15:44:37 -07009826 // Also end outer for loop
Gilles Debunne69340442011-03-31 13:37:51 -07009827 spanIndex = nbSpans;
9828 break;
9829 }
9830 }
9831 }
9832
Gilles Debunnee90bed12011-08-30 14:28:27 -07009833 for (int i = 0; i < mNumberOfSuggestions; i++) {
9834 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
9835 }
9836
Gilles Debunnee300be92011-12-06 10:15:56 -08009837 // Add to dictionary item if there is a span with the misspelled flag
Gilles Debunnee90bed12011-08-30 14:28:27 -07009838 if (misspelledSpan != null) {
9839 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
9840 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
9841 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
9842 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
Gilles Debunnee90bed12011-08-30 14:28:27 -07009843 suggestionInfo.suggestionSpan = misspelledSpan;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009844 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
Gilles Debunnee90bed12011-08-30 14:28:27 -07009845 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9846 getContext().getString(com.android.internal.R.string.addToDictionary));
Gilles Debunnee6701012011-09-29 10:33:19 -07009847 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9848 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunnee90bed12011-08-30 14:28:27 -07009849
9850 mNumberOfSuggestions++;
9851 }
9852 }
9853
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009854 // Delete item
9855 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9856 suggestionInfo.suggestionSpan = null;
9857 suggestionInfo.suggestionIndex = DELETE_TEXT;
9858 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9859 getContext().getString(com.android.internal.R.string.deleteText));
Gilles Debunnee6701012011-09-29 10:33:19 -07009860 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
9861 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009862 mNumberOfSuggestions++;
Gilles Debunne214a8622011-04-26 15:44:37 -07009863
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009864 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
9865 if (underlineColor == 0) {
9866 // Fallback on the default highlight color when the first span does not provide one
9867 mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
9868 } else {
Gilles Debunnec2deadc2011-09-30 10:43:43 -07009869 final float BACKGROUND_TRANSPARENCY = 0.4f;
Gilles Debunnefa4e2d92011-09-08 18:34:22 -07009870 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
9871 mSuggestionRangeSpan.setBackgroundColor(
9872 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
9873 }
9874 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
Gilles Debunne6435a562011-08-04 21:22:30 -07009875 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9876
Gilles Debunne0eea6682011-08-29 13:30:31 -07009877 mSuggestionsAdapter.notifyDataSetChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -07009878 }
9879
Luca Zanolin2346e012011-09-06 22:56:47 +01009880 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
9881 int unionEnd) {
Gilles Debunnebd4016e92011-09-14 10:10:41 -07009882 final Spannable text = (Spannable) mText;
9883 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
9884 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunne214a8622011-04-26 15:44:37 -07009885
Luca Zanolin2346e012011-09-06 22:56:47 +01009886 // Adjust the start/end of the suggestion span
9887 suggestionInfo.suggestionStart = spanStart - unionStart;
9888 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
9889 + suggestionInfo.text.length();
Gilles Debunnee6701012011-09-29 10:33:19 -07009890
Luca Zanolin2346e012011-09-06 22:56:47 +01009891 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
9892 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne214a8622011-04-26 15:44:37 -07009893
Luca Zanolin2346e012011-09-06 22:56:47 +01009894 // Add the text before and after the span.
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009895 suggestionInfo.text.insert(0, mText.toString().substring(unionStart, spanStart));
9896 suggestionInfo.text.append(mText.toString().substring(spanEnd, unionEnd));
Gilles Debunne214a8622011-04-26 15:44:37 -07009897 }
9898
Gilles Debunne69340442011-03-31 13:37:51 -07009899 @Override
Gilles Debunne0eea6682011-08-29 13:30:31 -07009900 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009901 Editable editable = (Editable) mText;
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009902 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Gilles Debunnee90bed12011-08-30 14:28:27 -07009903
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009904 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
9905 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
9906 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
Gilles Debunne28ef9042011-10-11 16:01:22 -07009907 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
9908 // Do not leave two adjacent spaces after deletion, or one at beginning of text
9909 if (spanUnionEnd < editable.length() &&
9910 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
9911 (spanUnionStart == 0 ||
9912 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009913 spanUnionEnd = spanUnionEnd + 1;
Gilles Debunne28ef9042011-10-11 16:01:22 -07009914 }
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009915 deleteText_internal(spanUnionStart, spanUnionEnd);
Gilles Debunne69340442011-03-31 13:37:51 -07009916 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009917 hide();
9918 return;
Gilles Debunne69340442011-03-31 13:37:51 -07009919 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009920
9921 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
9922 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
Gilles Debunnee300be92011-12-06 10:15:56 -08009923 if (spanStart < 0 || spanEnd <= spanStart) {
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009924 // Span has been removed
9925 hide();
9926 return;
9927 }
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009928 final String originalText = mText.toString().substring(spanStart, spanEnd);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009929
9930 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
9931 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
9932 intent.putExtra("word", originalText);
Jean Chalard5fa67372011-12-07 13:57:59 +09009933 intent.putExtra("locale", getTextServicesLocale().toString());
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009934 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
9935 getContext().startActivity(intent);
Gilles Debunne845d9c72011-09-23 11:09:46 -07009936 // There is no way to know if the word was indeed added. Re-check.
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009937 // TODO The ExtractEditText should remove the span in the original text instead
Gilles Debunne845d9c72011-09-23 11:09:46 -07009938 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunnec115fa02011-12-07 13:38:31 -08009939 updateSpellCheckSpans(spanStart, spanEnd, false);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009940 } else {
9941 // SuggestionSpans are removed by replace: save them before
9942 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
9943 SuggestionSpan.class);
9944 final int length = suggestionSpans.length;
9945 int[] suggestionSpansStarts = new int[length];
9946 int[] suggestionSpansEnds = new int[length];
9947 int[] suggestionSpansFlags = new int[length];
9948 for (int i = 0; i < length; i++) {
9949 final SuggestionSpan suggestionSpan = suggestionSpans[i];
9950 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
9951 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
9952 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
Gilles Debunne5915c882011-10-10 18:17:22 -07009953
9954 // Remove potential misspelled flags
9955 int suggestionSpanFlags = suggestionSpan.getFlags();
9956 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
9957 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
9958 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
9959 suggestionSpan.setFlags(suggestionSpanFlags);
9960 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009961 }
9962
9963 final int suggestionStart = suggestionInfo.suggestionStart;
9964 final int suggestionEnd = suggestionInfo.suggestionEnd;
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009965 final String suggestion = suggestionInfo.text.subSequence(
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009966 suggestionStart, suggestionEnd).toString();
Gilles Debunne39ba6d92011-11-09 05:26:26 +01009967 replaceText_internal(spanStart, spanEnd, suggestion);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009968
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009969 // Notify source IME of the suggestion pick. Do this before swaping texts.
9970 if (!TextUtils.isEmpty(
9971 suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
9972 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009973 if (imm != null) {
9974 imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
9975 suggestionInfo.suggestionIndex);
9976 }
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009977 }
9978
9979 // Swap text content between actual text and Suggestion span
9980 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
9981 suggestions[suggestionInfo.suggestionIndex] = originalText;
9982
9983 // Restore previous SuggestionSpans
9984 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
9985 for (int i = 0; i < length; i++) {
9986 // Only spans that include the modified region make sense after replacement
9987 // Spans partially included in the replaced region are removed, there is no
9988 // way to assign them a valid range after replacement
9989 if (suggestionSpansStarts[i] <= spanStart &&
9990 suggestionSpansEnds[i] >= spanEnd) {
Gilles Debunnee300be92011-12-06 10:15:56 -08009991 setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009992 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
9993 }
9994 }
9995
Gilles Debunnee1fc4f62011-10-03 17:01:19 -07009996 // Move cursor at the end of the replaced word
Gilles Debunnee300be92011-12-06 10:15:56 -08009997 final int newCursorPosition = spanEnd + lengthDifference;
9998 setCursorPosition_internal(newCursorPosition, newCursorPosition);
Gilles Debunnea6c673b2011-09-27 16:45:03 -07009999 }
10000
10001 hide();
Gilles Debunne69340442011-03-31 13:37:51 -070010002 }
Gilles Debunne69340442011-03-31 13:37:51 -070010003 }
10004
Luca Zanoline0760452011-09-08 12:03:37 +010010005 /**
10006 * Removes the suggestion spans.
10007 */
10008 CharSequence removeSuggestionSpans(CharSequence text) {
10009 if (text instanceof Spanned) {
10010 Spannable spannable;
10011 if (text instanceof Spannable) {
10012 spannable = (Spannable) text;
10013 } else {
10014 spannable = new SpannableString(text);
10015 text = spannable;
10016 }
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010017
Luca Zanoline0760452011-09-08 12:03:37 +010010018 SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
10019 for (int i = 0; i < spans.length; i++) {
10020 spannable.removeSpan(spans[i]);
10021 }
10022 }
10023 return text;
10024 }
10025
10026 void showSuggestions() {
Gilles Debunne69340442011-03-31 13:37:51 -070010027 if (mSuggestionsPopupWindow == null) {
10028 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
10029 }
10030 hideControllers();
10031 mSuggestionsPopupWindow.show();
10032 }
10033
Gilles Debunne4a7199a2011-07-11 14:58:27 -070010034 boolean areSuggestionsShown() {
10035 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
Gilles Debunne6435a562011-08-04 21:22:30 -070010036 }
10037
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010038 /**
Gilles Debunne6435a562011-08-04 21:22:30 -070010039 * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
10040 * by the IME or by the spell checker as the user types. This is done by adding
10041 * {@link SuggestionSpan}s to the text.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010042 *
10043 * When suggestions are enabled (default), this list of suggestions will be displayed when the
Gilles Debunne6435a562011-08-04 21:22:30 -070010044 * user asks for them on these parts of the text. This value depends on the inputType of this
10045 * TextView.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010046 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010047 * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
10048 *
10049 * In addition, the type variation must be one of
Gilles Debunne248b1122011-08-12 13:24:16 -070010050 * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
10051 * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
10052 * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
10053 * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
10054 * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
10055 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010056 * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010057 *
Gilles Debunne6435a562011-08-04 21:22:30 -070010058 * @return true if the suggestions popup window is enabled, based on the inputType.
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010059 */
10060 public boolean isSuggestionsEnabled() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010061 if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false;
Gilles Debunne6435a562011-08-04 21:22:30 -070010062 if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
10063
Luca Zanolin15b80162011-08-17 18:47:27 +010010064 final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Gilles Debunne6435a562011-08-04 21:22:30 -070010065 return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL ||
Gilles Debunne248b1122011-08-12 13:24:16 -070010066 variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT ||
10067 variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE ||
10068 variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE ||
Gilles Debunne6435a562011-08-04 21:22:30 -070010069 variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
Gilles Debunnef3a135b2011-05-23 16:28:47 -070010070 }
10071
10072 /**
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010073 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
10074 * selection is initiated in this View.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010075 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010076 * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
10077 * Paste actions, depending on what this View supports.
10078 *
10079 * A custom implementation can add new entries in the default menu in its
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010080 * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The
10081 * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and
10082 * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
10083 * or {@link android.R.id#paste} ids as parameters.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010084 *
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010085 * Returning false from
10086 * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent
10087 * the action mode from being started.
Gilles Debunneddd6f392011-01-27 09:48:01 -080010088 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010089 * Action click events should be handled by the custom implementation of
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010090 * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010091 *
10092 * Note that text selection mode is not started when a TextView receives focus and the
10093 * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
10094 * that case, to allow for quick replacement.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010095 */
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010096 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
10097 mCustomSelectionActionModeCallback = actionModeCallback;
10098 }
10099
10100 /**
10101 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
10102 *
10103 * @return The current custom selection callback.
10104 */
10105 public ActionMode.Callback getCustomSelectionActionModeCallback() {
10106 return mCustomSelectionActionModeCallback;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010107 }
10108
10109 /**
10110 *
10111 * @return true if the selection mode was actually started.
10112 */
10113 private boolean startSelectionActionMode() {
10114 if (mSelectionActionMode != null) {
10115 // Selection action mode is already started
10116 return false;
10117 }
10118
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010119 if (!canSelectText() || !requestFocus()) {
10120 Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
10121 return false;
10122 }
10123
Gilles Debunneed674182011-06-02 19:52:22 -070010124 if (!hasSelection()) {
10125 // There may already be a selection on device rotation
Gilles Debunnee1fc4f62011-10-03 17:01:19 -070010126 if (!selectCurrentWord()) {
Gilles Debunneed674182011-06-02 19:52:22 -070010127 // No word found under cursor or text selection not permitted.
10128 return false;
10129 }
Gilles Debunnec01f3fe2010-12-22 17:07:36 -080010130 }
10131
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010132 boolean willExtract = extractedTextModeWillBeStarted();
Gilles Debunne17d31de2011-01-27 11:02:18 -080010133
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010134 // Do not start the action mode when extracted text will show up full screen, thus
10135 // immediately hiding the newly created action bar, which would be visually distracting.
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010136 if (!willExtract) {
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010137 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
10138 mSelectionActionMode = startActionMode(actionModeCallback);
10139 }
Gilles Debunne98fb9ed2011-09-07 17:15:41 -070010140
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010141 final boolean selectionStarted = mSelectionActionMode != null || willExtract;
10142 if (selectionStarted && !mTextIsSelectable && mSoftInputShownOnFocus) {
Gilles Debunne17d31de2011-01-27 11:02:18 -080010143 // Show the IME to be able to replace text, except when selecting non editable text.
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010144 final InputMethodManager imm = InputMethodManager.peekInstance();
10145 if (imm != null) {
10146 imm.showSoftInput(this, 0, null);
10147 }
Gilles Debunne17d31de2011-01-27 11:02:18 -080010148 }
10149
10150 return selectionStarted;
Gilles Debunne05336272010-07-09 20:13:45 -070010151 }
10152
Gilles Debunne61ddbba2011-11-09 09:48:40 +010010153 private boolean extractedTextModeWillBeStarted() {
10154 if (!(this instanceof ExtractEditText)) {
10155 final InputMethodManager imm = InputMethodManager.peekInstance();
10156 return imm != null && imm.isFullscreenMode();
10157 }
10158 return false;
10159 }
10160
Gilles Debunneed279f82010-08-18 21:24:35 -070010161 private void stopSelectionActionMode() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010162 if (mSelectionActionMode != null) {
Gilles Debunned94f8c52011-01-10 11:29:15 -080010163 // This will hide the mSelectionModifierCursorController
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010164 mSelectionActionMode.finish();
10165 }
10166 }
10167
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010168 /**
10169 * Paste clipboard content between min and max positions.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010170 */
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010171 private void paste(int min, int max) {
10172 ClipboardManager clipboard =
10173 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010174 ClipData clip = clipboard.getPrimaryClip();
10175 if (clip != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010176 boolean didFirst = false;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010177 for (int i=0; i<clip.getItemCount(); i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080010178 CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010179 if (paste != null) {
Gilles Debunne75beb332011-04-29 11:40:22 -070010180 if (!didFirst) {
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010181 long minMax = prepareSpacesAroundPaste(min, max, paste);
10182 min = extractRangeStartFromLong(minMax);
10183 max = extractRangeEndFromLong(minMax);
10184 Selection.setSelection((Spannable) mText, max);
10185 ((Editable) mText).replace(min, max, paste);
Gilles Debunne75beb332011-04-29 11:40:22 -070010186 didFirst = true;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010187 } else {
10188 ((Editable) mText).insert(getSelectionEnd(), "\n");
10189 ((Editable) mText).insert(getSelectionEnd(), paste);
10190 }
10191 }
10192 }
10193 stopSelectionActionMode();
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010194 sLastCutOrCopyTime = 0;
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010195 }
10196 }
10197
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010198 private void setPrimaryClip(ClipData clip) {
10199 ClipboardManager clipboard = (ClipboardManager) getContext().
10200 getSystemService(Context.CLIPBOARD_SERVICE);
10201 clipboard.setPrimaryClip(clip);
10202 sLastCutOrCopyTime = SystemClock.uptimeMillis();
10203 }
10204
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010205 /**
10206 * An ActionMode Callback class that is used to provide actions while in text selection mode.
10207 *
10208 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
10209 * on which of these this TextView supports.
10210 */
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010211 private class SelectionActionModeCallback implements ActionMode.Callback {
10212
10213 @Override
10214 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Adam Powelld2b58942011-10-03 13:41:03 -070010215 TypedArray styledAttributes = mContext.obtainStyledAttributes(
10216 com.android.internal.R.styleable.SelectionModeDrawables);
Gilles Debunne78996c92010-10-12 16:01:47 -070010217
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010218 boolean allowText = getContext().getResources().getBoolean(
Adam Powell35aecd52011-07-01 13:43:49 -070010219 com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010220
Gilles Debunne21078e42011-08-02 10:22:35 -070010221 mode.setTitle(allowText ?
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010222 mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010223 mode.setSubtitle(null);
10224
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010225 int selectAllIconId = 0; // No icon by default
10226 if (!allowText) {
10227 // Provide an icon, text will not be displayed on smaller screens.
10228 selectAllIconId = styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010229 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010230 }
10231
Gilles Debunnecbcb3452010-12-17 15:31:02 -080010232 menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Gilles Debunne5e9af2d2011-05-27 17:28:11 -070010233 setIcon(selectAllIconId).
Adam Powelld8404b22010-10-13 14:26:41 -070010234 setAlphabeticShortcut('a').
10235 setShowAsAction(
10236 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010237
10238 if (canCut()) {
10239 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010240 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010241 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010242 setAlphabeticShortcut('x').
10243 setShowAsAction(
10244 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010245 }
10246
10247 if (canCopy()) {
10248 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010249 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010250 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010251 setAlphabeticShortcut('c').
10252 setShowAsAction(
10253 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010254 }
10255
10256 if (canPaste()) {
10257 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010258 setIcon(styledAttributes.getResourceId(
Adam Powelld2b58942011-10-03 13:41:03 -070010259 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -070010260 setAlphabeticShortcut('v').
10261 setShowAsAction(
10262 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010263 }
10264
Gilles Debunne78996c92010-10-12 16:01:47 -070010265 styledAttributes.recycle();
10266
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010267 if (mCustomSelectionActionModeCallback != null) {
Gilles Debunneddd6f392011-01-27 09:48:01 -080010268 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
10269 // The custom mode can choose to cancel the action mode
10270 return false;
10271 }
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010272 }
10273
10274 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -080010275 getSelectionController().show();
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010276 return true;
10277 } else {
10278 return false;
10279 }
Gilles Debunne05336272010-07-09 20:13:45 -070010280 }
10281
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010282 @Override
10283 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010284 if (mCustomSelectionActionModeCallback != null) {
10285 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
10286 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010287 return true;
10288 }
10289
10290 @Override
10291 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010292 if (mCustomSelectionActionModeCallback != null &&
10293 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
10294 return true;
10295 }
Jeff Brownc1df9072010-12-21 16:38:50 -080010296 return onTextContextMenuItem(item.getItemId());
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010297 }
10298
10299 @Override
10300 public void onDestroyActionMode(ActionMode mode) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -080010301 if (mCustomSelectionActionModeCallback != null) {
10302 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
10303 }
Gilles Debunned94f8c52011-01-10 11:29:15 -080010304 Selection.setSelection((Spannable) mText, getSelectionEnd());
10305
10306 if (mSelectionModifierCursorController != null) {
10307 mSelectionModifierCursorController.hide();
10308 }
10309
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010310 mSelectionActionMode = null;
10311 }
10312 }
Gilles Debunne05336272010-07-09 20:13:45 -070010313
Gilles Debunne21078e42011-08-02 10:22:35 -070010314 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
10315 private static final int POPUP_TEXT_LAYOUT =
Gilles Debunne646f8562011-07-27 17:44:03 -070010316 com.android.internal.R.layout.text_edit_action_popup_text;
Gilles Debunne646f8562011-07-27 17:44:03 -070010317 private TextView mPasteTextView;
10318 private TextView mReplaceTextView;
Gilles Debunne646f8562011-07-27 17:44:03 -070010319
Gilles Debunne21078e42011-08-02 10:22:35 -070010320 @Override
10321 protected void createPopupWindow() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010322 mPopupWindow = new PopupWindow(TextView.this.mContext, null,
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010323 com.android.internal.R.attr.textSelectHandleWindowStyle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010324 mPopupWindow.setClippingEnabled(true);
Gilles Debunne21078e42011-08-02 10:22:35 -070010325 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010326
Gilles Debunne21078e42011-08-02 10:22:35 -070010327 @Override
10328 protected void initContentView() {
Gilles Debunne0eea6682011-08-29 13:30:31 -070010329 LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
10330 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
10331 mContentView = linearLayout;
Gilles Debunne646f8562011-07-27 17:44:03 -070010332 mContentView.setBackgroundResource(
Gilles Debunne0eea6682011-08-29 13:30:31 -070010333 com.android.internal.R.drawable.text_edit_paste_window);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010334
Gilles Debunne646f8562011-07-27 17:44:03 -070010335 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010336 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010337
Gilles Debunne21078e42011-08-02 10:22:35 -070010338 LayoutParams wrapContent = new LayoutParams(
10339 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
10340
10341 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010342 mPasteTextView.setLayoutParams(wrapContent);
10343 mContentView.addView(mPasteTextView);
10344 mPasteTextView.setText(com.android.internal.R.string.paste);
10345 mPasteTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010346
Gilles Debunne21078e42011-08-02 10:22:35 -070010347 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
Gilles Debunne646f8562011-07-27 17:44:03 -070010348 mReplaceTextView.setLayoutParams(wrapContent);
10349 mContentView.addView(mReplaceTextView);
10350 mReplaceTextView.setText(com.android.internal.R.string.replace);
10351 mReplaceTextView.setOnClickListener(this);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010352 }
10353
Gilles Debunne21078e42011-08-02 10:22:35 -070010354 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010355 public void show() {
Gilles Debunne248b1122011-08-12 13:24:16 -070010356 boolean canPaste = canPaste();
Gilles Debunne6435a562011-08-04 21:22:30 -070010357 boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
Gilles Debunne248b1122011-08-12 13:24:16 -070010358 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
Gilles Debunne6435a562011-08-04 21:22:30 -070010359 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
Gilles Debunne072b50c2011-08-02 20:22:43 -070010360
Gilles Debunne6435a562011-08-04 21:22:30 -070010361 if (!canPaste && !canSuggest) return;
Gilles Debunne072b50c2011-08-02 20:22:43 -070010362
Gilles Debunne21078e42011-08-02 10:22:35 -070010363 super.show();
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010364 }
10365
10366 @Override
Gilles Debunne646f8562011-07-27 17:44:03 -070010367 public void onClick(View view) {
10368 if (view == mPasteTextView && canPaste()) {
Gilles Debunne459ac632011-07-12 16:36:33 -070010369 onTextContextMenuItem(ID_PASTE);
Gilles Debunne646f8562011-07-27 17:44:03 -070010370 hide();
10371 } else if (view == mReplaceTextView) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010372 final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
10373 stopSelectionActionMode();
10374 Selection.setSelection((Spannable) mText, middle);
Gilles Debunne646f8562011-07-27 17:44:03 -070010375 showSuggestions();
Gilles Debunnee1c14e62010-11-03 19:24:29 -070010376 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010377 }
10378
Gilles Debunne21078e42011-08-02 10:22:35 -070010379 @Override
10380 protected int getTextOffset() {
10381 return (getSelectionStart() + getSelectionEnd()) / 2;
10382 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010383
Gilles Debunne21078e42011-08-02 10:22:35 -070010384 @Override
10385 protected int getVerticalLocalPosition(int line) {
10386 return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
10387 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010388
Gilles Debunne21078e42011-08-02 10:22:35 -070010389 @Override
10390 protected int clipVertically(int positionY) {
10391 if (positionY < 0) {
10392 final int offset = getTextOffset();
10393 final int line = mLayout.getLineForOffset(offset);
10394 positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
10395 positionY += mContentView.getMeasuredHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010396
Gilles Debunne646f8562011-07-27 17:44:03 -070010397 // Assumes insertion and selection handles share the same height
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010398 final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
Gilles Debunne21078e42011-08-02 10:22:35 -070010399 positionY += handle.getIntrinsicHeight();
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010400 }
10401
Gilles Debunne21078e42011-08-02 10:22:35 -070010402 return positionY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010403 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010404 }
10405
Gilles Debunne21078e42011-08-02 10:22:35 -070010406 private abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010407 protected Drawable mDrawable;
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010408 protected Drawable mDrawableLtr;
10409 protected Drawable mDrawableRtl;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010410 private final PopupWindow mContainer;
10411 // Position with respect to the parent TextView
10412 private int mPositionX, mPositionY;
Adam Powell879fb6b2010-09-20 11:23:56 -070010413 private boolean mIsDragging;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010414 // Offset from touch position to mPosition
10415 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
Gilles Debunne21078e42011-08-02 10:22:35 -070010416 protected int mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010417 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
Adam Powellfbb3b472010-10-06 21:04:35 -070010418 private float mTouchOffsetY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010419 // Where the touch position should be on the handle to ensure a maximum cursor visibility
10420 private float mIdealVerticalOffset;
Gilles Debunne2037b822011-04-22 13:07:33 -070010421 // Parent's (TextView) previous position in window
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010422 private int mLastParentX, mLastParentY;
Gilles Debunne646f8562011-07-27 17:44:03 -070010423 // Transient action popup window for Paste and Replace actions
10424 protected ActionPopupWindow mActionPopupWindow;
Gilles Debunne21078e42011-08-02 10:22:35 -070010425 // Previous text character offset
10426 private int mPreviousOffset = -1;
10427 // Previous text character offset
10428 private boolean mPositionHasChanged = true;
Gilles Debunne646f8562011-07-27 17:44:03 -070010429 // Used to delay the appearance of the action popup window
10430 private Runnable mActionPopupShower;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010431
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010432 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010433 super(TextView.this.mContext);
10434 mContainer = new PopupWindow(TextView.this.mContext, null,
10435 com.android.internal.R.attr.textSelectHandleWindowStyle);
10436 mContainer.setSplitTouchEnabled(true);
10437 mContainer.setClippingEnabled(false);
10438 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
10439 mContainer.setContentView(this);
10440
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010441 mDrawableLtr = drawableLtr;
10442 mDrawableRtl = drawableRtl;
10443
10444 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010445
10446 final int handleHeight = mDrawable.getIntrinsicHeight();
10447 mTouchOffsetY = -0.3f * handleHeight;
10448 mIdealVerticalOffset = 0.7f * handleHeight;
10449 }
10450
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010451 protected void updateDrawable() {
10452 final int offset = getCurrentCursorOffset();
10453 final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
10454 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
10455 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
10456 }
10457
10458 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070010459
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010460 // Touch-up filter: number of previous positions remembered
10461 private static final int HISTORY_SIZE = 5;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010462 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
10463 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010464 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
10465 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
10466 private int mPreviousOffsetIndex = 0;
10467 private int mNumberPreviousOffsets = 0;
10468
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010469 private void startTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010470 mNumberPreviousOffsets = 0;
10471 addPositionToTouchUpFilter(offset);
10472 }
10473
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010474 private void addPositionToTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010475 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
10476 mPreviousOffsets[mPreviousOffsetIndex] = offset;
10477 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
10478 mNumberPreviousOffsets++;
10479 }
10480
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010481 private void filterOnTouchUp() {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010482 final long now = SystemClock.uptimeMillis();
10483 int i = 0;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010484 int index = mPreviousOffsetIndex;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010485 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010486 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010487 i++;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010488 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010489 }
10490
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010491 if (i > 0 && i < iMax &&
10492 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010493 positionAtCursorOffset(mPreviousOffsets[index], false);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -080010494 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010495 }
10496
Gilles Debunne040023a2011-08-02 18:23:31 -070010497 public boolean offsetHasBeenChanged() {
10498 return mNumberPreviousOffsets > 1;
10499 }
10500
Adam Powell879fb6b2010-09-20 11:23:56 -070010501 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010502 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010503 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
Adam Powell879fb6b2010-09-20 11:23:56 -070010504 }
10505
10506 public void show() {
Gilles Debunne21078e42011-08-02 10:22:35 -070010507 if (isShowing()) return;
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010508
Gilles Debunnef682a772011-08-31 15:49:10 -070010509 getPositionListener().addSubscriber(this, true /* local position may change */);
Gilles Debunne21078e42011-08-02 10:22:35 -070010510
10511 // Make sure the offset is always considered new, even when focusing at same position
10512 mPreviousOffset = -1;
Gilles Debunnef682a772011-08-31 15:49:10 -070010513 positionAtCursorOffset(getCurrentCursorOffset(), false);
Gilles Debunne21078e42011-08-02 10:22:35 -070010514
Gilles Debunne646f8562011-07-27 17:44:03 -070010515 hideActionPopupWindow();
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010516 }
10517
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010518 protected void dismiss() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010519 mIsDragging = false;
10520 mContainer.dismiss();
Gilles Debunne646f8562011-07-27 17:44:03 -070010521 onDetached();
Adam Powell879fb6b2010-09-20 11:23:56 -070010522 }
10523
10524 public void hide() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010525 dismiss();
10526
Gilles Debunne21078e42011-08-02 10:22:35 -070010527 TextView.this.getPositionListener().removeSubscriber(this);
Gilles Debunne646f8562011-07-27 17:44:03 -070010528 }
10529
Gilles Debunnecb2516b2011-08-05 17:02:54 -070010530 void showActionPopupWindow(int delay) {
Gilles Debunne646f8562011-07-27 17:44:03 -070010531 if (mActionPopupWindow == null) {
10532 mActionPopupWindow = new ActionPopupWindow();
10533 }
10534 if (mActionPopupShower == null) {
10535 mActionPopupShower = new Runnable() {
10536 public void run() {
10537 mActionPopupWindow.show();
10538 }
10539 };
10540 } else {
10541 TextView.this.removeCallbacks(mActionPopupShower);
10542 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010543 TextView.this.postDelayed(mActionPopupShower, delay);
10544 }
10545
10546 protected void hideActionPopupWindow() {
10547 if (mActionPopupShower != null) {
10548 TextView.this.removeCallbacks(mActionPopupShower);
10549 }
10550 if (mActionPopupWindow != null) {
10551 mActionPopupWindow.hide();
10552 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010553 }
10554
10555 public boolean isShowing() {
10556 return mContainer.isShowing();
10557 }
10558
Gilles Debunne21078e42011-08-02 10:22:35 -070010559 private boolean isVisible() {
Adam Powellabcbb1a2010-10-04 21:12:19 -070010560 // Always show a dragging handle.
10561 if (mIsDragging) {
10562 return true;
10563 }
10564
Adam Powell965b9692010-10-21 18:44:32 -070010565 if (isInBatchEditMode()) {
10566 return false;
10567 }
10568
Gilles Debunne64901d42011-11-25 10:23:38 +010010569 return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY);
Adam Powell879fb6b2010-09-20 11:23:56 -070010570 }
10571
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010572 public abstract int getCurrentCursorOffset();
10573
Gilles Debunne186aaf92011-09-16 14:26:12 -070010574 protected abstract void updateSelection(int offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010575
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010576 public abstract void updatePosition(float x, float y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010577
Gilles Debunnef682a772011-08-31 15:49:10 -070010578 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010579 // A HandleView relies on the layout, which may be nulled by external methods
Gilles Debunned4bb0b02011-05-27 15:58:55 -070010580 if (mLayout == null) {
10581 // Will update controllers' state, hiding them and stopping selection mode if needed
10582 prepareCursorControllers();
10583 return;
10584 }
10585
Gilles Debunnef682a772011-08-31 15:49:10 -070010586 if (offset != mPreviousOffset || parentScrolled) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010587 updateSelection(offset);
10588 addPositionToTouchUpFilter(offset);
10589 final int line = mLayout.getLineForOffset(offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010590
Gilles Debunne21078e42011-08-02 10:22:35 -070010591 mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
10592 mPositionY = mLayout.getLineBottom(line);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010593
Gilles Debunnefec22c62011-10-21 13:48:18 -070010594 // Take TextView's padding and scroll into account.
Gilles Debunne21078e42011-08-02 10:22:35 -070010595 mPositionX += viewportToContentHorizontalOffset();
10596 mPositionY += viewportToContentVerticalOffset();
10597
10598 mPreviousOffset = offset;
10599 mPositionHasChanged = true;
10600 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010601 }
10602
Gilles Debunnef682a772011-08-31 15:49:10 -070010603 public void updatePosition(int parentPositionX, int parentPositionY,
10604 boolean parentPositionChanged, boolean parentScrolled) {
10605 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
10606 if (parentPositionChanged || mPositionHasChanged) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010607 if (mIsDragging) {
Gilles Debunne21078e42011-08-02 10:22:35 -070010608 // Update touchToWindow offset in case of parent scrolling while dragging
10609 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
10610 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
10611 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
10612 mLastParentX = parentPositionX;
10613 mLastParentY = parentPositionY;
Gilles Debunne2037b822011-04-22 13:07:33 -070010614 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010615
Gilles Debunne3784a7f2011-07-15 13:49:38 -070010616 onHandleMoved();
10617 }
Gilles Debunne2037b822011-04-22 13:07:33 -070010618
Gilles Debunne21078e42011-08-02 10:22:35 -070010619 if (isVisible()) {
10620 final int positionX = parentPositionX + mPositionX;
10621 final int positionY = parentPositionY + mPositionY;
10622 if (isShowing()) {
10623 mContainer.update(positionX, positionY, -1, -1);
10624 } else {
10625 mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
10626 positionX, positionY);
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010627 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010628 } else {
Gilles Debunnecfc22c52011-03-07 15:50:47 -080010629 if (isShowing()) {
10630 dismiss();
10631 }
Adam Powell879fb6b2010-09-20 11:23:56 -070010632 }
Gilles Debunne21078e42011-08-02 10:22:35 -070010633
10634 mPositionHasChanged = false;
Adam Powell879fb6b2010-09-20 11:23:56 -070010635 }
10636 }
10637
10638 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -070010639 protected void onDraw(Canvas c) {
Adam Powell879fb6b2010-09-20 11:23:56 -070010640 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010641 mDrawable.draw(c);
Adam Powell879fb6b2010-09-20 11:23:56 -070010642 }
10643
10644 @Override
10645 public boolean onTouchEvent(MotionEvent ev) {
10646 switch (ev.getActionMasked()) {
Gilles Debunne874d77c2011-01-25 15:29:13 -080010647 case MotionEvent.ACTION_DOWN: {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010648 startTouchUpFilter(getCurrentCursorOffset());
10649 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
10650 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010651
Gilles Debunne21078e42011-08-02 10:22:35 -070010652 final PositionListener positionListener = getPositionListener();
10653 mLastParentX = positionListener.getPositionX();
10654 mLastParentY = positionListener.getPositionY();
Gilles Debunne874d77c2011-01-25 15:29:13 -080010655 mIsDragging = true;
10656 break;
10657 }
10658
10659 case MotionEvent.ACTION_MOVE: {
10660 final float rawX = ev.getRawX();
10661 final float rawY = ev.getRawY();
Gilles Debunneddf00b82011-02-23 17:25:13 -080010662
10663 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
10664 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
10665 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
10666 float newVerticalOffset;
10667 if (previousVerticalOffset < mIdealVerticalOffset) {
10668 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
10669 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
10670 } else {
10671 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
10672 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
10673 }
10674 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
10675
Gilles Debunne874d77c2011-01-25 15:29:13 -080010676 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -080010677 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunne874d77c2011-01-25 15:29:13 -080010678
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010679 updatePosition(newPosX, newPosY);
Gilles Debunne874d77c2011-01-25 15:29:13 -080010680 break;
10681 }
10682
10683 case MotionEvent.ACTION_UP:
Gilles Debunne874d77c2011-01-25 15:29:13 -080010684 filterOnTouchUp();
10685 mIsDragging = false;
10686 break;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -080010687
Gilles Debunne874d77c2011-01-25 15:29:13 -080010688 case MotionEvent.ACTION_CANCEL:
10689 mIsDragging = false;
10690 break;
Adam Powell879fb6b2010-09-20 11:23:56 -070010691 }
10692 return true;
10693 }
10694
10695 public boolean isDragging() {
10696 return mIsDragging;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010697 }
Gilles Debunne64e54a62010-09-07 19:07:17 -070010698
Gilles Debunne2037b822011-04-22 13:07:33 -070010699 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010700 hideActionPopupWindow();
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010701 }
Gilles Debunne69340442011-03-31 13:37:51 -070010702
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010703 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010704 hideActionPopupWindow();
Gilles Debunne81f08082011-02-17 14:07:19 -080010705 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070010706 }
10707
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010708 private class InsertionHandleView extends HandleView {
Gilles Debunne646f8562011-07-27 17:44:03 -070010709 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010710 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010711
Gilles Debunne646f8562011-07-27 17:44:03 -070010712 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010713 private float mDownPositionX, mDownPositionY;
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010714 private Runnable mHider;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080010715
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010716 public InsertionHandleView(Drawable drawable) {
10717 super(drawable, drawable);
10718 }
10719
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010720 @Override
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010721 public void show() {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010722 super.show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010723
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010724 final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
10725 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
Gilles Debunne6435a562011-08-04 21:22:30 -070010726 showActionPopupWindow(0);
Gilles Debunnee60e1e52011-01-20 12:19:44 -080010727 }
Gilles Debunne6435a562011-08-04 21:22:30 -070010728
10729 hideAfterDelay();
10730 }
10731
10732 public void showWithActionPopup() {
10733 show();
10734 showActionPopupWindow(0);
Gilles Debunne9948ad72010-11-24 14:00:46 -080010735 }
10736
Gilles Debunne646f8562011-07-27 17:44:03 -070010737 private void hideAfterDelay() {
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010738 removeHiderCallback();
10739 if (mHider == null) {
10740 mHider = new Runnable() {
10741 public void run() {
10742 hide();
10743 }
10744 };
10745 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010746 TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010747 }
10748
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010749 private void removeHiderCallback() {
10750 if (mHider != null) {
Gilles Debunne2037b822011-04-22 13:07:33 -070010751 TextView.this.removeCallbacks(mHider);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010752 }
10753 }
10754
10755 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010756 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10757 return drawable.getIntrinsicWidth() / 2;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010758 }
10759
10760 @Override
10761 public boolean onTouchEvent(MotionEvent ev) {
10762 final boolean result = super.onTouchEvent(ev);
10763
10764 switch (ev.getActionMasked()) {
10765 case MotionEvent.ACTION_DOWN:
10766 mDownPositionX = ev.getRawX();
10767 mDownPositionY = ev.getRawY();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010768 break;
10769
10770 case MotionEvent.ACTION_UP:
Gilles Debunne040023a2011-08-02 18:23:31 -070010771 if (!offsetHasBeenChanged()) {
10772 final float deltaX = mDownPositionX - ev.getRawX();
10773 final float deltaY = mDownPositionY - ev.getRawY();
10774 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10775 if (distanceSquared < mSquaredTouchSlopDistance) {
10776 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
10777 // Tapping on the handle dismisses the displayed action popup
10778 mActionPopupWindow.hide();
10779 } else {
Gilles Debunne6435a562011-08-04 21:22:30 -070010780 showWithActionPopup();
Gilles Debunne040023a2011-08-02 18:23:31 -070010781 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010782 }
10783 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010784 hideAfterDelay();
Gilles Debunne2037b822011-04-22 13:07:33 -070010785 break;
10786
10787 case MotionEvent.ACTION_CANCEL:
Gilles Debunne646f8562011-07-27 17:44:03 -070010788 hideAfterDelay();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010789 break;
10790
10791 default:
10792 break;
10793 }
10794
10795 return result;
10796 }
10797
10798 @Override
10799 public int getCurrentCursorOffset() {
10800 return TextView.this.getSelectionStart();
10801 }
10802
10803 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010804 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010805 Selection.setSelection((Spannable) mText, offset);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010806 }
10807
10808 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010809 public void updatePosition(float x, float y) {
Gilles Debunnef682a772011-08-31 15:49:10 -070010810 positionAtCursorOffset(getOffsetForPosition(x, y), false);
Gilles Debunne05336272010-07-09 20:13:45 -070010811 }
10812
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010813 @Override
Gilles Debunne2037b822011-04-22 13:07:33 -070010814 void onHandleMoved() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010815 super.onHandleMoved();
Gilles Debunne2037b822011-04-22 13:07:33 -070010816 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010817 }
10818
10819 @Override
10820 public void onDetached() {
Gilles Debunne646f8562011-07-27 17:44:03 -070010821 super.onDetached();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010822 removeHiderCallback();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010823 }
10824 }
10825
10826 private class SelectionStartHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010827
10828 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10829 super(drawableLtr, drawableRtl);
10830 }
10831
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010832 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010833 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10834 if (isRtlRun) {
10835 return drawable.getIntrinsicWidth() / 4;
10836 } else {
10837 return (drawable.getIntrinsicWidth() * 3) / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010838 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010839 }
10840
10841 @Override
10842 public int getCurrentCursorOffset() {
10843 return TextView.this.getSelectionStart();
10844 }
10845
10846 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010847 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010848 Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
Gilles Debunne186aaf92011-09-16 14:26:12 -070010849 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010850 }
10851
10852 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010853 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010854 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010855
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010856 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010857 final int selectionEnd = getSelectionEnd();
Gilles Debunne1a22db22011-11-20 22:13:21 +010010858 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010859
Gilles Debunnef682a772011-08-31 15:49:10 -070010860 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010861 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010862
10863 public ActionPopupWindow getActionPopupWindow() {
10864 return mActionPopupWindow;
10865 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010866 }
10867
10868 private class SelectionEndHandleView extends HandleView {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010869
10870 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10871 super(drawableLtr, drawableRtl);
10872 }
10873
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010874 @Override
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010875 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10876 if (isRtlRun) {
10877 return (drawable.getIntrinsicWidth() * 3) / 4;
10878 } else {
10879 return drawable.getIntrinsicWidth() / 4;
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010880 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010881 }
10882
10883 @Override
10884 public int getCurrentCursorOffset() {
10885 return TextView.this.getSelectionEnd();
10886 }
10887
10888 @Override
Gilles Debunne21078e42011-08-02 10:22:35 -070010889 public void updateSelection(int offset) {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010890 Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
Gilles Debunne186aaf92011-09-16 14:26:12 -070010891 updateDrawable();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010892 }
10893
10894 @Override
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010895 public void updatePosition(float x, float y) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010896 int offset = getOffsetForPosition(x, y);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010897
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010898 // Handles can not cross and selection is at least one character
Gilles Debunnef682a772011-08-31 15:49:10 -070010899 final int selectionStart = getSelectionStart();
Gilles Debunne1a22db22011-11-20 22:13:21 +010010900 if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length());
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010901
Gilles Debunnef682a772011-08-31 15:49:10 -070010902 positionAtCursorOffset(offset, false);
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010903 }
Gilles Debunne646f8562011-07-27 17:44:03 -070010904
10905 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
10906 mActionPopupWindow = actionPopupWindow;
10907 }
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010908 }
10909
10910 /**
10911 * A CursorController instance can be used to control a cursor in the text.
10912 * It is not used outside of {@link TextView}.
10913 * @hide
10914 */
10915 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
10916 /**
10917 * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
10918 * See also {@link #hide()}.
10919 */
10920 public void show();
10921
10922 /**
10923 * Hide the cursor controller from screen.
10924 * See also {@link #show()}.
10925 */
10926 public void hide();
10927
10928 /**
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010929 * Called when the view is detached from window. Perform house keeping task, such as
10930 * stopping Runnable thread that would otherwise keep a reference on the context, thus
10931 * preventing the activity from being recycled.
10932 */
10933 public void onDetached();
10934 }
10935
10936 private class InsertionPointCursorController implements CursorController {
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010937 private InsertionHandleView mHandle;
10938
10939 public void show() {
Gilles Debunne6435a562011-08-04 21:22:30 -070010940 getHandle().show();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010941 }
10942
Gilles Debunne6435a562011-08-04 21:22:30 -070010943 public void showWithActionPopup() {
10944 getHandle().showWithActionPopup();
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010945 }
10946
10947 public void hide() {
10948 if (mHandle != null) {
10949 mHandle.hide();
10950 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080010951 }
10952
Adam Powell624380a2010-10-02 18:12:02 -070010953 public void onTouchModeChanged(boolean isInTouchMode) {
10954 if (!isInTouchMode) {
10955 hide();
10956 }
10957 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010958
Gilles Debunne646f8562011-07-27 17:44:03 -070010959 private InsertionHandleView getHandle() {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010960 if (mSelectHandleCenter == null) {
10961 mSelectHandleCenter = mContext.getResources().getDrawable(
10962 mTextSelectHandleRes);
10963 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080010964 if (mHandle == null) {
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010965 mHandle = new InsertionHandleView(mSelectHandleCenter);
Gilles Debunnec4440f02010-11-24 14:40:48 -080010966 }
10967 return mHandle;
10968 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010969
10970 @Override
10971 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070010972 final ViewTreeObserver observer = getViewTreeObserver();
10973 observer.removeOnTouchModeChangeListener(this);
10974
Gilles Debunne6a855082011-03-11 16:51:24 -080010975 if (mHandle != null) mHandle.onDetached();
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080010976 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -070010977 }
10978
Jeff Brown01ce2e92010-09-26 22:20:12 -070010979 private class SelectionModifierCursorController implements CursorController {
Gilles Debunne6435a562011-08-04 21:22:30 -070010980 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
Gilles Debunne180bb1b2011-03-10 11:14:00 -080010981 // The cursor controller handles, lazily created when shown.
10982 private SelectionStartHandleView mStartHandle;
10983 private SelectionEndHandleView mEndHandle;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070010984 // The offsets of that last touch down event. Remembered to start selection there.
10985 private int mMinTouchOffset, mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070010986
Gilles Debunne5347c582010-10-27 14:22:35 -070010987 // Double tap detection
10988 private long mPreviousTapUpTime = 0;
Gilles Debunne3bca69b2011-05-23 18:20:22 -070010989 private float mPreviousTapPositionX, mPreviousTapPositionY;
Gilles Debunne5347c582010-10-27 14:22:35 -070010990
Gilles Debunne05336272010-07-09 20:13:45 -070010991 SelectionModifierCursorController() {
Gilles Debunne380b6042010-10-08 16:12:11 -070010992 resetTouchOffsets();
Gilles Debunne05336272010-07-09 20:13:45 -070010993 }
10994
10995 public void show() {
Adam Powell965b9692010-10-21 18:44:32 -070010996 if (isInBatchEditMode()) {
10997 return;
10998 }
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070010999 initDrawables();
11000 initHandles();
11001 hideInsertionPointCursorController();
11002 }
Adam Powell965b9692010-10-21 18:44:32 -070011003
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070011004 private void initDrawables() {
11005 if (mSelectHandleLeft == null) {
11006 mSelectHandleLeft = mContext.getResources().getDrawable(
11007 mTextSelectHandleLeftRes);
11008 }
11009 if (mSelectHandleRight == null) {
11010 mSelectHandleRight = mContext.getResources().getDrawable(
11011 mTextSelectHandleRightRes);
11012 }
11013 }
11014
11015 private void initHandles() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011016 // Lazy object creation has to be done before updatePosition() is called.
Fabrice Di Meglio34d2eba2011-08-31 19:46:15 -070011017 if (mStartHandle == null) {
11018 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
11019 }
11020 if (mEndHandle == null) {
11021 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
11022 }
Gilles Debunnec4440f02010-11-24 14:40:48 -080011023
Adam Powell879fb6b2010-09-20 11:23:56 -070011024 mStartHandle.show();
11025 mEndHandle.show();
Gilles Debunnec4440f02010-11-24 14:40:48 -080011026
Gilles Debunne646f8562011-07-27 17:44:03 -070011027 // Make sure both left and right handles share the same ActionPopupWindow (so that
11028 // moving any of the handles hides the action popup).
Gilles Debunnecb2516b2011-08-05 17:02:54 -070011029 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
Gilles Debunne646f8562011-07-27 17:44:03 -070011030 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
11031
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011032 hideInsertionPointCursorController();
Gilles Debunne05336272010-07-09 20:13:45 -070011033 }
11034
11035 public void hide() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011036 if (mStartHandle != null) mStartHandle.hide();
11037 if (mEndHandle != null) mEndHandle.hide();
Gilles Debunneaa8d73b2011-01-17 09:16:40 -080011038 }
11039
Gilles Debunnebb588da2011-07-11 18:26:19 -070011040 public void onTouchEvent(MotionEvent event) {
Gilles Debunne528c64882010-10-08 11:56:13 -070011041 // This is done even when the View does not have focus, so that long presses can start
11042 // selection and tap can move cursor from this tap position.
Gilles Debunnebb588da2011-07-11 18:26:19 -070011043 switch (event.getActionMasked()) {
11044 case MotionEvent.ACTION_DOWN:
11045 final float x = event.getX();
11046 final float y = event.getY();
Gilles Debunne05336272010-07-09 20:13:45 -070011047
Gilles Debunnebb588da2011-07-11 18:26:19 -070011048 // Remember finger down position, to be able to start selection from there
11049 mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
Gilles Debunne05336272010-07-09 20:13:45 -070011050
Gilles Debunnebb588da2011-07-11 18:26:19 -070011051 // Double tap detection
11052 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
11053 if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
11054 isPositionOnText(x, y)) {
11055 final float deltaX = x - mPreviousTapPositionX;
11056 final float deltaY = y - mPreviousTapPositionY;
11057 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
11058 if (distanceSquared < mSquaredTouchSlopDistance) {
Gilles Debunne646f8562011-07-27 17:44:03 -070011059 startSelectionActionMode();
Gilles Debunnebb588da2011-07-11 18:26:19 -070011060 mDiscardNextActionUp = true;
Gilles Debunne5347c582010-10-27 14:22:35 -070011061 }
Gilles Debunnebb588da2011-07-11 18:26:19 -070011062 }
Gilles Debunne5347c582010-10-27 14:22:35 -070011063
Gilles Debunnebb588da2011-07-11 18:26:19 -070011064 mPreviousTapPositionX = x;
11065 mPreviousTapPositionY = y;
Gilles Debunnebb588da2011-07-11 18:26:19 -070011066 break;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011067
Gilles Debunnebb588da2011-07-11 18:26:19 -070011068 case MotionEvent.ACTION_POINTER_DOWN:
11069 case MotionEvent.ACTION_POINTER_UP:
11070 // Handle multi-point gestures. Keep min and max offset positions.
11071 // Only activated for devices that correctly handle multi-touch.
11072 if (mContext.getPackageManager().hasSystemFeature(
11073 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
11074 updateMinAndMaxOffsets(event);
11075 }
11076 break;
Gilles Debunne5347c582010-10-27 14:22:35 -070011077
Gilles Debunnebb588da2011-07-11 18:26:19 -070011078 case MotionEvent.ACTION_UP:
11079 mPreviousTapUpTime = SystemClock.uptimeMillis();
11080 break;
Gilles Debunne05336272010-07-09 20:13:45 -070011081 }
11082 }
11083
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011084 /**
11085 * @param event
11086 */
11087 private void updateMinAndMaxOffsets(MotionEvent event) {
11088 int pointerCount = event.getPointerCount();
11089 for (int index = 0; index < pointerCount; index++) {
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011090 int offset = getOffsetForPosition(event.getX(index), event.getY(index));
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011091 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
11092 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
11093 }
11094 }
11095
11096 public int getMinTouchOffset() {
11097 return mMinTouchOffset;
11098 }
11099
11100 public int getMaxTouchOffset() {
11101 return mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -070011102 }
11103
Gilles Debunne380b6042010-10-08 16:12:11 -070011104 public void resetTouchOffsets() {
11105 mMinTouchOffset = mMaxTouchOffset = -1;
11106 }
11107
Gilles Debunne05336272010-07-09 20:13:45 -070011108 /**
11109 * @return true iff this controller is currently used to move the selection start.
11110 */
11111 public boolean isSelectionStartDragged() {
Gilles Debunnec4440f02010-11-24 14:40:48 -080011112 return mStartHandle != null && mStartHandle.isDragging();
Gilles Debunne05336272010-07-09 20:13:45 -070011113 }
Adam Powell624380a2010-10-02 18:12:02 -070011114
11115 public void onTouchModeChanged(boolean isInTouchMode) {
11116 if (!isInTouchMode) {
11117 hide();
11118 }
11119 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -080011120
11121 @Override
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011122 public void onDetached() {
Gilles Debunne6f72cf82011-03-24 16:35:28 -070011123 final ViewTreeObserver observer = getViewTreeObserver();
11124 observer.removeOnTouchModeChangeListener(this);
11125
Gilles Debunne180bb1b2011-03-10 11:14:00 -080011126 if (mStartHandle != null) mStartHandle.onDetached();
11127 if (mEndHandle != null) mEndHandle.onDetached();
11128 }
Gilles Debunne05336272010-07-09 20:13:45 -070011129 }
11130
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011131 private void hideInsertionPointCursorController() {
Gilles Debunnee587d832010-11-23 20:20:11 -080011132 // No need to create the controller to hide it.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011133 if (mInsertionPointCursorController != null) {
11134 mInsertionPointCursorController.hide();
11135 }
11136 }
11137
Gilles Debunned94f8c52011-01-10 11:29:15 -080011138 /**
11139 * Hides the insertion controller and stops text selection mode, hiding the selection controller
11140 */
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011141 private void hideControllers() {
Luca Zanolin1564fc72011-09-07 00:01:28 +010011142 hideCursorControllers();
11143 hideSpanControllers();
11144 }
Luca Zanoline6d36822011-08-30 18:04:34 +010011145
Luca Zanolin1564fc72011-09-07 00:01:28 +010011146 private void hideSpanControllers() {
Luca Zanoline6d36822011-08-30 18:04:34 +010011147 if (mChangeWatcher != null) {
11148 mChangeWatcher.hideControllers();
11149 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -070011150 }
11151
Luca Zanolin1564fc72011-09-07 00:01:28 +010011152 private void hideCursorControllers() {
Gilles Debunne26c8b3a2011-10-12 14:06:58 -070011153 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
11154 // Should be done before hide insertion point controller since it triggers a show of it
11155 mSuggestionsPopupWindow.hide();
11156 }
Luca Zanolin1564fc72011-09-07 00:01:28 +010011157 hideInsertionPointCursorController();
11158 stopSelectionActionMode();
11159 }
11160
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011161 /**
Gilles Debunne9511b412011-05-23 18:33:48 -070011162 * Get the character offset closest to the specified absolute position. A typical use case is to
11163 * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011164 *
11165 * @param x The horizontal absolute position of a point on screen
11166 * @param y The vertical absolute position of a point on screen
11167 * @return the character offset for the character whose position is closest to the specified
11168 * position. Returns -1 if there is no layout.
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011169 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011170 public int getOffsetForPosition(float x, float y) {
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011171 if (getLayout() == null) return -1;
Gilles Debunne9948ad72010-11-24 14:00:46 -080011172 final int line = getLineAtCoordinate(y);
11173 final int offset = getOffsetAtCoordinate(line, x);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -070011174 return offset;
11175 }
11176
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011177 private float convertToLocalHorizontalCoordinate(float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011178 x -= getTotalPaddingLeft();
11179 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011180 x = Math.max(0.0f, x);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011181 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11182 x += getScrollX();
11183 return x;
11184 }
11185
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011186 private int getLineAtCoordinate(float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011187 y -= getTotalPaddingTop();
11188 // Clamp the position to inside of the view.
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011189 y = Math.max(0.0f, y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011190 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11191 y += getScrollY();
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011192 return getLayout().getLineForVertical((int) y);
Gilles Debunne9948ad72010-11-24 14:00:46 -080011193 }
11194
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011195 private int getOffsetAtCoordinate(int line, float x) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011196 x = convertToLocalHorizontalCoordinate(x);
11197 return getLayout().getOffsetForHorizontal(line, x);
11198 }
11199
11200 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
11201 * in the view. Returns false when the position is in the empty space of left/right of text.
11202 */
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011203 private boolean isPositionOnText(float x, float y) {
Gilles Debunne9948ad72010-11-24 14:00:46 -080011204 if (getLayout() == null) return false;
11205
11206 final int line = getLineAtCoordinate(y);
11207 x = convertToLocalHorizontalCoordinate(x);
11208
11209 if (x < getLayout().getLineLeft(line)) return false;
11210 if (x > getLayout().getLineRight(line)) return false;
11211 return true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011212 }
11213
Gilles Debunnef170a342010-11-11 11:08:59 -080011214 @Override
11215 public boolean onDragEvent(DragEvent event) {
11216 switch (event.getAction()) {
11217 case DragEvent.ACTION_DRAG_STARTED:
Gilles Debunnee587d832010-11-23 20:20:11 -080011218 return hasInsertionController();
Gilles Debunnef170a342010-11-11 11:08:59 -080011219
11220 case DragEvent.ACTION_DRAG_ENTERED:
11221 TextView.this.requestFocus();
11222 return true;
11223
Gilles Debunne2226a192010-11-30 18:50:51 -080011224 case DragEvent.ACTION_DRAG_LOCATION:
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011225 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunnef170a342010-11-11 11:08:59 -080011226 Selection.setSelection((Spannable)mText, offset);
11227 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011228
Gilles Debunne2226a192010-11-30 18:50:51 -080011229 case DragEvent.ACTION_DROP:
11230 onDrop(event);
Gilles Debunnef170a342010-11-11 11:08:59 -080011231 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -080011232
Gilles Debunnef170a342010-11-11 11:08:59 -080011233 case DragEvent.ACTION_DRAG_ENDED:
Gilles Debunne4ae0f292010-11-29 14:56:39 -080011234 case DragEvent.ACTION_DRAG_EXITED:
Gilles Debunnef170a342010-11-11 11:08:59 -080011235 default:
11236 return true;
11237 }
11238 }
11239
Gilles Debunne2226a192010-11-30 18:50:51 -080011240 private void onDrop(DragEvent event) {
11241 StringBuilder content = new StringBuilder("");
11242 ClipData clipData = event.getClipData();
11243 final int itemCount = clipData.getItemCount();
11244 for (int i=0; i < itemCount; i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -080011245 Item item = clipData.getItemAt(i);
Gilles Debunne2226a192010-11-30 18:50:51 -080011246 content.append(item.coerceToText(TextView.this.mContext));
11247 }
11248
Gilles Debunne3bca69b2011-05-23 18:20:22 -070011249 final int offset = getOffsetForPosition(event.getX(), event.getY());
Gilles Debunne2226a192010-11-30 18:50:51 -080011250
Gilles Debunneaaa84792010-12-03 11:10:14 -080011251 Object localState = event.getLocalState();
11252 DragLocalState dragLocalState = null;
11253 if (localState instanceof DragLocalState) {
11254 dragLocalState = (DragLocalState) localState;
11255 }
11256 boolean dragDropIntoItself = dragLocalState != null &&
11257 dragLocalState.sourceTextView == this;
11258
11259 if (dragDropIntoItself) {
11260 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
Gilles Debunne2226a192010-11-30 18:50:51 -080011261 // A drop inside the original selection discards the drop.
11262 return;
11263 }
11264 }
11265
11266 final int originalLength = mText.length();
11267 long minMax = prepareSpacesAroundPaste(offset, offset, content);
11268 int min = extractRangeStartFromLong(minMax);
11269 int max = extractRangeEndFromLong(minMax);
11270
11271 Selection.setSelection((Spannable) mText, max);
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011272 replaceText_internal(min, max, content);
Gilles Debunne2226a192010-11-30 18:50:51 -080011273
Gilles Debunneaaa84792010-12-03 11:10:14 -080011274 if (dragDropIntoItself) {
11275 int dragSourceStart = dragLocalState.start;
11276 int dragSourceEnd = dragLocalState.end;
Gilles Debunne2226a192010-11-30 18:50:51 -080011277 if (max <= dragSourceStart) {
11278 // Inserting text before selection has shifted positions
11279 final int shift = mText.length() - originalLength;
11280 dragSourceStart += shift;
11281 dragSourceEnd += shift;
11282 }
11283
11284 // Delete original selection
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011285 deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunne2226a192010-11-30 18:50:51 -080011286
11287 // Make sure we do not leave two adjacent spaces.
11288 if ((dragSourceStart == 0 ||
11289 Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
11290 (dragSourceStart == mText.length() ||
11291 Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
11292 final int pos = dragSourceStart == mText.length() ?
11293 dragSourceStart - 1 : dragSourceStart;
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011294 deleteText_internal(pos, pos + 1);
Gilles Debunne2226a192010-11-30 18:50:51 -080011295 }
11296 }
11297 }
11298
Adam Powell965b9692010-10-21 18:44:32 -070011299 /**
11300 * @return True if this view supports insertion handles.
11301 */
11302 boolean hasInsertionController() {
11303 return mInsertionControllerEnabled;
11304 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011305
Adam Powell965b9692010-10-21 18:44:32 -070011306 /**
11307 * @return True if this view supports selection handles.
11308 */
11309 boolean hasSelectionController() {
11310 return mSelectionControllerEnabled;
11311 }
11312
Gilles Debunne9948ad72010-11-24 14:00:46 -080011313 InsertionPointCursorController getInsertionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011314 if (!mInsertionControllerEnabled) {
11315 return null;
11316 }
11317
11318 if (mInsertionPointCursorController == null) {
11319 mInsertionPointCursorController = new InsertionPointCursorController();
11320
11321 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011322 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011323 }
11324
11325 return mInsertionPointCursorController;
11326 }
11327
Gilles Debunnee587d832010-11-23 20:20:11 -080011328 SelectionModifierCursorController getSelectionController() {
Adam Powell965b9692010-10-21 18:44:32 -070011329 if (!mSelectionControllerEnabled) {
11330 return null;
11331 }
11332
11333 if (mSelectionModifierCursorController == null) {
11334 mSelectionModifierCursorController = new SelectionModifierCursorController();
11335
11336 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -080011337 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell965b9692010-10-21 18:44:32 -070011338 }
11339
11340 return mSelectionModifierCursorController;
11341 }
11342
11343 boolean isInBatchEditMode() {
11344 final InputMethodState ims = mInputMethodState;
11345 if (ims != null) {
11346 return ims.mBatchEditNesting > 0;
11347 }
11348 return mInBatchEditControllers;
11349 }
11350
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011351 @Override
11352 protected void resolveTextDirection() {
Fabrice Di Meglio7810b5f2011-08-24 18:26:14 -070011353 if (hasPasswordTransformationMethod()) {
11354 mTextDir = TextDirectionHeuristics.LOCALE;
11355 return;
11356 }
11357
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011358 // Always need to resolve layout direction first
11359 final boolean defaultIsRtl = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL);
11360
11361 // Then resolve text direction on the parent
Doug Feltcb3791202011-07-07 11:57:48 -070011362 super.resolveTextDirection();
11363
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011364 // Now, we can select the heuristic
Doug Feltcb3791202011-07-07 11:57:48 -070011365 int textDir = getResolvedTextDirection();
11366 switch (textDir) {
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011367 default:
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011368 case TEXT_DIRECTION_FIRST_STRONG:
Fabrice Di Meglio4b60c302011-08-17 16:56:55 -070011369 mTextDir = (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11370 TextDirectionHeuristics.FIRSTSTRONG_LTR);
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011371 break;
11372 case TEXT_DIRECTION_ANY_RTL:
Fabrice Di Meglioa6461452011-08-19 15:42:04 -070011373 mTextDir = TextDirectionHeuristics.ANYRTL_LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011374 break;
11375 case TEXT_DIRECTION_LTR:
Doug Feltcb3791202011-07-07 11:57:48 -070011376 mTextDir = TextDirectionHeuristics.LTR;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011377 break;
11378 case TEXT_DIRECTION_RTL:
Doug Feltcb3791202011-07-07 11:57:48 -070011379 mTextDir = TextDirectionHeuristics.RTL;
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011380 break;
11381 }
Fabrice Di Meglio22268862011-06-27 18:13:18 -070011382 }
11383
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011384 /**
11385 * Subclasses will need to override this method to implement their own way of resolving
11386 * drawables depending on the layout direction.
11387 *
11388 * A call to the super method will be required from the subclasses implementation.
11389 *
11390 */
11391 protected void resolveDrawables() {
11392 // No need to resolve twice
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011393 if (mResolvedDrawables) {
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011394 return;
11395 }
11396 // No drawable to resolve
11397 if (mDrawables == null) {
11398 return;
11399 }
11400 // No relative drawable to resolve
11401 if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011402 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011403 return;
11404 }
11405
11406 Drawables dr = mDrawables;
11407 switch(getResolvedLayoutDirection()) {
11408 case LAYOUT_DIRECTION_RTL:
11409 if (dr.mDrawableStart != null) {
11410 dr.mDrawableRight = dr.mDrawableStart;
11411
11412 dr.mDrawableSizeRight = dr.mDrawableSizeStart;
11413 dr.mDrawableHeightRight = dr.mDrawableHeightStart;
11414 }
11415 if (dr.mDrawableEnd != null) {
11416 dr.mDrawableLeft = dr.mDrawableEnd;
11417
11418 dr.mDrawableSizeLeft = dr.mDrawableSizeEnd;
11419 dr.mDrawableHeightLeft = dr.mDrawableHeightEnd;
11420 }
11421 break;
11422
11423 case LAYOUT_DIRECTION_LTR:
11424 default:
11425 if (dr.mDrawableStart != null) {
11426 dr.mDrawableLeft = dr.mDrawableStart;
11427
11428 dr.mDrawableSizeLeft = dr.mDrawableSizeStart;
11429 dr.mDrawableHeightLeft = dr.mDrawableHeightStart;
11430 }
11431 if (dr.mDrawableEnd != null) {
11432 dr.mDrawableRight = dr.mDrawableEnd;
11433
11434 dr.mDrawableSizeRight = dr.mDrawableSizeEnd;
11435 dr.mDrawableHeightRight = dr.mDrawableHeightEnd;
11436 }
11437 break;
11438 }
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011439 mResolvedDrawables = true;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011440 }
11441
11442 protected void resetResolvedDrawables() {
Fabrice Di Megliof66fdad2011-07-19 13:08:47 -070011443 mResolvedDrawables = false;
Fabrice Di Meglioa3b6b952011-06-29 16:44:43 -070011444 }
11445
satoka67a3cf2011-09-07 17:14:03 +090011446 /**
11447 * @hide
11448 */
11449 protected void viewClicked(InputMethodManager imm) {
11450 if (imm != null) {
11451 imm.viewClicked(this);
11452 }
11453 }
11454
Gilles Debunne39ba6d92011-11-09 05:26:26 +010011455 /**
11456 * Deletes the range of text [start, end[.
11457 * @hide
11458 */
11459 protected void deleteText_internal(int start, int end) {
11460 ((Editable) mText).delete(start, end);
11461 }
11462
11463 /**
11464 * Replaces the range of text [start, end[ by replacement text
11465 * @hide
11466 */
11467 protected void replaceText_internal(int start, int end, CharSequence text) {
11468 ((Editable) mText).replace(start, end, text);
11469 }
11470
Gilles Debunnee300be92011-12-06 10:15:56 -080011471 /**
11472 * Sets a span on the specified range of text
11473 * @hide
11474 */
11475 protected void setSpan_internal(Object span, int start, int end, int flags) {
11476 ((Editable) mText).setSpan(span, start, end, flags);
11477 }
11478
11479 /**
11480 * Moves the cursor to the specified offset position in text
11481 * @hide
11482 */
11483 protected void setCursorPosition_internal(int start, int end) {
11484 Selection.setSelection(((Editable) mText), start, end);
11485 }
11486
Romain Guy6d956622010-11-29 11:38:05 -080011487 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011488 private CharSequence mText;
11489 private CharSequence mTransformed;
11490 private BufferType mBufferType = BufferType.NORMAL;
11491
11492 private int mInputType = EditorInfo.TYPE_NULL;
11493 private CharSequence mHint;
11494 private Layout mHintLayout;
11495
11496 private KeyListener mInput;
11497
11498 private MovementMethod mMovement;
11499 private TransformationMethod mTransformation;
Adam Powell7f8f79a2011-07-07 18:35:54 -070011500 private boolean mAllowTransformationLengthChange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011501 private ChangeWatcher mChangeWatcher;
11502
11503 private ArrayList<TextWatcher> mListeners = null;
11504
11505 // display attributes
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011506 private final TextPaint mTextPaint;
Romain Guy939151f2009-04-08 14:22:40 -070011507 private boolean mUserSetTextScaleX;
Gilles Debunneb6ca7232010-06-24 11:45:21 -070011508 private final Paint mHighlightPaint;
Gilles Debunne6fbe5ca2011-09-06 15:24:45 -070011509 private int mHighlightColor = 0x6633B5E5;
Gilles Debunnecdfab192011-11-28 18:05:03 -080011510 private Layout mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011511
11512 private long mShowCursor;
11513 private Blink mBlink;
11514 private boolean mCursorVisible = true;
11515
Adam Powell965b9692010-10-21 18:44:32 -070011516 // Cursor Controllers.
Gilles Debunne9948ad72010-11-24 14:00:46 -080011517 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunnee587d832010-11-23 20:20:11 -080011518 private SelectionModifierCursorController mSelectionModifierCursorController;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070011519 private ActionMode mSelectionActionMode;
Adam Powell965b9692010-10-21 18:44:32 -070011520 private boolean mInsertionControllerEnabled;
11521 private boolean mSelectionControllerEnabled;
11522 private boolean mInBatchEditControllers;
11523
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011524 private boolean mSelectAllOnFocus = false;
11525
Fabrice Di Meglio9e3b0022011-06-06 16:30:29 -070011526 private int mGravity = Gravity.TOP | Gravity.START;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011527 private boolean mHorizontallyScrolling;
11528
11529 private int mAutoLinkMask;
11530 private boolean mLinksClickable = true;
11531
Gilles Debunne6435a562011-08-04 21:22:30 -070011532 private float mSpacingMult = 1.0f;
11533 private float mSpacingAdd = 0.0f;
Gilles Debunne86b9c782010-11-11 10:43:48 -080011534 private boolean mTextIsSelectable = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011535
11536 private static final int LINES = 1;
11537 private static final int EMS = LINES;
11538 private static final int PIXELS = 2;
11539
11540 private int mMaximum = Integer.MAX_VALUE;
11541 private int mMaxMode = LINES;
11542 private int mMinimum = 0;
11543 private int mMinMode = LINES;
11544
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -070011545 private int mOldMaximum = mMaximum;
11546 private int mOldMaxMode = mMaxMode;
11547
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011548 private int mMaxWidth = Integer.MAX_VALUE;
11549 private int mMaxWidthMode = PIXELS;
11550 private int mMinWidth = 0;
11551 private int mMinWidthMode = PIXELS;
11552
11553 private boolean mSingleLine;
11554 private int mDesiredHeightAtMeasure = -1;
11555 private boolean mIncludePad = true;
11556
11557 // tmp primitives, so we don't alloc them on each draw
11558 private Path mHighlightPath;
11559 private boolean mHighlightPathBogus = true;
11560 private static final RectF sTempRect = new RectF();
Gilles Debunne64901d42011-11-25 10:23:38 +010011561 private static final float[] sTmpPosition = new float[2];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011562
Jeff Brown033a0012011-11-11 15:30:16 -080011563 // XXX should be much larger
11564 private static final int VERY_WIDE = 1024*1024;
11565
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011566 private static final int BLINK = 500;
11567
11568 private static final int ANIMATED_SCROLL_GAP = 250;
11569 private long mLastScroll;
11570 private Scroller mScroller = null;
11571
11572 private BoringLayout.Metrics mBoring;
11573 private BoringLayout.Metrics mHintBoring;
11574
11575 private BoringLayout mSavedLayout, mSavedHintLayout;
11576
Doug Feltcb3791202011-07-07 11:57:48 -070011577 private TextDirectionHeuristic mTextDir = null;
11578
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011579 private static final InputFilter[] NO_FILTERS = new InputFilter[0];
11580 private InputFilter[] mFilters = NO_FILTERS;
11581 private static final Spanned EMPTY_SPANNED = new SpannedString("");
Christopher Tate36d4c3f2011-01-07 13:34:24 -080011582 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Gilles Debunne0a2aa402010-11-24 17:57:46 -080011583 // System wide time for last cut or copy action.
11584 private static long sLastCutOrCopyTime;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080011585 // Used to highlight a word when it is corrected by the IME
11586 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunneda0a3f02010-12-22 10:07:15 -080011587 // New state used to change background based on whether this TextView is multiline.
11588 private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080011589}