blob: 13b9285f6fd537bb2b11304c5fe374fe2ae1db7b [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
svetoslavganov75986cf2009-05-14 22:28:01 -070019import com.android.internal.util.FastMath;
20import com.android.internal.widget.EditableInputConnection;
21
Gilles Debunne27113f82010-08-23 12:09:14 -070022import org.xmlpull.v1.XmlPullParserException;
23
Gilles Debunne78996c92010-10-12 16:01:47 -070024import android.R;
Dianne Hackborn1040dc42010-08-26 22:11:06 -070025import android.content.ClipData;
Gilles Debunnef170a342010-11-11 11:08:59 -080026import android.content.ClipData.Item;
Gilles Debunne64e54a62010-09-07 19:07:17 -070027import android.content.ClipboardManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.content.Context;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070029import android.content.pm.PackageManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.content.res.ColorStateList;
31import android.content.res.Resources;
32import android.content.res.TypedArray;
33import android.content.res.XmlResourceParser;
34import android.graphics.Canvas;
Gilles Debunne12d91ce2010-12-10 11:36:29 -080035import android.graphics.Color;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import android.graphics.Paint;
37import android.graphics.Path;
38import android.graphics.Rect;
39import android.graphics.RectF;
40import android.graphics.Typeface;
41import android.graphics.drawable.Drawable;
Gilles Debunne64e54a62010-09-07 19:07:17 -070042import android.inputmethodservice.ExtractEditText;
Dianne Hackborn23fdaf62010-08-06 12:16:55 -070043import android.net.Uri;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044import android.os.Bundle;
45import android.os.Handler;
svetoslavganov75986cf2009-05-14 22:28:01 -070046import android.os.Message;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.os.Parcel;
48import android.os.Parcelable;
49import android.os.SystemClock;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080050import android.text.BoringLayout;
51import android.text.DynamicLayout;
52import android.text.Editable;
53import android.text.GetChars;
54import android.text.GraphicsOperations;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080055import android.text.InputFilter;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -070056import android.text.InputType;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057import android.text.Layout;
58import android.text.ParcelableSpan;
59import android.text.Selection;
60import android.text.SpanWatcher;
61import android.text.Spannable;
svetoslavganov75986cf2009-05-14 22:28:01 -070062import android.text.SpannableString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063import android.text.Spanned;
64import android.text.SpannedString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065import android.text.StaticLayout;
66import android.text.TextPaint;
67import android.text.TextUtils;
68import android.text.TextWatcher;
Gilles Debunne86b9c782010-11-11 10:43:48 -080069import android.text.method.ArrowKeyMovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070import android.text.method.DateKeyListener;
71import android.text.method.DateTimeKeyListener;
72import android.text.method.DialerKeyListener;
73import android.text.method.DigitsKeyListener;
74import android.text.method.KeyListener;
75import android.text.method.LinkMovementMethod;
76import android.text.method.MetaKeyKeyListener;
77import android.text.method.MovementMethod;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078import android.text.method.PasswordTransformationMethod;
79import android.text.method.SingleLineTransformationMethod;
80import android.text.method.TextKeyListener;
svetoslavganov75986cf2009-05-14 22:28:01 -070081import android.text.method.TimeKeyListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080082import android.text.method.TransformationMethod;
Gilles Debunnef3895ed2010-12-21 12:53:58 -080083import android.text.style.ClickableSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080084import android.text.style.ParagraphStyle;
85import android.text.style.URLSpan;
86import android.text.style.UpdateAppearance;
87import android.text.util.Linkify;
88import android.util.AttributeSet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080089import android.util.FloatMath;
svetoslavganov75986cf2009-05-14 22:28:01 -070090import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080091import android.util.TypedValue;
Gilles Debunne27113f82010-08-23 12:09:14 -070092import android.view.ActionMode;
Gilles Debunnef4dceb12010-12-01 15:54:20 -080093import android.view.ActionMode.Callback;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080094import android.view.ContextMenu;
Gilles Debunnef170a342010-11-11 11:08:59 -080095import android.view.DragEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080096import android.view.Gravity;
Gilles Debunnef788a9f2010-07-22 10:17:23 -070097import android.view.HapticFeedbackConstants;
Jeff Brown6b53e8d2010-11-10 16:03:06 -080098import android.view.KeyCharacterMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080099import android.view.KeyEvent;
100import android.view.LayoutInflater;
Gilles Debunnef788a9f2010-07-22 10:17:23 -0700101import android.view.Menu;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102import android.view.MenuItem;
103import android.view.MotionEvent;
104import android.view.View;
Gilles Debunne65f60412010-10-15 16:18:35 -0700105import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106import android.view.ViewDebug;
Adam Powell8c8293b2010-10-12 14:45:12 -0700107import android.view.ViewGroup;
Gilles Debunne27113f82010-08-23 12:09:14 -0700108import android.view.ViewGroup.LayoutParams;
Adam Powellabcbb1a2010-10-04 21:12:19 -0700109import android.view.ViewParent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110import android.view.ViewRoot;
111import android.view.ViewTreeObserver;
Gilles Debunnecbfbb522010-10-07 16:57:31 -0700112import android.view.WindowManager;
svetoslavganov75986cf2009-05-14 22:28:01 -0700113import android.view.accessibility.AccessibilityEvent;
114import android.view.accessibility.AccessibilityManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115import android.view.animation.AnimationUtils;
116import android.view.inputmethod.BaseInputConnection;
117import android.view.inputmethod.CompletionInfo;
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -0800118import android.view.inputmethod.CorrectionInfo;
svetoslavganov75986cf2009-05-14 22:28:01 -0700119import android.view.inputmethod.EditorInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800120import android.view.inputmethod.ExtractedText;
121import android.view.inputmethod.ExtractedTextRequest;
122import android.view.inputmethod.InputConnection;
123import android.view.inputmethod.InputMethodManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800124import android.widget.RemoteViews.RemoteView;
125
Gilles Debunne27113f82010-08-23 12:09:14 -0700126import java.io.IOException;
127import java.lang.ref.WeakReference;
128import java.util.ArrayList;
129
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130/**
131 * Displays text to the user and optionally allows them to edit it. A TextView
132 * is a complete text editor, however the basic class is configured to not
133 * allow editing; see {@link EditText} for a subclass that configures the text
134 * view for editing.
135 *
136 * <p>
137 * <b>XML attributes</b>
138 * <p>
139 * See {@link android.R.styleable#TextView TextView Attributes},
140 * {@link android.R.styleable#View View Attributes}
141 *
142 * @attr ref android.R.styleable#TextView_text
143 * @attr ref android.R.styleable#TextView_bufferType
144 * @attr ref android.R.styleable#TextView_hint
145 * @attr ref android.R.styleable#TextView_textColor
146 * @attr ref android.R.styleable#TextView_textColorHighlight
147 * @attr ref android.R.styleable#TextView_textColorHint
Romain Guyd6a463a2009-05-21 23:10:10 -0700148 * @attr ref android.R.styleable#TextView_textAppearance
149 * @attr ref android.R.styleable#TextView_textColorLink
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800150 * @attr ref android.R.styleable#TextView_textSize
151 * @attr ref android.R.styleable#TextView_textScaleX
152 * @attr ref android.R.styleable#TextView_typeface
153 * @attr ref android.R.styleable#TextView_textStyle
154 * @attr ref android.R.styleable#TextView_cursorVisible
155 * @attr ref android.R.styleable#TextView_maxLines
156 * @attr ref android.R.styleable#TextView_maxHeight
157 * @attr ref android.R.styleable#TextView_lines
158 * @attr ref android.R.styleable#TextView_height
159 * @attr ref android.R.styleable#TextView_minLines
160 * @attr ref android.R.styleable#TextView_minHeight
161 * @attr ref android.R.styleable#TextView_maxEms
162 * @attr ref android.R.styleable#TextView_maxWidth
163 * @attr ref android.R.styleable#TextView_ems
164 * @attr ref android.R.styleable#TextView_width
165 * @attr ref android.R.styleable#TextView_minEms
166 * @attr ref android.R.styleable#TextView_minWidth
167 * @attr ref android.R.styleable#TextView_gravity
168 * @attr ref android.R.styleable#TextView_scrollHorizontally
169 * @attr ref android.R.styleable#TextView_password
170 * @attr ref android.R.styleable#TextView_singleLine
171 * @attr ref android.R.styleable#TextView_selectAllOnFocus
172 * @attr ref android.R.styleable#TextView_includeFontPadding
173 * @attr ref android.R.styleable#TextView_maxLength
174 * @attr ref android.R.styleable#TextView_shadowColor
175 * @attr ref android.R.styleable#TextView_shadowDx
176 * @attr ref android.R.styleable#TextView_shadowDy
177 * @attr ref android.R.styleable#TextView_shadowRadius
178 * @attr ref android.R.styleable#TextView_autoLink
179 * @attr ref android.R.styleable#TextView_linksClickable
180 * @attr ref android.R.styleable#TextView_numeric
181 * @attr ref android.R.styleable#TextView_digits
182 * @attr ref android.R.styleable#TextView_phoneNumber
183 * @attr ref android.R.styleable#TextView_inputMethod
184 * @attr ref android.R.styleable#TextView_capitalize
185 * @attr ref android.R.styleable#TextView_autoText
186 * @attr ref android.R.styleable#TextView_editable
Romain Guyd6a463a2009-05-21 23:10:10 -0700187 * @attr ref android.R.styleable#TextView_freezesText
188 * @attr ref android.R.styleable#TextView_ellipsize
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189 * @attr ref android.R.styleable#TextView_drawableTop
190 * @attr ref android.R.styleable#TextView_drawableBottom
191 * @attr ref android.R.styleable#TextView_drawableRight
192 * @attr ref android.R.styleable#TextView_drawableLeft
Romain Guyd6a463a2009-05-21 23:10:10 -0700193 * @attr ref android.R.styleable#TextView_drawablePadding
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800194 * @attr ref android.R.styleable#TextView_lineSpacingExtra
195 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
196 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
Romain Guyd6a463a2009-05-21 23:10:10 -0700197 * @attr ref android.R.styleable#TextView_inputType
198 * @attr ref android.R.styleable#TextView_imeOptions
199 * @attr ref android.R.styleable#TextView_privateImeOptions
200 * @attr ref android.R.styleable#TextView_imeActionLabel
201 * @attr ref android.R.styleable#TextView_imeActionId
202 * @attr ref android.R.styleable#TextView_editorExtras
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 */
204@RemoteView
205public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700206 static final String LOG_TAG = "TextView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 static final boolean DEBUG_EXTRACT = false;
Gilles Debunneb7012e842011-02-24 15:40:38 -0800208
Romain Guy8b55f372010-08-18 17:10:07 -0700209 private static final int PRIORITY = 100;
Gilles Debunne3dbf55c2010-12-16 10:31:51 -0800210 private int mCurrentAlpha = 255;
Adam Powell879fb6b2010-09-20 11:23:56 -0700211
Adam Powellabcbb1a2010-10-04 21:12:19 -0700212 final int[] mTempCoords = new int[2];
213 Rect mTempRect;
Adam Powell879fb6b2010-09-20 11:23:56 -0700214
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 private ColorStateList mTextColor;
216 private int mCurTextColor;
217 private ColorStateList mHintTextColor;
218 private ColorStateList mLinkTextColor;
219 private int mCurHintTextColor;
220 private boolean mFreezesText;
221 private boolean mFrozenWithFocus;
222 private boolean mTemporaryDetach;
Romain Guya440b002010-02-24 15:57:54 -0800223 private boolean mDispatchTemporaryDetach;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800224
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800225 private boolean mDiscardNextActionUp = false;
226 private boolean mIgnoreActionUpEvent = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227
228 private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
229 private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
230
231 private float mShadowRadius, mShadowDx, mShadowDy;
232
233 private static final int PREDRAW_NOT_REGISTERED = 0;
234 private static final int PREDRAW_PENDING = 1;
235 private static final int PREDRAW_DONE = 2;
236 private int mPreDrawState = PREDRAW_NOT_REGISTERED;
237
238 private TextUtils.TruncateAt mEllipsize = null;
239
240 // Enum for the "typeface" XML parameter.
241 // TODO: How can we get this from the XML instead of hardcoding it here?
242 private static final int SANS = 1;
243 private static final int SERIF = 2;
244 private static final int MONOSPACE = 3;
245
246 // Bitfield for the "numeric" XML parameter.
247 // TODO: How can we get this from the XML instead of hardcoding it here?
248 private static final int SIGNED = 2;
249 private static final int DECIMAL = 4;
250
251 class Drawables {
252 final Rect mCompoundRect = new Rect();
253 Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight;
254 int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight;
255 int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight;
256 int mDrawablePadding;
257 }
258 private Drawables mDrawables;
259
260 private CharSequence mError;
261 private boolean mErrorWasChanged;
The Android Open Source Project10592532009-03-18 17:39:46 -0700262 private ErrorPopup mPopup;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 /**
264 * This flag is set if the TextView tries to display an error before it
265 * is attached to the window (so its position is still unknown).
266 * It causes the error to be shown later, when onAttachedToWindow()
267 * is called.
268 */
269 private boolean mShowErrorAfterAttach;
270
271 private CharWrapper mCharWrapper = null;
272
273 private boolean mSelectionMoved = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700274 private boolean mTouchFocusSelected = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275
276 private Marquee mMarquee;
277 private boolean mRestartMarquee;
278
279 private int mMarqueeRepeatLimit = 3;
280
281 class InputContentType {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700282 int imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800283 String privateImeOptions;
284 CharSequence imeActionLabel;
285 int imeActionId;
286 Bundle extras;
287 OnEditorActionListener onEditorActionListener;
288 boolean enterDown;
289 }
290 InputContentType mInputContentType;
291
292 class InputMethodState {
293 Rect mCursorRectInWindow = new Rect();
294 RectF mTmpRectF = new RectF();
295 float[] mTmpOffset = new float[2];
296 ExtractedTextRequest mExtracting;
297 final ExtractedText mTmpExtracted = new ExtractedText();
298 int mBatchEditNesting;
299 boolean mCursorChanged;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700300 boolean mSelectionModeChanged;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800301 boolean mContentChanged;
302 int mChangedStart, mChangedEnd, mChangedDelta;
303 }
304 InputMethodState mInputMethodState;
305
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800306 private int mTextSelectHandleLeftRes;
307 private int mTextSelectHandleRightRes;
308 private int mTextSelectHandleRes;
309 private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout;
310 private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout;
Adam Powellfbb3b472010-10-06 21:04:35 -0700311
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800312 private int mCursorDrawableRes;
313 private final Drawable[] mCursorDrawable = new Drawable[2];
314 private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
315
316 private Drawable mSelectHandleLeft;
317 private Drawable mSelectHandleRight;
318 private Drawable mSelectHandleCenter;
Adam Powellb08013c2010-09-16 16:28:11 -0700319
Gilles Debunne9948ad72010-11-24 14:00:46 -0800320 private int mLastDownPositionX, mLastDownPositionY;
Gilles Debunnef4dceb12010-12-01 15:54:20 -0800321 private Callback mCustomSelectionActionModeCallback;
Gilles Debunne9948ad72010-11-24 14:00:46 -0800322
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800323 private final int mSquaredTouchSlopDistance;
Gilles Debunnec01f3fe2010-12-22 17:07:36 -0800324 // Set when this TextView gained focus with some text selected. Will start selection mode.
325 private boolean mCreatedWithASelection = false;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -0800326
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 /*
328 * Kick-start the font cache for the zygote process (to pay the cost of
329 * initializing freetype for our default font only once).
330 */
331 static {
332 Paint p = new Paint();
333 p.setAntiAlias(true);
334 // We don't care about the result, just the side-effect of measuring.
335 p.measureText("H");
336 }
337
338 /**
339 * Interface definition for a callback to be invoked when an action is
340 * performed on the editor.
341 */
342 public interface OnEditorActionListener {
343 /**
344 * Called when an action is being performed.
345 *
346 * @param v The view that was clicked.
347 * @param actionId Identifier of the action. This will be either the
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700348 * identifier you supplied, or {@link EditorInfo#IME_NULL
349 * EditorInfo.IME_NULL} if being called due to the enter key
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800350 * being pressed.
351 * @param event If triggered by an enter key, this is the event;
352 * otherwise, this is null.
353 * @return Return true if you have consumed the action, else false.
354 */
355 boolean onEditorAction(TextView v, int actionId, KeyEvent event);
356 }
357
358 public TextView(Context context) {
359 this(context, null);
360 }
361
362 public TextView(Context context,
363 AttributeSet attrs) {
364 this(context, attrs, com.android.internal.R.attr.textViewStyle);
365 }
366
Gilles Debunnee15b3582010-06-16 15:17:21 -0700367 @SuppressWarnings("deprecation")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368 public TextView(Context context,
369 AttributeSet attrs,
370 int defStyle) {
371 super(context, attrs, defStyle);
372 mText = "";
373
374 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Eric Fischera9f1dd02009-08-12 15:00:10 -0700375 mTextPaint.density = getResources().getDisplayMetrics().density;
Dianne Hackbornafa78962009-09-28 17:33:54 -0700376 mTextPaint.setCompatibilityScaling(
377 getResources().getCompatibilityInfo().applicationScale);
Gilles Debunne8cbb4c62011-01-24 12:33:56 -0800378
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 // If we get the paint from the skin, we should set it to left, since
380 // the layout always wants it to be left.
381 // mTextPaint.setTextAlign(Paint.Align.LEFT);
382
383 mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Dianne Hackbornafa78962009-09-28 17:33:54 -0700384 mHighlightPaint.setCompatibilityScaling(
385 getResources().getCompatibilityInfo().applicationScale);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800386
387 mMovement = getDefaultMovementMethod();
388 mTransformation = null;
389
390 TypedArray a =
391 context.obtainStyledAttributes(
392 attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
393
394 int textColorHighlight = 0;
395 ColorStateList textColor = null;
396 ColorStateList textColorHint = null;
397 ColorStateList textColorLink = null;
398 int textSize = 15;
399 int typefaceIndex = -1;
400 int styleIndex = -1;
401
402 /*
403 * Look the appearance up without checking first if it exists because
404 * almost every TextView has one and it greatly simplifies the logic
405 * to be able to parse the appearance first and then let specific tags
406 * for this View override it.
407 */
408 TypedArray appearance = null;
409 int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1);
410 if (ap != -1) {
411 appearance = context.obtainStyledAttributes(ap,
412 com.android.internal.R.styleable.
413 TextAppearance);
414 }
415 if (appearance != null) {
416 int n = appearance.getIndexCount();
417 for (int i = 0; i < n; i++) {
418 int attr = appearance.getIndex(i);
419
420 switch (attr) {
421 case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
422 textColorHighlight = appearance.getColor(attr, textColorHighlight);
423 break;
424
425 case com.android.internal.R.styleable.TextAppearance_textColor:
426 textColor = appearance.getColorStateList(attr);
427 break;
428
429 case com.android.internal.R.styleable.TextAppearance_textColorHint:
430 textColorHint = appearance.getColorStateList(attr);
431 break;
432
433 case com.android.internal.R.styleable.TextAppearance_textColorLink:
434 textColorLink = appearance.getColorStateList(attr);
435 break;
436
437 case com.android.internal.R.styleable.TextAppearance_textSize:
438 textSize = appearance.getDimensionPixelSize(attr, textSize);
439 break;
440
441 case com.android.internal.R.styleable.TextAppearance_typeface:
442 typefaceIndex = appearance.getInt(attr, -1);
443 break;
444
445 case com.android.internal.R.styleable.TextAppearance_textStyle:
446 styleIndex = appearance.getInt(attr, -1);
447 break;
448 }
449 }
450
451 appearance.recycle();
452 }
453
454 boolean editable = getDefaultEditable();
455 CharSequence inputMethod = null;
456 int numeric = 0;
457 CharSequence digits = null;
458 boolean phone = false;
459 boolean autotext = false;
460 int autocap = -1;
461 int buffertype = 0;
462 boolean selectallonfocus = false;
463 Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
464 drawableBottom = null;
465 int drawablePadding = 0;
466 int ellipsize = -1;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700467 boolean singleLine = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800468 int maxlength = -1;
469 CharSequence text = "";
Romain Guy4dc4f732009-06-19 15:16:40 -0700470 CharSequence hint = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800471 int shadowcolor = 0;
472 float dx = 0, dy = 0, r = 0;
473 boolean password = false;
474 int inputType = EditorInfo.TYPE_NULL;
475
476 int n = a.getIndexCount();
477 for (int i = 0; i < n; i++) {
478 int attr = a.getIndex(i);
479
480 switch (attr) {
481 case com.android.internal.R.styleable.TextView_editable:
482 editable = a.getBoolean(attr, editable);
483 break;
484
485 case com.android.internal.R.styleable.TextView_inputMethod:
486 inputMethod = a.getText(attr);
487 break;
488
489 case com.android.internal.R.styleable.TextView_numeric:
490 numeric = a.getInt(attr, numeric);
491 break;
492
493 case com.android.internal.R.styleable.TextView_digits:
494 digits = a.getText(attr);
495 break;
496
497 case com.android.internal.R.styleable.TextView_phoneNumber:
498 phone = a.getBoolean(attr, phone);
499 break;
500
501 case com.android.internal.R.styleable.TextView_autoText:
502 autotext = a.getBoolean(attr, autotext);
503 break;
504
505 case com.android.internal.R.styleable.TextView_capitalize:
506 autocap = a.getInt(attr, autocap);
507 break;
508
509 case com.android.internal.R.styleable.TextView_bufferType:
510 buffertype = a.getInt(attr, buffertype);
511 break;
512
513 case com.android.internal.R.styleable.TextView_selectAllOnFocus:
514 selectallonfocus = a.getBoolean(attr, selectallonfocus);
515 break;
516
517 case com.android.internal.R.styleable.TextView_autoLink:
518 mAutoLinkMask = a.getInt(attr, 0);
519 break;
520
521 case com.android.internal.R.styleable.TextView_linksClickable:
522 mLinksClickable = a.getBoolean(attr, true);
523 break;
524
525 case com.android.internal.R.styleable.TextView_drawableLeft:
526 drawableLeft = a.getDrawable(attr);
527 break;
528
529 case com.android.internal.R.styleable.TextView_drawableTop:
530 drawableTop = a.getDrawable(attr);
531 break;
532
533 case com.android.internal.R.styleable.TextView_drawableRight:
534 drawableRight = a.getDrawable(attr);
535 break;
536
537 case com.android.internal.R.styleable.TextView_drawableBottom:
538 drawableBottom = a.getDrawable(attr);
539 break;
540
541 case com.android.internal.R.styleable.TextView_drawablePadding:
542 drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
543 break;
544
545 case com.android.internal.R.styleable.TextView_maxLines:
546 setMaxLines(a.getInt(attr, -1));
547 break;
548
549 case com.android.internal.R.styleable.TextView_maxHeight:
550 setMaxHeight(a.getDimensionPixelSize(attr, -1));
551 break;
552
553 case com.android.internal.R.styleable.TextView_lines:
554 setLines(a.getInt(attr, -1));
555 break;
556
557 case com.android.internal.R.styleable.TextView_height:
558 setHeight(a.getDimensionPixelSize(attr, -1));
559 break;
560
561 case com.android.internal.R.styleable.TextView_minLines:
562 setMinLines(a.getInt(attr, -1));
563 break;
564
565 case com.android.internal.R.styleable.TextView_minHeight:
566 setMinHeight(a.getDimensionPixelSize(attr, -1));
567 break;
568
569 case com.android.internal.R.styleable.TextView_maxEms:
570 setMaxEms(a.getInt(attr, -1));
571 break;
572
573 case com.android.internal.R.styleable.TextView_maxWidth:
574 setMaxWidth(a.getDimensionPixelSize(attr, -1));
575 break;
576
577 case com.android.internal.R.styleable.TextView_ems:
578 setEms(a.getInt(attr, -1));
579 break;
580
581 case com.android.internal.R.styleable.TextView_width:
582 setWidth(a.getDimensionPixelSize(attr, -1));
583 break;
584
585 case com.android.internal.R.styleable.TextView_minEms:
586 setMinEms(a.getInt(attr, -1));
587 break;
588
589 case com.android.internal.R.styleable.TextView_minWidth:
590 setMinWidth(a.getDimensionPixelSize(attr, -1));
591 break;
592
593 case com.android.internal.R.styleable.TextView_gravity:
594 setGravity(a.getInt(attr, -1));
595 break;
596
597 case com.android.internal.R.styleable.TextView_hint:
Romain Guy4dc4f732009-06-19 15:16:40 -0700598 hint = a.getText(attr);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800599 break;
600
601 case com.android.internal.R.styleable.TextView_text:
602 text = a.getText(attr);
603 break;
604
605 case com.android.internal.R.styleable.TextView_scrollHorizontally:
606 if (a.getBoolean(attr, false)) {
607 setHorizontallyScrolling(true);
608 }
609 break;
610
611 case com.android.internal.R.styleable.TextView_singleLine:
612 singleLine = a.getBoolean(attr, singleLine);
613 break;
614
615 case com.android.internal.R.styleable.TextView_ellipsize:
616 ellipsize = a.getInt(attr, ellipsize);
617 break;
618
619 case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
620 setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
621 break;
622
623 case com.android.internal.R.styleable.TextView_includeFontPadding:
624 if (!a.getBoolean(attr, true)) {
625 setIncludeFontPadding(false);
626 }
627 break;
628
629 case com.android.internal.R.styleable.TextView_cursorVisible:
630 if (!a.getBoolean(attr, true)) {
631 setCursorVisible(false);
632 }
633 break;
634
635 case com.android.internal.R.styleable.TextView_maxLength:
636 maxlength = a.getInt(attr, -1);
637 break;
638
639 case com.android.internal.R.styleable.TextView_textScaleX:
640 setTextScaleX(a.getFloat(attr, 1.0f));
641 break;
642
643 case com.android.internal.R.styleable.TextView_freezesText:
644 mFreezesText = a.getBoolean(attr, false);
645 break;
646
647 case com.android.internal.R.styleable.TextView_shadowColor:
648 shadowcolor = a.getInt(attr, 0);
649 break;
650
651 case com.android.internal.R.styleable.TextView_shadowDx:
652 dx = a.getFloat(attr, 0);
653 break;
654
655 case com.android.internal.R.styleable.TextView_shadowDy:
656 dy = a.getFloat(attr, 0);
657 break;
658
659 case com.android.internal.R.styleable.TextView_shadowRadius:
660 r = a.getFloat(attr, 0);
661 break;
662
663 case com.android.internal.R.styleable.TextView_enabled:
664 setEnabled(a.getBoolean(attr, isEnabled()));
665 break;
666
667 case com.android.internal.R.styleable.TextView_textColorHighlight:
668 textColorHighlight = a.getColor(attr, textColorHighlight);
669 break;
670
671 case com.android.internal.R.styleable.TextView_textColor:
672 textColor = a.getColorStateList(attr);
673 break;
674
675 case com.android.internal.R.styleable.TextView_textColorHint:
676 textColorHint = a.getColorStateList(attr);
677 break;
678
679 case com.android.internal.R.styleable.TextView_textColorLink:
680 textColorLink = a.getColorStateList(attr);
681 break;
682
683 case com.android.internal.R.styleable.TextView_textSize:
684 textSize = a.getDimensionPixelSize(attr, textSize);
685 break;
686
687 case com.android.internal.R.styleable.TextView_typeface:
688 typefaceIndex = a.getInt(attr, typefaceIndex);
689 break;
690
691 case com.android.internal.R.styleable.TextView_textStyle:
692 styleIndex = a.getInt(attr, styleIndex);
693 break;
694
695 case com.android.internal.R.styleable.TextView_password:
696 password = a.getBoolean(attr, password);
697 break;
698
699 case com.android.internal.R.styleable.TextView_lineSpacingExtra:
700 mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
701 break;
702
703 case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
704 mSpacingMult = a.getFloat(attr, mSpacingMult);
705 break;
706
707 case com.android.internal.R.styleable.TextView_inputType:
708 inputType = a.getInt(attr, mInputType);
709 break;
710
711 case com.android.internal.R.styleable.TextView_imeOptions:
712 if (mInputContentType == null) {
713 mInputContentType = new InputContentType();
714 }
715 mInputContentType.imeOptions = a.getInt(attr,
716 mInputContentType.imeOptions);
717 break;
718
719 case com.android.internal.R.styleable.TextView_imeActionLabel:
720 if (mInputContentType == null) {
721 mInputContentType = new InputContentType();
722 }
723 mInputContentType.imeActionLabel = a.getText(attr);
724 break;
725
726 case com.android.internal.R.styleable.TextView_imeActionId:
727 if (mInputContentType == null) {
728 mInputContentType = new InputContentType();
729 }
730 mInputContentType.imeActionId = a.getInt(attr,
731 mInputContentType.imeActionId);
732 break;
733
734 case com.android.internal.R.styleable.TextView_privateImeOptions:
735 setPrivateImeOptions(a.getString(attr));
736 break;
737
738 case com.android.internal.R.styleable.TextView_editorExtras:
739 try {
740 setInputExtras(a.getResourceId(attr, 0));
741 } catch (XmlPullParserException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700742 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800743 } catch (IOException e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700744 Log.w(LOG_TAG, "Failure reading input extras", e);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745 }
746 break;
Adam Powellb08013c2010-09-16 16:28:11 -0700747
Gilles Debunnef75c97e2011-02-10 16:09:53 -0800748 case com.android.internal.R.styleable.TextView_textCursorDrawable:
749 mCursorDrawableRes = a.getResourceId(attr, 0);
750 break;
751
Adam Powellb08013c2010-09-16 16:28:11 -0700752 case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
753 mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
754 break;
755
756 case com.android.internal.R.styleable.TextView_textSelectHandleRight:
757 mTextSelectHandleRightRes = a.getResourceId(attr, 0);
758 break;
759
760 case com.android.internal.R.styleable.TextView_textSelectHandle:
761 mTextSelectHandleRes = a.getResourceId(attr, 0);
762 break;
Gilles Debunne7b9652b2010-10-26 16:27:12 -0700763
764 case com.android.internal.R.styleable.TextView_textEditPasteWindowLayout:
765 mTextEditPasteWindowLayout = a.getResourceId(attr, 0);
766 break;
767
768 case com.android.internal.R.styleable.TextView_textEditNoPasteWindowLayout:
769 mTextEditNoPasteWindowLayout = a.getResourceId(attr, 0);
770 break;
771
Gilles Debunnee60e1e52011-01-20 12:19:44 -0800772 case com.android.internal.R.styleable.TextView_textEditSidePasteWindowLayout:
773 mTextEditSidePasteWindowLayout = a.getResourceId(attr, 0);
774 break;
775
776 case com.android.internal.R.styleable.TextView_textEditSideNoPasteWindowLayout:
777 mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0);
778 break;
779
Gilles Debunne86b9c782010-11-11 10:43:48 -0800780 case com.android.internal.R.styleable.TextView_textIsSelectable:
781 mTextIsSelectable = a.getBoolean(attr, false);
782 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800783 }
784 }
785 a.recycle();
786
787 BufferType bufferType = BufferType.EDITABLE;
788
Gilles Debunned7483bf2010-11-10 10:47:45 -0800789 final int variation =
790 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
791 final boolean passwordInputType = variation
792 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
793 final boolean webPasswordInputType = variation
794 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
Ken Wakasa82d731a2010-12-24 23:42:41 +0900795 final boolean numberPasswordInputType = variation
796 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Gilles Debunned7483bf2010-11-10 10:47:45 -0800797
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800798 if (inputMethod != null) {
Gilles Debunnee15b3582010-06-16 15:17:21 -0700799 Class<?> c;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800800
801 try {
802 c = Class.forName(inputMethod.toString());
803 } catch (ClassNotFoundException ex) {
804 throw new RuntimeException(ex);
805 }
806
807 try {
808 mInput = (KeyListener) c.newInstance();
809 } catch (InstantiationException ex) {
810 throw new RuntimeException(ex);
811 } catch (IllegalAccessException ex) {
812 throw new RuntimeException(ex);
813 }
814 try {
815 mInputType = inputType != EditorInfo.TYPE_NULL
816 ? inputType
817 : mInput.getInputType();
818 } catch (IncompatibleClassChangeError e) {
819 mInputType = EditorInfo.TYPE_CLASS_TEXT;
820 }
821 } else if (digits != null) {
822 mInput = DigitsKeyListener.getInstance(digits.toString());
Dianne Hackborn7ed6ee52009-09-10 18:41:28 -0700823 // If no input type was specified, we will default to generic
824 // text, since we can't tell the IME about the set of digits
825 // that was selected.
826 mInputType = inputType != EditorInfo.TYPE_NULL
827 ? inputType : EditorInfo.TYPE_CLASS_TEXT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800828 } else if (inputType != EditorInfo.TYPE_NULL) {
829 setInputType(inputType, true);
Gilles Debunne91a08cf2010-11-08 17:34:49 -0800830 // If set, the input type overrides what was set using the deprecated singleLine flag.
831 singleLine = !isMultilineInputType(inputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800832 } else if (phone) {
833 mInput = DialerKeyListener.getInstance();
Dianne Hackborn49a1a9b52009-03-24 20:06:11 -0700834 mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800835 } else if (numeric != 0) {
836 mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
837 (numeric & DECIMAL) != 0);
838 inputType = EditorInfo.TYPE_CLASS_NUMBER;
839 if ((numeric & SIGNED) != 0) {
840 inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
841 }
842 if ((numeric & DECIMAL) != 0) {
843 inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
844 }
845 mInputType = inputType;
846 } else if (autotext || autocap != -1) {
847 TextKeyListener.Capitalize cap;
848
849 inputType = EditorInfo.TYPE_CLASS_TEXT;
Gilles Debunnef95449d2010-11-05 13:54:13 -0700850
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800851 switch (autocap) {
852 case 1:
853 cap = TextKeyListener.Capitalize.SENTENCES;
854 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
855 break;
856
857 case 2:
858 cap = TextKeyListener.Capitalize.WORDS;
859 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
860 break;
861
862 case 3:
863 cap = TextKeyListener.Capitalize.CHARACTERS;
864 inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
865 break;
866
867 default:
868 cap = TextKeyListener.Capitalize.NONE;
869 break;
870 }
871
872 mInput = TextKeyListener.getInstance(autotext, cap);
873 mInputType = inputType;
Gilles Debunne86b9c782010-11-11 10:43:48 -0800874 } else if (mTextIsSelectable) {
875 // Prevent text changes from keyboard.
876 mInputType = EditorInfo.TYPE_NULL;
877 mInput = null;
878 bufferType = BufferType.SPANNABLE;
Gilles Debunnecbcb3452010-12-17 15:31:02 -0800879 // Required to request focus while in touch mode.
880 setFocusableInTouchMode(true);
Gilles Debunne86b9c782010-11-11 10:43:48 -0800881 // So that selection can be changed using arrow keys and touch is handled.
882 setMovementMethod(ArrowKeyMovementMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800883 } else if (editable) {
884 mInput = TextKeyListener.getInstance();
885 mInputType = EditorInfo.TYPE_CLASS_TEXT;
886 } else {
887 mInput = null;
888
889 switch (buffertype) {
890 case 0:
891 bufferType = BufferType.NORMAL;
892 break;
893 case 1:
894 bufferType = BufferType.SPANNABLE;
895 break;
896 case 2:
897 bufferType = BufferType.EDITABLE;
898 break;
899 }
900 }
901
Gilles Debunned7483bf2010-11-10 10:47:45 -0800902 // mInputType has been set from inputType, possibly modified by mInputMethod.
903 // Specialize mInputType to [web]password if we have a text class and the original input
904 // type was a password.
905 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
906 if (password || passwordInputType) {
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -0400907 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
Gilles Debunned7483bf2010-11-10 10:47:45 -0800908 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
Leon Scrogginsb5ce0e02010-11-01 13:20:24 -0400909 }
Gilles Debunned7483bf2010-11-10 10:47:45 -0800910 if (webPasswordInputType) {
911 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
912 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
913 }
Ken Wakasa82d731a2010-12-24 23:42:41 +0900914 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
915 if (numberPasswordInputType) {
916 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
917 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
918 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800919 }
920
921 if (selectallonfocus) {
922 mSelectAllOnFocus = true;
923
924 if (bufferType == BufferType.NORMAL)
925 bufferType = BufferType.SPANNABLE;
926 }
927
928 setCompoundDrawablesWithIntrinsicBounds(
929 drawableLeft, drawableTop, drawableRight, drawableBottom);
930 setCompoundDrawablePadding(drawablePadding);
931
Gilles Debunnea3ae4a02010-12-13 17:22:39 -0800932 // Same as setSingleLine(), but make sure the transformation method and the maximum number
Gilles Debunne066460f2010-12-15 17:31:51 -0800933 // of lines of height are unchanged for multi-line TextViews.
Gilles Debunned7483bf2010-11-10 10:47:45 -0800934 setInputTypeSingleLine(singleLine);
Gilles Debunne066460f2010-12-15 17:31:51 -0800935 applySingleLine(singleLine, singleLine, singleLine);
Gilles Debunned7483bf2010-11-10 10:47:45 -0800936
Gilles Debunne91a08cf2010-11-08 17:34:49 -0800937 if (singleLine && mInput == null && ellipsize < 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800938 ellipsize = 3; // END
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800939 }
940
941 switch (ellipsize) {
942 case 1:
943 setEllipsize(TextUtils.TruncateAt.START);
944 break;
945 case 2:
946 setEllipsize(TextUtils.TruncateAt.MIDDLE);
947 break;
948 case 3:
949 setEllipsize(TextUtils.TruncateAt.END);
950 break;
951 case 4:
952 setHorizontalFadingEdgeEnabled(true);
953 setEllipsize(TextUtils.TruncateAt.MARQUEE);
954 break;
955 }
956
957 setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
958 setHintTextColor(textColorHint);
959 setLinkTextColor(textColorLink);
960 if (textColorHighlight != 0) {
961 setHighlightColor(textColorHighlight);
962 }
963 setRawTextSize(textSize);
964
Ken Wakasa82d731a2010-12-24 23:42:41 +0900965 if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800966 setTransformationMethod(PasswordTransformationMethod.getInstance());
967 typefaceIndex = MONOSPACE;
Gilles Debunned7483bf2010-11-10 10:47:45 -0800968 } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
969 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -0800970 typefaceIndex = MONOSPACE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800971 }
972
973 setTypefaceByIndex(typefaceIndex, styleIndex);
974
975 if (shadowcolor != 0) {
976 setShadowLayer(r, dx, dy, shadowcolor);
977 }
978
979 if (maxlength >= 0) {
980 setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
981 } else {
982 setFilters(NO_FILTERS);
983 }
984
985 setText(text, bufferType);
Romain Guy4dc4f732009-06-19 15:16:40 -0700986 if (hint != null) setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800987
988 /*
989 * Views are not normally focusable unless specified to be.
990 * However, TextViews that have input or movement methods *are*
991 * focusable by default.
992 */
993 a = context.obtainStyledAttributes(attrs,
994 com.android.internal.R.styleable.View,
995 defStyle, 0);
996
997 boolean focusable = mMovement != null || mInput != null;
998 boolean clickable = focusable;
999 boolean longClickable = focusable;
1000
1001 n = a.getIndexCount();
1002 for (int i = 0; i < n; i++) {
1003 int attr = a.getIndex(i);
1004
1005 switch (attr) {
1006 case com.android.internal.R.styleable.View_focusable:
1007 focusable = a.getBoolean(attr, focusable);
1008 break;
1009
1010 case com.android.internal.R.styleable.View_clickable:
1011 clickable = a.getBoolean(attr, clickable);
1012 break;
1013
1014 case com.android.internal.R.styleable.View_longClickable:
1015 longClickable = a.getBoolean(attr, longClickable);
1016 break;
1017 }
1018 }
1019 a.recycle();
1020
1021 setFocusable(focusable);
1022 setClickable(clickable);
1023 setLongClickable(longClickable);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07001024
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001025 prepareCursorControllers();
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08001026
1027 final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1028 final int touchSlop = viewConfiguration.getScaledTouchSlop();
1029 mSquaredTouchSlopDistance = touchSlop * touchSlop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001030 }
1031
1032 private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1033 Typeface tf = null;
1034 switch (typefaceIndex) {
1035 case SANS:
1036 tf = Typeface.SANS_SERIF;
1037 break;
1038
1039 case SERIF:
1040 tf = Typeface.SERIF;
1041 break;
1042
1043 case MONOSPACE:
1044 tf = Typeface.MONOSPACE;
1045 break;
1046 }
1047
1048 setTypeface(tf, styleIndex);
1049 }
1050
Janos Levai042856c2010-10-15 02:53:58 +03001051 @Override
1052 public void setEnabled(boolean enabled) {
1053 if (enabled == isEnabled()) {
1054 return;
1055 }
1056
1057 if (!enabled) {
1058 // Hide the soft input if the currently active TextView is disabled
1059 InputMethodManager imm = InputMethodManager.peekInstance();
1060 if (imm != null && imm.isActive(this)) {
1061 imm.hideSoftInputFromWindow(getWindowToken(), 0);
1062 }
1063 }
1064 super.setEnabled(enabled);
1065 }
1066
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001067 /**
1068 * Sets the typeface and style in which the text should be displayed,
1069 * and turns on the fake bold and italic bits in the Paint if the
1070 * Typeface that you provided does not have all the bits in the
1071 * style that you specified.
1072 *
1073 * @attr ref android.R.styleable#TextView_typeface
1074 * @attr ref android.R.styleable#TextView_textStyle
1075 */
1076 public void setTypeface(Typeface tf, int style) {
1077 if (style > 0) {
1078 if (tf == null) {
1079 tf = Typeface.defaultFromStyle(style);
1080 } else {
1081 tf = Typeface.create(tf, style);
1082 }
1083
1084 setTypeface(tf);
1085 // now compute what (if any) algorithmic styling is needed
1086 int typefaceStyle = tf != null ? tf.getStyle() : 0;
1087 int need = style & ~typefaceStyle;
1088 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1089 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1090 } else {
1091 mTextPaint.setFakeBoldText(false);
1092 mTextPaint.setTextSkewX(0);
1093 setTypeface(tf);
1094 }
1095 }
1096
1097 /**
1098 * Subclasses override this to specify that they have a KeyListener
1099 * by default even if not specifically called for in the XML options.
1100 */
1101 protected boolean getDefaultEditable() {
1102 return false;
1103 }
1104
1105 /**
1106 * Subclasses override this to specify a default movement method.
1107 */
1108 protected MovementMethod getDefaultMovementMethod() {
1109 return null;
1110 }
1111
1112 /**
1113 * Return the text the TextView is displaying. If setText() was called with
1114 * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1115 * the return value from this method to Spannable or Editable, respectively.
1116 *
1117 * Note: The content of the return value should not be modified. If you want
1118 * a modifiable one, you should make your own copy first.
1119 */
1120 @ViewDebug.CapturedViewProperty
1121 public CharSequence getText() {
1122 return mText;
1123 }
1124
1125 /**
1126 * Returns the length, in characters, of the text managed by this TextView
1127 */
1128 public int length() {
1129 return mText.length();
1130 }
1131
1132 /**
1133 * Return the text the TextView is displaying as an Editable object. If
1134 * the text is not editable, null is returned.
1135 *
1136 * @see #getText
1137 */
1138 public Editable getEditableText() {
1139 return (mText instanceof Editable) ? (Editable)mText : null;
1140 }
1141
1142 /**
1143 * @return the height of one standard line in pixels. Note that markup
1144 * within the text can cause individual lines to be taller or shorter
1145 * than this height, and the layout may contain additional first-
1146 * or last-line padding.
1147 */
1148 public int getLineHeight() {
Gilles Debunne96e6b8b2010-12-14 13:43:45 -08001149 return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001150 }
1151
1152 /**
1153 * @return the Layout that is currently being used to display the text.
1154 * This can be null if the text or width has recently changes.
1155 */
1156 public final Layout getLayout() {
1157 return mLayout;
1158 }
1159
1160 /**
1161 * @return the current key listener for this TextView.
1162 * This will frequently be null for non-EditText TextViews.
1163 */
1164 public final KeyListener getKeyListener() {
1165 return mInput;
1166 }
1167
1168 /**
1169 * Sets the key listener to be used with this TextView. This can be null
1170 * to disallow user input. Note that this method has significant and
1171 * subtle interactions with soft keyboards and other input method:
1172 * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1173 * for important details. Calling this method will replace the current
1174 * content type of the text view with the content type returned by the
1175 * key listener.
1176 * <p>
1177 * Be warned that if you want a TextView with a key listener or movement
1178 * method not to be focusable, or if you want a TextView without a
1179 * key listener or movement method to be focusable, you must call
1180 * {@link #setFocusable} again after calling this to get the focusability
1181 * back the way you want it.
1182 *
1183 * @attr ref android.R.styleable#TextView_numeric
1184 * @attr ref android.R.styleable#TextView_digits
1185 * @attr ref android.R.styleable#TextView_phoneNumber
1186 * @attr ref android.R.styleable#TextView_inputMethod
1187 * @attr ref android.R.styleable#TextView_capitalize
1188 * @attr ref android.R.styleable#TextView_autoText
1189 */
1190 public void setKeyListener(KeyListener input) {
1191 setKeyListenerOnly(input);
1192 fixFocusableAndClickableSettings();
1193
1194 if (input != null) {
1195 try {
1196 mInputType = mInput.getInputType();
1197 } catch (IncompatibleClassChangeError e) {
1198 mInputType = EditorInfo.TYPE_CLASS_TEXT;
1199 }
Gilles Debunned7483bf2010-11-10 10:47:45 -08001200 // Change inputType, without affecting transformation.
1201 // No need to applySingleLine since mSingleLine is unchanged.
1202 setInputTypeSingleLine(mSingleLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001203 } else {
1204 mInputType = EditorInfo.TYPE_NULL;
1205 }
1206
1207 InputMethodManager imm = InputMethodManager.peekInstance();
1208 if (imm != null) imm.restartInput(this);
1209 }
1210
1211 private void setKeyListenerOnly(KeyListener input) {
1212 mInput = input;
1213 if (mInput != null && !(mText instanceof Editable))
1214 setText(mText);
1215
1216 setFilters((Editable) mText, mFilters);
1217 }
1218
1219 /**
1220 * @return the movement method being used for this TextView.
1221 * This will frequently be null for non-EditText TextViews.
1222 */
1223 public final MovementMethod getMovementMethod() {
1224 return mMovement;
1225 }
1226
1227 /**
1228 * Sets the movement method (arrow key handler) to be used for
1229 * this TextView. This can be null to disallow using the arrow keys
1230 * to move the cursor or scroll the view.
1231 * <p>
1232 * Be warned that if you want a TextView with a key listener or movement
1233 * method not to be focusable, or if you want a TextView without a
1234 * key listener or movement method to be focusable, you must call
1235 * {@link #setFocusable} again after calling this to get the focusability
1236 * back the way you want it.
1237 */
1238 public final void setMovementMethod(MovementMethod movement) {
1239 mMovement = movement;
1240
1241 if (mMovement != null && !(mText instanceof Spannable))
1242 setText(mText);
1243
1244 fixFocusableAndClickableSettings();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001245
Gilles Debunnebaaace52010-10-01 15:47:13 -07001246 // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
Gilles Debunnef788a9f2010-07-22 10:17:23 -07001247 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001248 }
1249
1250 private void fixFocusableAndClickableSettings() {
1251 if ((mMovement != null) || mInput != null) {
1252 setFocusable(true);
1253 setClickable(true);
1254 setLongClickable(true);
1255 } else {
1256 setFocusable(false);
1257 setClickable(false);
1258 setLongClickable(false);
1259 }
1260 }
1261
1262 /**
1263 * @return the current transformation method for this TextView.
1264 * This will frequently be null except for single-line and password
1265 * fields.
1266 */
1267 public final TransformationMethod getTransformationMethod() {
1268 return mTransformation;
1269 }
1270
1271 /**
1272 * Sets the transformation that is applied to the text that this
1273 * TextView is displaying.
1274 *
1275 * @attr ref android.R.styleable#TextView_password
1276 * @attr ref android.R.styleable#TextView_singleLine
1277 */
1278 public final void setTransformationMethod(TransformationMethod method) {
1279 if (method == mTransformation) {
1280 // Avoid the setText() below if the transformation is
1281 // the same.
1282 return;
1283 }
1284 if (mTransformation != null) {
1285 if (mText instanceof Spannable) {
1286 ((Spannable) mText).removeSpan(mTransformation);
1287 }
1288 }
1289
1290 mTransformation = method;
1291
1292 setText(mText);
1293 }
1294
1295 /**
1296 * Returns the top padding of the view, plus space for the top
1297 * Drawable if any.
1298 */
1299 public int getCompoundPaddingTop() {
1300 final Drawables dr = mDrawables;
1301 if (dr == null || dr.mDrawableTop == null) {
1302 return mPaddingTop;
1303 } else {
1304 return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1305 }
1306 }
1307
1308 /**
1309 * Returns the bottom padding of the view, plus space for the bottom
1310 * Drawable if any.
1311 */
1312 public int getCompoundPaddingBottom() {
1313 final Drawables dr = mDrawables;
1314 if (dr == null || dr.mDrawableBottom == null) {
1315 return mPaddingBottom;
1316 } else {
1317 return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1318 }
1319 }
1320
1321 /**
1322 * Returns the left padding of the view, plus space for the left
1323 * Drawable if any.
1324 */
1325 public int getCompoundPaddingLeft() {
1326 final Drawables dr = mDrawables;
1327 if (dr == null || dr.mDrawableLeft == null) {
1328 return mPaddingLeft;
1329 } else {
1330 return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1331 }
1332 }
1333
1334 /**
1335 * Returns the right padding of the view, plus space for the right
1336 * Drawable if any.
1337 */
1338 public int getCompoundPaddingRight() {
1339 final Drawables dr = mDrawables;
1340 if (dr == null || dr.mDrawableRight == null) {
1341 return mPaddingRight;
1342 } else {
1343 return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1344 }
1345 }
1346
1347 /**
1348 * Returns the extended top padding of the view, including both the
1349 * top Drawable if any and any extra space to keep more than maxLines
1350 * of text from showing. It is only valid to call this after measuring.
1351 */
1352 public int getExtendedPaddingTop() {
1353 if (mMaxMode != LINES) {
1354 return getCompoundPaddingTop();
1355 }
1356
1357 if (mLayout.getLineCount() <= mMaximum) {
1358 return getCompoundPaddingTop();
1359 }
1360
1361 int top = getCompoundPaddingTop();
1362 int bottom = getCompoundPaddingBottom();
1363 int viewht = getHeight() - top - bottom;
1364 int layoutht = mLayout.getLineTop(mMaximum);
1365
1366 if (layoutht >= viewht) {
1367 return top;
1368 }
1369
1370 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1371 if (gravity == Gravity.TOP) {
1372 return top;
1373 } else if (gravity == Gravity.BOTTOM) {
1374 return top + viewht - layoutht;
1375 } else { // (gravity == Gravity.CENTER_VERTICAL)
1376 return top + (viewht - layoutht) / 2;
1377 }
1378 }
1379
1380 /**
1381 * Returns the extended bottom padding of the view, including both the
1382 * bottom Drawable if any and any extra space to keep more than maxLines
1383 * of text from showing. It is only valid to call this after measuring.
1384 */
1385 public int getExtendedPaddingBottom() {
1386 if (mMaxMode != LINES) {
1387 return getCompoundPaddingBottom();
1388 }
1389
1390 if (mLayout.getLineCount() <= mMaximum) {
1391 return getCompoundPaddingBottom();
1392 }
1393
1394 int top = getCompoundPaddingTop();
1395 int bottom = getCompoundPaddingBottom();
1396 int viewht = getHeight() - top - bottom;
1397 int layoutht = mLayout.getLineTop(mMaximum);
1398
1399 if (layoutht >= viewht) {
1400 return bottom;
1401 }
1402
1403 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1404 if (gravity == Gravity.TOP) {
1405 return bottom + viewht - layoutht;
1406 } else if (gravity == Gravity.BOTTOM) {
1407 return bottom;
1408 } else { // (gravity == Gravity.CENTER_VERTICAL)
1409 return bottom + (viewht - layoutht) / 2;
1410 }
1411 }
1412
1413 /**
1414 * Returns the total left padding of the view, including the left
1415 * Drawable if any.
1416 */
1417 public int getTotalPaddingLeft() {
1418 return getCompoundPaddingLeft();
1419 }
1420
1421 /**
1422 * Returns the total right padding of the view, including the right
1423 * Drawable if any.
1424 */
1425 public int getTotalPaddingRight() {
1426 return getCompoundPaddingRight();
1427 }
1428
1429 /**
1430 * Returns the total top padding of the view, including the top
1431 * Drawable if any, the extra space to keep more than maxLines
1432 * from showing, and the vertical offset for gravity, if any.
1433 */
1434 public int getTotalPaddingTop() {
1435 return getExtendedPaddingTop() + getVerticalOffset(true);
1436 }
1437
1438 /**
1439 * Returns the total bottom padding of the view, including the bottom
1440 * Drawable if any, the extra space to keep more than maxLines
1441 * from showing, and the vertical offset for gravity, if any.
1442 */
1443 public int getTotalPaddingBottom() {
1444 return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1445 }
1446
1447 /**
1448 * Sets the Drawables (if any) to appear to the left of, above,
1449 * to the right of, and below the text. Use null if you do not
1450 * want a Drawable there. The Drawables must already have had
1451 * {@link Drawable#setBounds} called.
1452 *
1453 * @attr ref android.R.styleable#TextView_drawableLeft
1454 * @attr ref android.R.styleable#TextView_drawableTop
1455 * @attr ref android.R.styleable#TextView_drawableRight
1456 * @attr ref android.R.styleable#TextView_drawableBottom
1457 */
1458 public void setCompoundDrawables(Drawable left, Drawable top,
1459 Drawable right, Drawable bottom) {
1460 Drawables dr = mDrawables;
1461
1462 final boolean drawables = left != null || top != null
1463 || right != null || bottom != null;
1464
1465 if (!drawables) {
1466 // Clearing drawables... can we free the data structure?
1467 if (dr != null) {
1468 if (dr.mDrawablePadding == 0) {
1469 mDrawables = null;
1470 } else {
1471 // We need to retain the last set padding, so just clear
1472 // out all of the fields in the existing structure.
Romain Guy48540eb2009-05-19 16:44:57 -07001473 if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001474 dr.mDrawableLeft = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001475 if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001476 dr.mDrawableTop = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001477 if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001478 dr.mDrawableRight = null;
Romain Guy48540eb2009-05-19 16:44:57 -07001479 if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001480 dr.mDrawableBottom = null;
1481 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1482 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1483 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1484 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1485 }
1486 }
1487 } else {
1488 if (dr == null) {
1489 mDrawables = dr = new Drawables();
1490 }
1491
Romain Guy48540eb2009-05-19 16:44:57 -07001492 if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1493 dr.mDrawableLeft.setCallback(null);
1494 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001495 dr.mDrawableLeft = left;
Romain Guy8e618e52010-03-08 12:18:20 -08001496
1497 if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001498 dr.mDrawableTop.setCallback(null);
1499 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001500 dr.mDrawableTop = top;
Romain Guy8e618e52010-03-08 12:18:20 -08001501
1502 if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001503 dr.mDrawableRight.setCallback(null);
1504 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001505 dr.mDrawableRight = right;
Romain Guy8e618e52010-03-08 12:18:20 -08001506
1507 if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
Romain Guy48540eb2009-05-19 16:44:57 -07001508 dr.mDrawableBottom.setCallback(null);
1509 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001510 dr.mDrawableBottom = bottom;
1511
1512 final Rect compoundRect = dr.mCompoundRect;
Romain Guy48540eb2009-05-19 16:44:57 -07001513 int[] state;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001514
1515 state = getDrawableState();
1516
1517 if (left != null) {
1518 left.setState(state);
1519 left.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001520 left.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001521 dr.mDrawableSizeLeft = compoundRect.width();
1522 dr.mDrawableHeightLeft = compoundRect.height();
1523 } else {
1524 dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1525 }
1526
1527 if (right != null) {
1528 right.setState(state);
1529 right.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001530 right.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001531 dr.mDrawableSizeRight = compoundRect.width();
1532 dr.mDrawableHeightRight = compoundRect.height();
1533 } else {
1534 dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1535 }
1536
1537 if (top != null) {
1538 top.setState(state);
1539 top.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001540 top.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001541 dr.mDrawableSizeTop = compoundRect.height();
1542 dr.mDrawableWidthTop = compoundRect.width();
1543 } else {
1544 dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1545 }
1546
1547 if (bottom != null) {
1548 bottom.setState(state);
1549 bottom.copyBounds(compoundRect);
Romain Guy48540eb2009-05-19 16:44:57 -07001550 bottom.setCallback(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001551 dr.mDrawableSizeBottom = compoundRect.height();
1552 dr.mDrawableWidthBottom = compoundRect.width();
1553 } else {
1554 dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1555 }
1556 }
1557
1558 invalidate();
1559 requestLayout();
1560 }
1561
1562 /**
1563 * Sets the Drawables (if any) to appear to the left of, above,
1564 * to the right of, and below the text. Use 0 if you do not
1565 * want a Drawable there. The Drawables' bounds will be set to
1566 * their intrinsic bounds.
1567 *
1568 * @param left Resource identifier of the left Drawable.
1569 * @param top Resource identifier of the top Drawable.
1570 * @param right Resource identifier of the right Drawable.
1571 * @param bottom Resource identifier of the bottom Drawable.
1572 *
1573 * @attr ref android.R.styleable#TextView_drawableLeft
1574 * @attr ref android.R.styleable#TextView_drawableTop
1575 * @attr ref android.R.styleable#TextView_drawableRight
1576 * @attr ref android.R.styleable#TextView_drawableBottom
1577 */
1578 public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1579 final Resources resources = getContext().getResources();
1580 setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1581 top != 0 ? resources.getDrawable(top) : null,
1582 right != 0 ? resources.getDrawable(right) : null,
1583 bottom != 0 ? resources.getDrawable(bottom) : null);
1584 }
1585
1586 /**
1587 * Sets the Drawables (if any) to appear to the left of, above,
1588 * to the right of, and below the text. Use null if you do not
1589 * want a Drawable there. The Drawables' bounds will be set to
1590 * their intrinsic bounds.
1591 *
1592 * @attr ref android.R.styleable#TextView_drawableLeft
1593 * @attr ref android.R.styleable#TextView_drawableTop
1594 * @attr ref android.R.styleable#TextView_drawableRight
1595 * @attr ref android.R.styleable#TextView_drawableBottom
1596 */
1597 public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1598 Drawable right, Drawable bottom) {
1599
1600 if (left != null) {
1601 left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1602 }
1603 if (right != null) {
1604 right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1605 }
1606 if (top != null) {
1607 top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1608 }
1609 if (bottom != null) {
1610 bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1611 }
1612 setCompoundDrawables(left, top, right, bottom);
1613 }
1614
1615 /**
1616 * Returns drawables for the left, top, right, and bottom borders.
1617 */
1618 public Drawable[] getCompoundDrawables() {
1619 final Drawables dr = mDrawables;
1620 if (dr != null) {
1621 return new Drawable[] {
1622 dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
1623 };
1624 } else {
1625 return new Drawable[] { null, null, null, null };
1626 }
1627 }
1628
1629 /**
1630 * Sets the size of the padding between the compound drawables and
1631 * the text.
1632 *
1633 * @attr ref android.R.styleable#TextView_drawablePadding
1634 */
1635 public void setCompoundDrawablePadding(int pad) {
1636 Drawables dr = mDrawables;
1637 if (pad == 0) {
1638 if (dr != null) {
1639 dr.mDrawablePadding = pad;
1640 }
1641 } else {
1642 if (dr == null) {
1643 mDrawables = dr = new Drawables();
1644 }
1645 dr.mDrawablePadding = pad;
1646 }
1647
1648 invalidate();
1649 requestLayout();
1650 }
1651
1652 /**
1653 * Returns the padding between the compound drawables and the text.
1654 */
1655 public int getCompoundDrawablePadding() {
1656 final Drawables dr = mDrawables;
1657 return dr != null ? dr.mDrawablePadding : 0;
1658 }
1659
1660 @Override
1661 public void setPadding(int left, int top, int right, int bottom) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07001662 if (left != mPaddingLeft ||
1663 right != mPaddingRight ||
1664 top != mPaddingTop ||
1665 bottom != mPaddingBottom) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001666 nullLayouts();
1667 }
1668
1669 // the super call will requestLayout()
1670 super.setPadding(left, top, right, bottom);
1671 invalidate();
1672 }
1673
1674 /**
1675 * Gets the autolink mask of the text. See {@link
1676 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1677 * possible values.
1678 *
1679 * @attr ref android.R.styleable#TextView_autoLink
1680 */
1681 public final int getAutoLinkMask() {
1682 return mAutoLinkMask;
1683 }
1684
1685 /**
1686 * Sets the text color, size, style, hint color, and highlight color
1687 * from the specified TextAppearance resource.
1688 */
1689 public void setTextAppearance(Context context, int resid) {
1690 TypedArray appearance =
1691 context.obtainStyledAttributes(resid,
1692 com.android.internal.R.styleable.TextAppearance);
1693
1694 int color;
1695 ColorStateList colors;
1696 int ts;
1697
1698 color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
1699 if (color != 0) {
1700 setHighlightColor(color);
1701 }
1702
1703 colors = appearance.getColorStateList(com.android.internal.R.styleable.
1704 TextAppearance_textColor);
1705 if (colors != null) {
1706 setTextColor(colors);
1707 }
1708
1709 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
1710 TextAppearance_textSize, 0);
1711 if (ts != 0) {
1712 setRawTextSize(ts);
1713 }
1714
1715 colors = appearance.getColorStateList(com.android.internal.R.styleable.
1716 TextAppearance_textColorHint);
1717 if (colors != null) {
1718 setHintTextColor(colors);
1719 }
1720
1721 colors = appearance.getColorStateList(com.android.internal.R.styleable.
1722 TextAppearance_textColorLink);
1723 if (colors != null) {
1724 setLinkTextColor(colors);
1725 }
1726
1727 int typefaceIndex, styleIndex;
1728
1729 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
1730 TextAppearance_typeface, -1);
1731 styleIndex = appearance.getInt(com.android.internal.R.styleable.
1732 TextAppearance_textStyle, -1);
1733
1734 setTypefaceByIndex(typefaceIndex, styleIndex);
Adam Powellbe4d68e2010-10-08 18:16:34 -07001735
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001736 appearance.recycle();
1737 }
1738
1739 /**
1740 * @return the size (in pixels) of the default text size in this TextView.
1741 */
1742 public float getTextSize() {
1743 return mTextPaint.getTextSize();
1744 }
1745
1746 /**
1747 * Set the default text size to the given value, interpreted as "scaled
1748 * pixel" units. This size is adjusted based on the current density and
1749 * user font size preference.
1750 *
1751 * @param size The scaled pixel size.
1752 *
1753 * @attr ref android.R.styleable#TextView_textSize
1754 */
1755 @android.view.RemotableViewMethod
1756 public void setTextSize(float size) {
1757 setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
1758 }
1759
1760 /**
1761 * Set the default text size to a given unit and value. See {@link
1762 * TypedValue} for the possible dimension units.
1763 *
1764 * @param unit The desired dimension unit.
1765 * @param size The desired size in the given units.
1766 *
1767 * @attr ref android.R.styleable#TextView_textSize
1768 */
1769 public void setTextSize(int unit, float size) {
1770 Context c = getContext();
1771 Resources r;
1772
1773 if (c == null)
1774 r = Resources.getSystem();
1775 else
1776 r = c.getResources();
1777
1778 setRawTextSize(TypedValue.applyDimension(
1779 unit, size, r.getDisplayMetrics()));
1780 }
1781
1782 private void setRawTextSize(float size) {
1783 if (size != mTextPaint.getTextSize()) {
1784 mTextPaint.setTextSize(size);
1785
1786 if (mLayout != null) {
1787 nullLayouts();
1788 requestLayout();
1789 invalidate();
1790 }
1791 }
1792 }
1793
1794 /**
1795 * @return the extent by which text is currently being stretched
1796 * horizontally. This will usually be 1.
1797 */
1798 public float getTextScaleX() {
1799 return mTextPaint.getTextScaleX();
1800 }
1801
1802 /**
1803 * Sets the extent by which text should be stretched horizontally.
1804 *
1805 * @attr ref android.R.styleable#TextView_textScaleX
1806 */
1807 @android.view.RemotableViewMethod
1808 public void setTextScaleX(float size) {
1809 if (size != mTextPaint.getTextScaleX()) {
Romain Guy939151f2009-04-08 14:22:40 -07001810 mUserSetTextScaleX = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001811 mTextPaint.setTextScaleX(size);
1812
1813 if (mLayout != null) {
1814 nullLayouts();
1815 requestLayout();
1816 invalidate();
1817 }
1818 }
1819 }
1820
1821 /**
1822 * Sets the typeface and style in which the text should be displayed.
1823 * Note that not all Typeface families actually have bold and italic
1824 * variants, so you may need to use
1825 * {@link #setTypeface(Typeface, int)} to get the appearance
1826 * that you actually want.
1827 *
1828 * @attr ref android.R.styleable#TextView_typeface
1829 * @attr ref android.R.styleable#TextView_textStyle
1830 */
1831 public void setTypeface(Typeface tf) {
1832 if (mTextPaint.getTypeface() != tf) {
1833 mTextPaint.setTypeface(tf);
1834
1835 if (mLayout != null) {
1836 nullLayouts();
1837 requestLayout();
1838 invalidate();
1839 }
1840 }
1841 }
1842
1843 /**
1844 * @return the current typeface and style in which the text is being
1845 * displayed.
1846 */
1847 public Typeface getTypeface() {
1848 return mTextPaint.getTypeface();
1849 }
1850
1851 /**
1852 * Sets the text color for all the states (normal, selected,
1853 * focused) to be this color.
1854 *
1855 * @attr ref android.R.styleable#TextView_textColor
1856 */
1857 @android.view.RemotableViewMethod
1858 public void setTextColor(int color) {
1859 mTextColor = ColorStateList.valueOf(color);
1860 updateTextColors();
1861 }
1862
1863 /**
1864 * Sets the text color.
1865 *
1866 * @attr ref android.R.styleable#TextView_textColor
1867 */
1868 public void setTextColor(ColorStateList colors) {
1869 if (colors == null) {
1870 throw new NullPointerException();
1871 }
1872
1873 mTextColor = colors;
1874 updateTextColors();
1875 }
1876
1877 /**
1878 * Return the set of text colors.
1879 *
1880 * @return Returns the set of text colors.
1881 */
1882 public final ColorStateList getTextColors() {
1883 return mTextColor;
1884 }
1885
1886 /**
1887 * <p>Return the current color selected for normal text.</p>
1888 *
1889 * @return Returns the current text color.
1890 */
1891 public final int getCurrentTextColor() {
1892 return mCurTextColor;
1893 }
1894
1895 /**
1896 * Sets the color used to display the selection highlight.
1897 *
1898 * @attr ref android.R.styleable#TextView_textColorHighlight
1899 */
1900 @android.view.RemotableViewMethod
1901 public void setHighlightColor(int color) {
1902 if (mHighlightColor != color) {
1903 mHighlightColor = color;
1904 invalidate();
1905 }
1906 }
1907
1908 /**
1909 * Gives the text a shadow of the specified radius and color, the specified
1910 * distance from its normal position.
1911 *
1912 * @attr ref android.R.styleable#TextView_shadowColor
1913 * @attr ref android.R.styleable#TextView_shadowDx
1914 * @attr ref android.R.styleable#TextView_shadowDy
1915 * @attr ref android.R.styleable#TextView_shadowRadius
1916 */
1917 public void setShadowLayer(float radius, float dx, float dy, int color) {
1918 mTextPaint.setShadowLayer(radius, dx, dy, color);
1919
1920 mShadowRadius = radius;
1921 mShadowDx = dx;
1922 mShadowDy = dy;
1923
1924 invalidate();
1925 }
1926
1927 /**
1928 * @return the base paint used for the text. Please use this only to
1929 * consult the Paint's properties and not to change them.
1930 */
1931 public TextPaint getPaint() {
1932 return mTextPaint;
1933 }
1934
1935 /**
1936 * Sets the autolink mask of the text. See {@link
1937 * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1938 * possible values.
1939 *
1940 * @attr ref android.R.styleable#TextView_autoLink
1941 */
1942 @android.view.RemotableViewMethod
1943 public final void setAutoLinkMask(int mask) {
1944 mAutoLinkMask = mask;
1945 }
1946
1947 /**
1948 * Sets whether the movement method will automatically be set to
1949 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1950 * set to nonzero and links are detected in {@link #setText}.
1951 * The default is true.
1952 *
1953 * @attr ref android.R.styleable#TextView_linksClickable
1954 */
1955 @android.view.RemotableViewMethod
1956 public final void setLinksClickable(boolean whether) {
1957 mLinksClickable = whether;
1958 }
1959
1960 /**
1961 * Returns whether the movement method will automatically be set to
1962 * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1963 * set to nonzero and links are detected in {@link #setText}.
1964 * The default is true.
1965 *
1966 * @attr ref android.R.styleable#TextView_linksClickable
1967 */
1968 public final boolean getLinksClickable() {
1969 return mLinksClickable;
1970 }
1971
1972 /**
1973 * Returns the list of URLSpans attached to the text
1974 * (by {@link Linkify} or otherwise) if any. You can call
1975 * {@link URLSpan#getURL} on them to find where they link to
1976 * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
1977 * to find the region of the text they are attached to.
1978 */
1979 public URLSpan[] getUrls() {
1980 if (mText instanceof Spanned) {
1981 return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
1982 } else {
1983 return new URLSpan[0];
1984 }
1985 }
1986
1987 /**
1988 * Sets the color of the hint text.
1989 *
1990 * @attr ref android.R.styleable#TextView_textColorHint
1991 */
1992 @android.view.RemotableViewMethod
1993 public final void setHintTextColor(int color) {
1994 mHintTextColor = ColorStateList.valueOf(color);
1995 updateTextColors();
1996 }
1997
1998 /**
1999 * Sets the color of the hint text.
2000 *
2001 * @attr ref android.R.styleable#TextView_textColorHint
2002 */
2003 public final void setHintTextColor(ColorStateList colors) {
2004 mHintTextColor = colors;
2005 updateTextColors();
2006 }
2007
2008 /**
2009 * <p>Return the color used to paint the hint text.</p>
2010 *
2011 * @return Returns the list of hint text colors.
2012 */
2013 public final ColorStateList getHintTextColors() {
2014 return mHintTextColor;
2015 }
2016
2017 /**
2018 * <p>Return the current color selected to paint the hint text.</p>
2019 *
2020 * @return Returns the current hint text color.
2021 */
2022 public final int getCurrentHintTextColor() {
2023 return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2024 }
2025
2026 /**
2027 * Sets the color of links in the text.
2028 *
2029 * @attr ref android.R.styleable#TextView_textColorLink
2030 */
2031 @android.view.RemotableViewMethod
2032 public final void setLinkTextColor(int color) {
2033 mLinkTextColor = ColorStateList.valueOf(color);
2034 updateTextColors();
2035 }
2036
2037 /**
2038 * Sets the color of links in the text.
2039 *
2040 * @attr ref android.R.styleable#TextView_textColorLink
2041 */
2042 public final void setLinkTextColor(ColorStateList colors) {
2043 mLinkTextColor = colors;
2044 updateTextColors();
2045 }
2046
2047 /**
2048 * <p>Returns the color used to paint links in the text.</p>
2049 *
2050 * @return Returns the list of link text colors.
2051 */
2052 public final ColorStateList getLinkTextColors() {
2053 return mLinkTextColor;
2054 }
2055
2056 /**
2057 * Sets the horizontal alignment of the text and the
2058 * vertical gravity that will be used when there is extra space
2059 * in the TextView beyond what is required for the text itself.
2060 *
2061 * @see android.view.Gravity
2062 * @attr ref android.R.styleable#TextView_gravity
2063 */
2064 public void setGravity(int gravity) {
2065 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
2066 gravity |= Gravity.LEFT;
2067 }
2068 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2069 gravity |= Gravity.TOP;
2070 }
2071
2072 boolean newLayout = false;
2073
2074 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) !=
2075 (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
2076 newLayout = true;
2077 }
2078
2079 if (gravity != mGravity) {
2080 invalidate();
2081 }
2082
2083 mGravity = gravity;
2084
2085 if (mLayout != null && newLayout) {
2086 // XXX this is heavy-handed because no actual content changes.
2087 int want = mLayout.getWidth();
2088 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2089
2090 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2091 mRight - mLeft - getCompoundPaddingLeft() -
2092 getCompoundPaddingRight(), true);
2093 }
2094 }
2095
2096 /**
2097 * Returns the horizontal and vertical alignment of this TextView.
2098 *
2099 * @see android.view.Gravity
2100 * @attr ref android.R.styleable#TextView_gravity
2101 */
2102 public int getGravity() {
2103 return mGravity;
2104 }
2105
2106 /**
2107 * @return the flags on the Paint being used to display the text.
2108 * @see Paint#getFlags
2109 */
2110 public int getPaintFlags() {
2111 return mTextPaint.getFlags();
2112 }
2113
2114 /**
2115 * Sets flags on the Paint being used to display the text and
2116 * reflows the text if they are different from the old flags.
2117 * @see Paint#setFlags
2118 */
2119 @android.view.RemotableViewMethod
2120 public void setPaintFlags(int flags) {
2121 if (mTextPaint.getFlags() != flags) {
2122 mTextPaint.setFlags(flags);
2123
2124 if (mLayout != null) {
2125 nullLayouts();
2126 requestLayout();
2127 invalidate();
2128 }
2129 }
2130 }
2131
2132 /**
2133 * Sets whether the text should be allowed to be wider than the
2134 * View is. If false, it will be wrapped to the width of the View.
2135 *
2136 * @attr ref android.R.styleable#TextView_scrollHorizontally
2137 */
2138 public void setHorizontallyScrolling(boolean whether) {
2139 mHorizontallyScrolling = whether;
2140
2141 if (mLayout != null) {
2142 nullLayouts();
2143 requestLayout();
2144 invalidate();
2145 }
2146 }
2147
2148 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002149 * Makes the TextView at least this many lines tall.
2150 *
2151 * Setting this value overrides any other (minimum) height setting. A single line TextView will
2152 * set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002153 *
2154 * @attr ref android.R.styleable#TextView_minLines
2155 */
2156 @android.view.RemotableViewMethod
2157 public void setMinLines(int minlines) {
2158 mMinimum = minlines;
2159 mMinMode = LINES;
2160
2161 requestLayout();
2162 invalidate();
2163 }
2164
2165 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002166 * Makes the TextView at least this many pixels tall.
2167 *
2168 * Setting this value overrides any other (minimum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002169 *
2170 * @attr ref android.R.styleable#TextView_minHeight
2171 */
2172 @android.view.RemotableViewMethod
2173 public void setMinHeight(int minHeight) {
2174 mMinimum = minHeight;
2175 mMinMode = PIXELS;
2176
2177 requestLayout();
2178 invalidate();
2179 }
2180
2181 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002182 * Makes the TextView at most this many lines tall.
2183 *
2184 * Setting this value overrides any other (maximum) height setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002185 *
2186 * @attr ref android.R.styleable#TextView_maxLines
2187 */
2188 @android.view.RemotableViewMethod
2189 public void setMaxLines(int maxlines) {
2190 mMaximum = maxlines;
2191 mMaxMode = LINES;
2192
2193 requestLayout();
2194 invalidate();
2195 }
2196
2197 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002198 * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the
2199 * {@link #setMaxLines(int)} method.
2200 *
2201 * Setting this value overrides any other (maximum) number of lines setting.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002202 *
2203 * @attr ref android.R.styleable#TextView_maxHeight
2204 */
2205 @android.view.RemotableViewMethod
2206 public void setMaxHeight(int maxHeight) {
2207 mMaximum = maxHeight;
2208 mMaxMode = PIXELS;
2209
2210 requestLayout();
2211 invalidate();
2212 }
2213
2214 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002215 * Makes the TextView exactly this many lines tall.
2216 *
2217 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2218 * height setting. A single line TextView will set this value to 1.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002219 *
2220 * @attr ref android.R.styleable#TextView_lines
2221 */
2222 @android.view.RemotableViewMethod
2223 public void setLines(int lines) {
2224 mMaximum = mMinimum = lines;
2225 mMaxMode = mMinMode = LINES;
2226
2227 requestLayout();
2228 invalidate();
2229 }
2230
2231 /**
2232 * Makes the TextView exactly this many pixels tall.
2233 * You could do the same thing by specifying this number in the
2234 * LayoutParams.
2235 *
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08002236 * Note that setting this value overrides any other (minimum / maximum) number of lines or
2237 * height setting.
2238 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002239 * @attr ref android.R.styleable#TextView_height
2240 */
2241 @android.view.RemotableViewMethod
2242 public void setHeight(int pixels) {
2243 mMaximum = mMinimum = pixels;
2244 mMaxMode = mMinMode = PIXELS;
2245
2246 requestLayout();
2247 invalidate();
2248 }
2249
2250 /**
2251 * Makes the TextView at least this many ems wide
2252 *
2253 * @attr ref android.R.styleable#TextView_minEms
2254 */
2255 @android.view.RemotableViewMethod
2256 public void setMinEms(int minems) {
2257 mMinWidth = minems;
2258 mMinWidthMode = EMS;
2259
2260 requestLayout();
2261 invalidate();
2262 }
2263
2264 /**
2265 * Makes the TextView at least this many pixels wide
2266 *
2267 * @attr ref android.R.styleable#TextView_minWidth
2268 */
2269 @android.view.RemotableViewMethod
2270 public void setMinWidth(int minpixels) {
2271 mMinWidth = minpixels;
2272 mMinWidthMode = PIXELS;
2273
2274 requestLayout();
2275 invalidate();
2276 }
2277
2278 /**
2279 * Makes the TextView at most this many ems wide
2280 *
2281 * @attr ref android.R.styleable#TextView_maxEms
2282 */
2283 @android.view.RemotableViewMethod
2284 public void setMaxEms(int maxems) {
2285 mMaxWidth = maxems;
2286 mMaxWidthMode = EMS;
2287
2288 requestLayout();
2289 invalidate();
2290 }
2291
2292 /**
2293 * Makes the TextView at most this many pixels wide
2294 *
2295 * @attr ref android.R.styleable#TextView_maxWidth
2296 */
2297 @android.view.RemotableViewMethod
2298 public void setMaxWidth(int maxpixels) {
2299 mMaxWidth = maxpixels;
2300 mMaxWidthMode = PIXELS;
2301
2302 requestLayout();
2303 invalidate();
2304 }
2305
2306 /**
2307 * Makes the TextView exactly this many ems wide
2308 *
2309 * @attr ref android.R.styleable#TextView_ems
2310 */
2311 @android.view.RemotableViewMethod
2312 public void setEms(int ems) {
2313 mMaxWidth = mMinWidth = ems;
2314 mMaxWidthMode = mMinWidthMode = EMS;
2315
2316 requestLayout();
2317 invalidate();
2318 }
2319
2320 /**
2321 * Makes the TextView exactly this many pixels wide.
2322 * You could do the same thing by specifying this number in the
2323 * LayoutParams.
2324 *
2325 * @attr ref android.R.styleable#TextView_width
2326 */
2327 @android.view.RemotableViewMethod
2328 public void setWidth(int pixels) {
2329 mMaxWidth = mMinWidth = pixels;
2330 mMaxWidthMode = mMinWidthMode = PIXELS;
2331
2332 requestLayout();
2333 invalidate();
2334 }
2335
2336
2337 /**
2338 * Sets line spacing for this TextView. Each line will have its height
2339 * multiplied by <code>mult</code> and have <code>add</code> added to it.
2340 *
2341 * @attr ref android.R.styleable#TextView_lineSpacingExtra
2342 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2343 */
2344 public void setLineSpacing(float add, float mult) {
2345 mSpacingMult = mult;
2346 mSpacingAdd = add;
2347
2348 if (mLayout != null) {
2349 nullLayouts();
2350 requestLayout();
2351 invalidate();
2352 }
2353 }
2354
2355 /**
2356 * Convenience method: Append the specified text to the TextView's
2357 * display buffer, upgrading it to BufferType.EDITABLE if it was
2358 * not already editable.
2359 */
2360 public final void append(CharSequence text) {
2361 append(text, 0, text.length());
2362 }
2363
2364 /**
2365 * Convenience method: Append the specified text slice to the TextView's
2366 * display buffer, upgrading it to BufferType.EDITABLE if it was
2367 * not already editable.
2368 */
2369 public void append(CharSequence text, int start, int end) {
2370 if (!(mText instanceof Editable)) {
2371 setText(mText, BufferType.EDITABLE);
2372 }
2373
2374 ((Editable) mText).append(text, start, end);
2375 }
2376
2377 private void updateTextColors() {
2378 boolean inval = false;
2379 int color = mTextColor.getColorForState(getDrawableState(), 0);
2380 if (color != mCurTextColor) {
2381 mCurTextColor = color;
2382 inval = true;
2383 }
2384 if (mLinkTextColor != null) {
2385 color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2386 if (color != mTextPaint.linkColor) {
2387 mTextPaint.linkColor = color;
2388 inval = true;
2389 }
2390 }
2391 if (mHintTextColor != null) {
2392 color = mHintTextColor.getColorForState(getDrawableState(), 0);
2393 if (color != mCurHintTextColor && mText.length() == 0) {
2394 mCurHintTextColor = color;
2395 inval = true;
2396 }
2397 }
2398 if (inval) {
2399 invalidate();
2400 }
2401 }
2402
2403 @Override
2404 protected void drawableStateChanged() {
2405 super.drawableStateChanged();
2406 if (mTextColor != null && mTextColor.isStateful()
2407 || (mHintTextColor != null && mHintTextColor.isStateful())
2408 || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2409 updateTextColors();
2410 }
2411
2412 final Drawables dr = mDrawables;
2413 if (dr != null) {
2414 int[] state = getDrawableState();
2415 if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2416 dr.mDrawableTop.setState(state);
2417 }
2418 if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2419 dr.mDrawableBottom.setState(state);
2420 }
2421 if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2422 dr.mDrawableLeft.setState(state);
2423 }
2424 if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2425 dr.mDrawableRight.setState(state);
2426 }
2427 }
2428 }
2429
2430 /**
2431 * User interface state that is stored by TextView for implementing
2432 * {@link View#onSaveInstanceState}.
2433 */
2434 public static class SavedState extends BaseSavedState {
2435 int selStart;
2436 int selEnd;
2437 CharSequence text;
2438 boolean frozenWithFocus;
The Android Open Source Project4df24232009-03-05 14:34:35 -08002439 CharSequence error;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002440
2441 SavedState(Parcelable superState) {
2442 super(superState);
2443 }
2444
2445 @Override
2446 public void writeToParcel(Parcel out, int flags) {
2447 super.writeToParcel(out, flags);
2448 out.writeInt(selStart);
2449 out.writeInt(selEnd);
2450 out.writeInt(frozenWithFocus ? 1 : 0);
2451 TextUtils.writeToParcel(text, out, flags);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002452
2453 if (error == null) {
2454 out.writeInt(0);
2455 } else {
2456 out.writeInt(1);
2457 TextUtils.writeToParcel(error, out, flags);
2458 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002459 }
2460
2461 @Override
2462 public String toString() {
2463 String str = "TextView.SavedState{"
2464 + Integer.toHexString(System.identityHashCode(this))
2465 + " start=" + selStart + " end=" + selEnd;
2466 if (text != null) {
2467 str += " text=" + text;
2468 }
2469 return str + "}";
2470 }
2471
Gilles Debunnee15b3582010-06-16 15:17:21 -07002472 @SuppressWarnings("hiding")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002473 public static final Parcelable.Creator<SavedState> CREATOR
2474 = new Parcelable.Creator<SavedState>() {
2475 public SavedState createFromParcel(Parcel in) {
2476 return new SavedState(in);
2477 }
2478
2479 public SavedState[] newArray(int size) {
2480 return new SavedState[size];
2481 }
2482 };
2483
2484 private SavedState(Parcel in) {
2485 super(in);
2486 selStart = in.readInt();
2487 selEnd = in.readInt();
2488 frozenWithFocus = (in.readInt() != 0);
2489 text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
The Android Open Source Project4df24232009-03-05 14:34:35 -08002490
2491 if (in.readInt() != 0) {
2492 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2493 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002494 }
2495 }
2496
2497 @Override
2498 public Parcelable onSaveInstanceState() {
2499 Parcelable superState = super.onSaveInstanceState();
2500
2501 // Save state if we are forced to
2502 boolean save = mFreezesText;
2503 int start = 0;
2504 int end = 0;
2505
2506 if (mText != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07002507 start = getSelectionStart();
2508 end = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002509 if (start >= 0 || end >= 0) {
2510 // Or save state if there is a selection
2511 save = true;
2512 }
2513 }
2514
2515 if (save) {
2516 SavedState ss = new SavedState(superState);
2517 // XXX Should also save the current scroll position!
2518 ss.selStart = start;
2519 ss.selEnd = end;
2520
2521 if (mText instanceof Spanned) {
2522 /*
2523 * Calling setText() strips off any ChangeWatchers;
2524 * strip them now to avoid leaking references.
2525 * But do it to a copy so that if there are any
2526 * further changes to the text of this view, it
2527 * won't get into an inconsistent state.
2528 */
2529
2530 Spannable sp = new SpannableString(mText);
2531
2532 for (ChangeWatcher cw :
2533 sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
2534 sp.removeSpan(cw);
2535 }
2536
2537 ss.text = sp;
2538 } else {
2539 ss.text = mText.toString();
2540 }
2541
2542 if (isFocused() && start >= 0 && end >= 0) {
2543 ss.frozenWithFocus = true;
2544 }
2545
The Android Open Source Project4df24232009-03-05 14:34:35 -08002546 ss.error = mError;
2547
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002548 return ss;
2549 }
2550
2551 return superState;
2552 }
2553
2554 @Override
2555 public void onRestoreInstanceState(Parcelable state) {
2556 if (!(state instanceof SavedState)) {
2557 super.onRestoreInstanceState(state);
2558 return;
2559 }
2560
2561 SavedState ss = (SavedState)state;
2562 super.onRestoreInstanceState(ss.getSuperState());
2563
2564 // XXX restore buffer type too, as well as lots of other stuff
2565 if (ss.text != null) {
2566 setText(ss.text);
2567 }
2568
2569 if (ss.selStart >= 0 && ss.selEnd >= 0) {
2570 if (mText instanceof Spannable) {
2571 int len = mText.length();
2572
2573 if (ss.selStart > len || ss.selEnd > len) {
2574 String restored = "";
2575
2576 if (ss.text != null) {
2577 restored = "(restored) ";
2578 }
2579
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07002580 Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002581 "/" + ss.selEnd + " out of range for " + restored +
2582 "text " + mText);
2583 } else {
2584 Selection.setSelection((Spannable) mText, ss.selStart,
2585 ss.selEnd);
2586
2587 if (ss.frozenWithFocus) {
2588 mFrozenWithFocus = true;
2589 }
2590 }
2591 }
2592 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08002593
2594 if (ss.error != null) {
Romain Guy9bc9fa12009-07-21 16:57:29 -07002595 final CharSequence error = ss.error;
2596 // Display the error later, after the first layout pass
2597 post(new Runnable() {
2598 public void run() {
2599 setError(error);
2600 }
2601 });
The Android Open Source Project4df24232009-03-05 14:34:35 -08002602 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002603 }
2604
2605 /**
2606 * Control whether this text view saves its entire text contents when
2607 * freezing to an icicle, in addition to dynamic state such as cursor
2608 * position. By default this is false, not saving the text. Set to true
2609 * if the text in the text view is not being saved somewhere else in
2610 * persistent storage (such as in a content provider) so that if the
2611 * view is later thawed the user will not lose their data.
2612 *
2613 * @param freezesText Controls whether a frozen icicle should include the
2614 * entire text data: true to include it, false to not.
2615 *
2616 * @attr ref android.R.styleable#TextView_freezesText
2617 */
2618 @android.view.RemotableViewMethod
2619 public void setFreezesText(boolean freezesText) {
2620 mFreezesText = freezesText;
2621 }
2622
2623 /**
2624 * Return whether this text view is including its entire text contents
2625 * in frozen icicles.
2626 *
2627 * @return Returns true if text is included, false if it isn't.
2628 *
2629 * @see #setFreezesText
2630 */
2631 public boolean getFreezesText() {
2632 return mFreezesText;
2633 }
2634
2635 ///////////////////////////////////////////////////////////////////////////
2636
2637 /**
2638 * Sets the Factory used to create new Editables.
2639 */
2640 public final void setEditableFactory(Editable.Factory factory) {
2641 mEditableFactory = factory;
2642 setText(mText);
2643 }
2644
2645 /**
2646 * Sets the Factory used to create new Spannables.
2647 */
2648 public final void setSpannableFactory(Spannable.Factory factory) {
2649 mSpannableFactory = factory;
2650 setText(mText);
2651 }
2652
2653 /**
2654 * Sets the string value of the TextView. TextView <em>does not</em> accept
2655 * HTML-like formatting, which you can do with text strings in XML resource files.
2656 * To style your strings, attach android.text.style.* objects to a
2657 * {@link android.text.SpannableString SpannableString}, or see the
2658 * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
2659 * Available Resource Types</a> documentation for an example of setting
2660 * formatted text in the XML resource file.
2661 *
2662 * @attr ref android.R.styleable#TextView_text
2663 */
2664 @android.view.RemotableViewMethod
2665 public final void setText(CharSequence text) {
2666 setText(text, mBufferType);
2667 }
2668
2669 /**
2670 * Like {@link #setText(CharSequence)},
2671 * except that the cursor position (if any) is retained in the new text.
2672 *
2673 * @param text The new text to place in the text view.
2674 *
2675 * @see #setText(CharSequence)
2676 */
2677 @android.view.RemotableViewMethod
2678 public final void setTextKeepState(CharSequence text) {
2679 setTextKeepState(text, mBufferType);
2680 }
2681
2682 /**
2683 * Sets the text that this TextView is to display (see
2684 * {@link #setText(CharSequence)}) and also sets whether it is stored
2685 * in a styleable/spannable buffer and whether it is editable.
2686 *
2687 * @attr ref android.R.styleable#TextView_text
2688 * @attr ref android.R.styleable#TextView_bufferType
2689 */
2690 public void setText(CharSequence text, BufferType type) {
2691 setText(text, type, true, 0);
2692
2693 if (mCharWrapper != null) {
2694 mCharWrapper.mChars = null;
2695 }
2696 }
2697
2698 private void setText(CharSequence text, BufferType type,
2699 boolean notifyBefore, int oldlen) {
2700 if (text == null) {
2701 text = "";
2702 }
2703
Romain Guy939151f2009-04-08 14:22:40 -07002704 if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
2705
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002706 if (text instanceof Spanned &&
2707 ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
2708 setHorizontalFadingEdgeEnabled(true);
2709 setEllipsize(TextUtils.TruncateAt.MARQUEE);
2710 }
2711
2712 int n = mFilters.length;
2713 for (int i = 0; i < n; i++) {
2714 CharSequence out = mFilters[i].filter(text, 0, text.length(),
2715 EMPTY_SPANNED, 0, 0);
2716 if (out != null) {
2717 text = out;
2718 }
2719 }
2720
2721 if (notifyBefore) {
2722 if (mText != null) {
2723 oldlen = mText.length();
2724 sendBeforeTextChanged(mText, 0, oldlen, text.length());
2725 } else {
2726 sendBeforeTextChanged("", 0, 0, text.length());
2727 }
2728 }
2729
2730 boolean needEditableForNotification = false;
2731
2732 if (mListeners != null && mListeners.size() != 0) {
2733 needEditableForNotification = true;
2734 }
2735
2736 if (type == BufferType.EDITABLE || mInput != null ||
2737 needEditableForNotification) {
2738 Editable t = mEditableFactory.newEditable(text);
2739 text = t;
2740 setFilters(t, mFilters);
2741 InputMethodManager imm = InputMethodManager.peekInstance();
2742 if (imm != null) imm.restartInput(this);
2743 } else if (type == BufferType.SPANNABLE || mMovement != null) {
2744 text = mSpannableFactory.newSpannable(text);
2745 } else if (!(text instanceof CharWrapper)) {
2746 text = TextUtils.stringOrSpannedString(text);
2747 }
2748
2749 if (mAutoLinkMask != 0) {
2750 Spannable s2;
2751
2752 if (type == BufferType.EDITABLE || text instanceof Spannable) {
2753 s2 = (Spannable) text;
2754 } else {
2755 s2 = mSpannableFactory.newSpannable(text);
2756 }
2757
2758 if (Linkify.addLinks(s2, mAutoLinkMask)) {
2759 text = s2;
2760 type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
2761
2762 /*
2763 * We must go ahead and set the text before changing the
2764 * movement method, because setMovementMethod() may call
2765 * setText() again to try to upgrade the buffer type.
2766 */
2767 mText = text;
2768
Gilles Debunnecbcb3452010-12-17 15:31:02 -08002769 // Do not change the movement method for text that support text selection as it
2770 // would prevent an arbitrary cursor displacement.
2771 final boolean hasTextSelection = this instanceof EditText || mTextIsSelectable;
2772 if (mLinksClickable && !hasTextSelection) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002773 setMovementMethod(LinkMovementMethod.getInstance());
2774 }
2775 }
2776 }
2777
2778 mBufferType = type;
2779 mText = text;
2780
2781 if (mTransformation == null)
2782 mTransformed = text;
2783 else
2784 mTransformed = mTransformation.getTransformation(text, this);
2785
2786 final int textLength = text.length();
2787
2788 if (text instanceof Spannable) {
2789 Spannable sp = (Spannable) text;
2790
2791 // Remove any ChangeWatchers that might have come
2792 // from other TextViews.
2793 final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
2794 final int count = watchers.length;
2795 for (int i = 0; i < count; i++)
2796 sp.removeSpan(watchers[i]);
2797
2798 if (mChangeWatcher == null)
2799 mChangeWatcher = new ChangeWatcher();
2800
2801 sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
2802 (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
2803
2804 if (mInput != null) {
2805 sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2806 }
2807
2808 if (mTransformation != null) {
2809 sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2810
2811 }
2812
2813 if (mMovement != null) {
2814 mMovement.initialize(this, (Spannable) text);
2815
2816 /*
2817 * Initializing the movement method will have set the
2818 * selection, so reset mSelectionMoved to keep that from
2819 * interfering with the normal on-focus selection-setting.
2820 */
2821 mSelectionMoved = false;
2822 }
2823 }
2824
2825 if (mLayout != null) {
2826 checkForRelayout();
2827 }
2828
2829 sendOnTextChanged(text, 0, oldlen, textLength);
2830 onTextChanged(text, 0, oldlen, textLength);
2831
2832 if (needEditableForNotification) {
2833 sendAfterTextChanged((Editable) text);
2834 }
Gilles Debunne05336272010-07-09 20:13:45 -07002835
Gilles Debunnebaaace52010-10-01 15:47:13 -07002836 // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
Gilles Debunnef788a9f2010-07-22 10:17:23 -07002837 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002838 }
2839
2840 /**
2841 * Sets the TextView to display the specified slice of the specified
2842 * char array. You must promise that you will not change the contents
2843 * of the array except for right before another call to setText(),
2844 * since the TextView has no way to know that the text
2845 * has changed and that it needs to invalidate and re-layout.
2846 */
2847 public final void setText(char[] text, int start, int len) {
2848 int oldlen = 0;
2849
2850 if (start < 0 || len < 0 || start + len > text.length) {
2851 throw new IndexOutOfBoundsException(start + ", " + len);
2852 }
2853
2854 /*
2855 * We must do the before-notification here ourselves because if
2856 * the old text is a CharWrapper we destroy it before calling
2857 * into the normal path.
2858 */
2859 if (mText != null) {
2860 oldlen = mText.length();
2861 sendBeforeTextChanged(mText, 0, oldlen, len);
2862 } else {
2863 sendBeforeTextChanged("", 0, 0, len);
2864 }
2865
2866 if (mCharWrapper == null) {
2867 mCharWrapper = new CharWrapper(text, start, len);
2868 } else {
2869 mCharWrapper.set(text, start, len);
2870 }
2871
2872 setText(mCharWrapper, mBufferType, false, oldlen);
2873 }
2874
2875 private static class CharWrapper
2876 implements CharSequence, GetChars, GraphicsOperations {
2877 private char[] mChars;
2878 private int mStart, mLength;
2879
2880 public CharWrapper(char[] chars, int start, int len) {
2881 mChars = chars;
2882 mStart = start;
2883 mLength = len;
2884 }
2885
2886 /* package */ void set(char[] chars, int start, int len) {
2887 mChars = chars;
2888 mStart = start;
2889 mLength = len;
2890 }
2891
2892 public int length() {
2893 return mLength;
2894 }
2895
2896 public char charAt(int off) {
2897 return mChars[off + mStart];
2898 }
2899
Gilles Debunnee15b3582010-06-16 15:17:21 -07002900 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002901 public String toString() {
2902 return new String(mChars, mStart, mLength);
2903 }
2904
2905 public CharSequence subSequence(int start, int end) {
2906 if (start < 0 || end < 0 || start > mLength || end > mLength) {
2907 throw new IndexOutOfBoundsException(start + ", " + end);
2908 }
2909
2910 return new String(mChars, start + mStart, end - start);
2911 }
2912
2913 public void getChars(int start, int end, char[] buf, int off) {
2914 if (start < 0 || end < 0 || start > mLength || end > mLength) {
2915 throw new IndexOutOfBoundsException(start + ", " + end);
2916 }
2917
2918 System.arraycopy(mChars, start + mStart, buf, off, end - start);
2919 }
2920
2921 public void drawText(Canvas c, int start, int end,
2922 float x, float y, Paint p) {
2923 c.drawText(mChars, start + mStart, end - start, x, y, p);
2924 }
2925
Doug Feltf47d7402010-04-21 16:01:52 -07002926 public void drawTextRun(Canvas c, int start, int end,
Doug Felt0c702b82010-05-14 10:55:42 -07002927 int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
2928 int count = end - start;
2929 int contextCount = contextEnd - contextStart;
2930 c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
2931 contextCount, x, y, flags, p);
Doug Feltf47d7402010-04-21 16:01:52 -07002932 }
2933
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002934 public float measureText(int start, int end, Paint p) {
2935 return p.measureText(mChars, start + mStart, end - start);
2936 }
2937
2938 public int getTextWidths(int start, int end, float[] widths, Paint p) {
2939 return p.getTextWidths(mChars, start + mStart, end - start, widths);
2940 }
Doug Felt0c702b82010-05-14 10:55:42 -07002941
2942 public float getTextRunAdvances(int start, int end, int contextStart,
2943 int contextEnd, int flags, float[] advances, int advancesIndex,
2944 Paint p) {
2945 int count = end - start;
2946 int contextCount = contextEnd - contextStart;
2947 return p.getTextRunAdvances(mChars, start + mStart, count,
2948 contextStart + mStart, contextCount, flags, advances,
2949 advancesIndex);
2950 }
2951
2952 public int getTextRunCursor(int contextStart, int contextEnd, int flags,
2953 int offset, int cursorOpt, Paint p) {
2954 int contextCount = contextEnd - contextStart;
2955 return p.getTextRunCursor(mChars, contextStart + mStart,
2956 contextCount, flags, offset + mStart, cursorOpt);
2957 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002958 }
2959
2960 /**
2961 * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
2962 * except that the cursor position (if any) is retained in the new text.
2963 *
2964 * @see #setText(CharSequence, android.widget.TextView.BufferType)
2965 */
2966 public final void setTextKeepState(CharSequence text, BufferType type) {
2967 int start = getSelectionStart();
2968 int end = getSelectionEnd();
2969 int len = text.length();
2970
2971 setText(text, type);
2972
2973 if (start >= 0 || end >= 0) {
2974 if (mText instanceof Spannable) {
2975 Selection.setSelection((Spannable) mText,
2976 Math.max(0, Math.min(start, len)),
2977 Math.max(0, Math.min(end, len)));
2978 }
2979 }
2980 }
2981
2982 @android.view.RemotableViewMethod
2983 public final void setText(int resid) {
2984 setText(getContext().getResources().getText(resid));
2985 }
2986
2987 public final void setText(int resid, BufferType type) {
2988 setText(getContext().getResources().getText(resid), type);
2989 }
2990
2991 /**
2992 * Sets the text to be displayed when the text of the TextView is empty.
2993 * Null means to use the normal empty text. The hint does not currently
2994 * participate in determining the size of the view.
2995 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08002996 * @attr ref android.R.styleable#TextView_hint
2997 */
2998 @android.view.RemotableViewMethod
2999 public final void setHint(CharSequence hint) {
3000 mHint = TextUtils.stringOrSpannedString(hint);
3001
3002 if (mLayout != null) {
3003 checkForRelayout();
3004 }
3005
Romain Guy4dc4f732009-06-19 15:16:40 -07003006 if (mText.length() == 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003007 invalidate();
Romain Guy4dc4f732009-06-19 15:16:40 -07003008 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003009 }
3010
3011 /**
3012 * Sets the text to be displayed when the text of the TextView is empty,
3013 * from a resource.
3014 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003015 * @attr ref android.R.styleable#TextView_hint
3016 */
3017 @android.view.RemotableViewMethod
3018 public final void setHint(int resid) {
3019 setHint(getContext().getResources().getText(resid));
3020 }
3021
3022 /**
3023 * Returns the hint that is displayed when the text of the TextView
3024 * is empty.
3025 *
3026 * @attr ref android.R.styleable#TextView_hint
3027 */
3028 @ViewDebug.CapturedViewProperty
3029 public CharSequence getHint() {
3030 return mHint;
3031 }
3032
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003033 private boolean isMultilineInputType(int type) {
3034 return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3035 (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3036 }
3037
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003038 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003039 * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3040 * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3041 * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
3042 * then a soft keyboard will not be displayed for this text view.
3043 *
3044 * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3045 * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3046 * type.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003047 *
3048 * @see #getInputType()
3049 * @see #setRawInputType(int)
3050 * @see android.text.InputType
3051 * @attr ref android.R.styleable#TextView_inputType
3052 */
3053 public void setInputType(int type) {
Bjorn Bringertad8da912009-09-17 10:47:35 +01003054 final boolean wasPassword = isPasswordInputType(mInputType);
3055 final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003056 setInputType(type, false);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003057 final boolean isPassword = isPasswordInputType(type);
3058 final boolean isVisiblePassword = isVisiblePasswordInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003059 boolean forceUpdate = false;
3060 if (isPassword) {
3061 setTransformationMethod(PasswordTransformationMethod.getInstance());
3062 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003063 } else if (isVisiblePassword) {
Amith Yamasania8c0edb2009-09-27 16:51:21 -07003064 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3065 forceUpdate = true;
3066 }
The Android Open Source Project4df24232009-03-05 14:34:35 -08003067 setTypefaceByIndex(MONOSPACE, 0);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003068 } else if (wasPassword || wasVisiblePassword) {
3069 // not in password mode, clean up typeface and transformation
3070 setTypefaceByIndex(-1, -1);
3071 if (mTransformation == PasswordTransformationMethod.getInstance()) {
3072 forceUpdate = true;
3073 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003074 }
3075
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003076 boolean singleLine = !isMultilineInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003077
3078 // We need to update the single line mode if it has changed or we
3079 // were previously in password mode.
Gilles Debunne91a08cf2010-11-08 17:34:49 -08003080 if (mSingleLine != singleLine || forceUpdate) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003081 // Change single line mode, but only change the transformation if
3082 // we are not in password mode.
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08003083 applySingleLine(singleLine, !isPassword, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003084 }
3085
3086 InputMethodManager imm = InputMethodManager.peekInstance();
3087 if (imm != null) imm.restartInput(this);
3088 }
3089
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07003090 /**
3091 * It would be better to rely on the input type for everything. A password inputType should have
3092 * a password transformation. We should hence use isPasswordInputType instead of this method.
3093 *
3094 * We should:
3095 * - Call setInputType in setKeyListener instead of changing the input type directly (which
3096 * would install the correct transformation).
3097 * - Refuse the installation of a non-password transformation in setTransformation if the input
3098 * type is password.
3099 *
3100 * However, this is like this for legacy reasons and we cannot break existing apps. This method
3101 * is useful since it matches what the user can see (obfuscated text or not).
3102 *
3103 * @return true if the current transformation method is of the password type.
3104 */
3105 private boolean hasPasswordTransformationMethod() {
3106 return mTransformation instanceof PasswordTransformationMethod;
3107 }
3108
Bjorn Bringertad8da912009-09-17 10:47:35 +01003109 private boolean isPasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003110 final int variation =
3111 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003112 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003113 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3114 || variation
Ken Wakasa82d731a2010-12-24 23:42:41 +09003115 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3116 || variation
3117 == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003118 }
3119
3120 private boolean isVisiblePasswordInputType(int inputType) {
Gilles Debunned7483bf2010-11-10 10:47:45 -08003121 final int variation =
3122 inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003123 return variation
Gilles Debunned7483bf2010-11-10 10:47:45 -08003124 == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
Bjorn Bringertad8da912009-09-17 10:47:35 +01003125 }
3126
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003127 /**
3128 * Directly change the content type integer of the text view, without
3129 * modifying any other state.
3130 * @see #setInputType(int)
3131 * @see android.text.InputType
3132 * @attr ref android.R.styleable#TextView_inputType
3133 */
3134 public void setRawInputType(int type) {
3135 mInputType = type;
3136 }
3137
3138 private void setInputType(int type, boolean direct) {
3139 final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3140 KeyListener input;
3141 if (cls == EditorInfo.TYPE_CLASS_TEXT) {
Gilles Debunnee67b58a2010-08-31 15:55:31 -07003142 boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003143 TextKeyListener.Capitalize cap;
3144 if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3145 cap = TextKeyListener.Capitalize.CHARACTERS;
3146 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3147 cap = TextKeyListener.Capitalize.WORDS;
3148 } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3149 cap = TextKeyListener.Capitalize.SENTENCES;
3150 } else {
3151 cap = TextKeyListener.Capitalize.NONE;
3152 }
3153 input = TextKeyListener.getInstance(autotext, cap);
3154 } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3155 input = DigitsKeyListener.getInstance(
3156 (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3157 (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3158 } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3159 switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3160 case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3161 input = DateKeyListener.getInstance();
3162 break;
3163 case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3164 input = TimeKeyListener.getInstance();
3165 break;
3166 default:
3167 input = DateTimeKeyListener.getInstance();
3168 break;
3169 }
3170 } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3171 input = DialerKeyListener.getInstance();
3172 } else {
3173 input = TextKeyListener.getInstance();
3174 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07003175 setRawInputType(type);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003176 if (direct) mInput = input;
3177 else {
3178 setKeyListenerOnly(input);
3179 }
3180 }
3181
3182 /**
3183 * Get the type of the content.
3184 *
3185 * @see #setInputType(int)
3186 * @see android.text.InputType
3187 */
3188 public int getInputType() {
3189 return mInputType;
3190 }
3191
3192 /**
3193 * Change the editor type integer associated with the text view, which
3194 * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3195 * has focus.
3196 * @see #getImeOptions
3197 * @see android.view.inputmethod.EditorInfo
3198 * @attr ref android.R.styleable#TextView_imeOptions
3199 */
3200 public void setImeOptions(int imeOptions) {
3201 if (mInputContentType == null) {
3202 mInputContentType = new InputContentType();
3203 }
3204 mInputContentType.imeOptions = imeOptions;
3205 }
3206
3207 /**
3208 * Get the type of the IME editor.
3209 *
3210 * @see #setImeOptions(int)
3211 * @see android.view.inputmethod.EditorInfo
3212 */
3213 public int getImeOptions() {
3214 return mInputContentType != null
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003215 ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003216 }
3217
3218 /**
3219 * Change the custom IME action associated with the text view, which
3220 * will be reported to an IME with {@link EditorInfo#actionLabel}
3221 * and {@link EditorInfo#actionId} when it has focus.
3222 * @see #getImeActionLabel
3223 * @see #getImeActionId
3224 * @see android.view.inputmethod.EditorInfo
3225 * @attr ref android.R.styleable#TextView_imeActionLabel
3226 * @attr ref android.R.styleable#TextView_imeActionId
3227 */
3228 public void setImeActionLabel(CharSequence label, int actionId) {
3229 if (mInputContentType == null) {
3230 mInputContentType = new InputContentType();
3231 }
3232 mInputContentType.imeActionLabel = label;
3233 mInputContentType.imeActionId = actionId;
3234 }
3235
3236 /**
3237 * Get the IME action label previous set with {@link #setImeActionLabel}.
3238 *
3239 * @see #setImeActionLabel
3240 * @see android.view.inputmethod.EditorInfo
3241 */
3242 public CharSequence getImeActionLabel() {
3243 return mInputContentType != null
3244 ? mInputContentType.imeActionLabel : null;
3245 }
3246
3247 /**
3248 * Get the IME action ID previous set with {@link #setImeActionLabel}.
3249 *
3250 * @see #setImeActionLabel
3251 * @see android.view.inputmethod.EditorInfo
3252 */
3253 public int getImeActionId() {
3254 return mInputContentType != null
3255 ? mInputContentType.imeActionId : 0;
3256 }
3257
3258 /**
3259 * Set a special listener to be called when an action is performed
3260 * on the text view. This will be called when the enter key is pressed,
3261 * or when an action supplied to the IME is selected by the user. Setting
3262 * this means that the normal hard key event will not insert a newline
3263 * into the text view, even if it is multi-line; holding down the ALT
3264 * modifier will, however, allow the user to insert a newline character.
3265 */
3266 public void setOnEditorActionListener(OnEditorActionListener l) {
3267 if (mInputContentType == null) {
3268 mInputContentType = new InputContentType();
3269 }
3270 mInputContentType.onEditorActionListener = l;
3271 }
3272
3273 /**
3274 * Called when an attached input method calls
3275 * {@link InputConnection#performEditorAction(int)
3276 * InputConnection.performEditorAction()}
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003277 * for this text view. The default implementation will call your action
3278 * listener supplied to {@link #setOnEditorActionListener}, or perform
3279 * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003280 * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3281 * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003282 * EditorInfo.IME_ACTION_DONE}.
3283 *
3284 * <p>For backwards compatibility, if no IME options have been set and the
3285 * text view would not normally advance focus on enter, then
3286 * the NEXT and DONE actions received here will be turned into an enter
3287 * key down/up pair to go through the normal key handling.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003288 *
3289 * @param actionCode The code of the action being performed.
3290 *
3291 * @see #setOnEditorActionListener
3292 */
3293 public void onEditorAction(int actionCode) {
3294 final InputContentType ict = mInputContentType;
3295 if (ict != null) {
3296 if (ict.onEditorActionListener != null) {
3297 if (ict.onEditorActionListener.onEditorAction(this,
3298 actionCode, null)) {
3299 return;
3300 }
3301 }
The Android Open Source Project10592532009-03-18 17:39:46 -07003302
The Android Open Source Project4df24232009-03-05 14:34:35 -08003303 // This is the handling for some default action.
3304 // Note that for backwards compatibility we don't do this
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003305 // default handling if explicit ime options have not been given,
The Android Open Source Project10592532009-03-18 17:39:46 -07003306 // instead turning this into the normal enter key codes that an
The Android Open Source Project4df24232009-03-05 14:34:35 -08003307 // app may be expecting.
3308 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
3309 View v = focusSearch(FOCUS_DOWN);
3310 if (v != null) {
3311 if (!v.requestFocus(FOCUS_DOWN)) {
3312 throw new IllegalStateException("focus search returned a view " +
3313 "that wasn't able to take focus!");
3314 }
3315 }
3316 return;
3317
Dianne Hackborndea3ef72010-10-28 14:24:22 -07003318 } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
3319 View v = focusSearch(FOCUS_UP);
3320 if (v != null) {
3321 if (!v.requestFocus(FOCUS_UP)) {
3322 throw new IllegalStateException("focus search returned a view " +
3323 "that wasn't able to take focus!");
3324 }
3325 }
3326 return;
3327
The Android Open Source Project4df24232009-03-05 14:34:35 -08003328 } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3329 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08003330 if (imm != null && imm.isActive(this)) {
The Android Open Source Project4df24232009-03-05 14:34:35 -08003331 imm.hideSoftInputFromWindow(getWindowToken(), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003332 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07003333 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003334 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003335 }
3336
3337 Handler h = getHandler();
The Android Open Source Project10592532009-03-18 17:39:46 -07003338 if (h != null) {
3339 long eventTime = SystemClock.uptimeMillis();
3340 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
3341 new KeyEvent(eventTime, eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003342 KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3343 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003344 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3345 | KeyEvent.FLAG_EDITOR_ACTION)));
3346 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
3347 new KeyEvent(SystemClock.uptimeMillis(), eventTime,
Jeff Brown6b53e8d2010-11-10 16:03:06 -08003348 KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3349 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
The Android Open Source Project10592532009-03-18 17:39:46 -07003350 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3351 | KeyEvent.FLAG_EDITOR_ACTION)));
3352 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003353 }
3354
3355 /**
3356 * Set the private content type of the text, which is the
3357 * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3358 * field that will be filled in when creating an input connection.
3359 *
3360 * @see #getPrivateImeOptions()
3361 * @see EditorInfo#privateImeOptions
3362 * @attr ref android.R.styleable#TextView_privateImeOptions
3363 */
3364 public void setPrivateImeOptions(String type) {
3365 if (mInputContentType == null) mInputContentType = new InputContentType();
3366 mInputContentType.privateImeOptions = type;
3367 }
3368
3369 /**
3370 * Get the private type of the content.
3371 *
3372 * @see #setPrivateImeOptions(String)
3373 * @see EditorInfo#privateImeOptions
3374 */
3375 public String getPrivateImeOptions() {
3376 return mInputContentType != null
3377 ? mInputContentType.privateImeOptions : null;
3378 }
3379
3380 /**
3381 * Set the extra input data of the text, which is the
3382 * {@link EditorInfo#extras TextBoxAttribute.extras}
3383 * Bundle that will be filled in when creating an input connection. The
3384 * given integer is the resource ID of an XML resource holding an
3385 * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3386 *
3387 * @see #getInputExtras(boolean)
3388 * @see EditorInfo#extras
3389 * @attr ref android.R.styleable#TextView_editorExtras
3390 */
3391 public void setInputExtras(int xmlResId)
3392 throws XmlPullParserException, IOException {
3393 XmlResourceParser parser = getResources().getXml(xmlResId);
3394 if (mInputContentType == null) mInputContentType = new InputContentType();
3395 mInputContentType.extras = new Bundle();
3396 getResources().parseBundleExtras(parser, mInputContentType.extras);
3397 }
3398
3399 /**
3400 * Retrieve the input extras currently associated with the text view, which
3401 * can be viewed as well as modified.
3402 *
3403 * @param create If true, the extras will be created if they don't already
3404 * exist. Otherwise, null will be returned if none have been created.
Gilles Debunnee15b3582010-06-16 15:17:21 -07003405 * @see #setInputExtras(int)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003406 * @see EditorInfo#extras
3407 * @attr ref android.R.styleable#TextView_editorExtras
3408 */
3409 public Bundle getInputExtras(boolean create) {
3410 if (mInputContentType == null) {
3411 if (!create) return null;
3412 mInputContentType = new InputContentType();
3413 }
3414 if (mInputContentType.extras == null) {
3415 if (!create) return null;
3416 mInputContentType.extras = new Bundle();
3417 }
3418 return mInputContentType.extras;
3419 }
3420
3421 /**
3422 * Returns the error message that was set to be displayed with
3423 * {@link #setError}, or <code>null</code> if no error was set
3424 * or if it the error was cleared by the widget after user input.
3425 */
3426 public CharSequence getError() {
3427 return mError;
3428 }
3429
3430 /**
3431 * Sets the right-hand compound drawable of the TextView to the "error"
3432 * icon and sets an error message that will be displayed in a popup when
3433 * the TextView has focus. The icon and error message will be reset to
3434 * null when any key events cause changes to the TextView's text. If the
3435 * <code>error</code> is <code>null</code>, the error message and icon
3436 * will be cleared.
3437 */
3438 @android.view.RemotableViewMethod
3439 public void setError(CharSequence error) {
3440 if (error == null) {
3441 setError(null, null);
3442 } else {
3443 Drawable dr = getContext().getResources().
Gilles Debunnea85467b2011-01-19 16:53:31 -08003444 getDrawable(com.android.internal.R.drawable.indicator_input_error);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003445
3446 dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3447 setError(error, dr);
3448 }
3449 }
3450
3451 /**
3452 * Sets the right-hand compound drawable of the TextView to the specified
3453 * icon and sets an error message that will be displayed in a popup when
3454 * the TextView has focus. The icon and error message will be reset to
3455 * null when any key events cause changes to the TextView's text. The
3456 * drawable must already have had {@link Drawable#setBounds} set on it.
3457 * If the <code>error</code> is <code>null</code>, the error message will
3458 * be cleared (and you should provide a <code>null</code> icon as well).
3459 */
3460 public void setError(CharSequence error, Drawable icon) {
3461 error = TextUtils.stringOrSpannedString(error);
3462
3463 mError = error;
3464 mErrorWasChanged = true;
3465 final Drawables dr = mDrawables;
3466 if (dr != null) {
Gilles Debunnea85467b2011-01-19 16:53:31 -08003467 setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, dr.mDrawableBottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003468 } else {
3469 setCompoundDrawables(null, null, icon, null);
3470 }
3471
3472 if (error == null) {
3473 if (mPopup != null) {
3474 if (mPopup.isShowing()) {
3475 mPopup.dismiss();
3476 }
3477
3478 mPopup = null;
3479 }
3480 } else {
3481 if (isFocused()) {
3482 showError();
3483 }
3484 }
3485 }
3486
3487 private void showError() {
3488 if (getWindowToken() == null) {
3489 mShowErrorAfterAttach = true;
3490 return;
3491 }
3492
3493 if (mPopup == null) {
3494 LayoutInflater inflater = LayoutInflater.from(getContext());
Gilles Debunnea85467b2011-01-19 16:53:31 -08003495 final TextView err = (TextView) inflater.inflate(
3496 com.android.internal.R.layout.textview_hint, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003497
Romain Guy9bc9fa12009-07-21 16:57:29 -07003498 final float scale = getResources().getDisplayMetrics().density;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003499 mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003500 mPopup.setFocusable(false);
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -07003501 // The user is entering text, so the input method is needed. We
3502 // don't want the popup to be displayed on top of it.
3503 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003504 }
3505
3506 TextView tv = (TextView) mPopup.getContentView();
3507 chooseSize(mPopup, mError, tv);
3508 tv.setText(mError);
3509
3510 mPopup.showAsDropDown(this, getErrorX(), getErrorY());
The Android Open Source Project10592532009-03-18 17:39:46 -07003511 mPopup.fixDirection(mPopup.isAboveAnchor());
3512 }
3513
3514 private static class ErrorPopup extends PopupWindow {
3515 private boolean mAbove = false;
Gilles Debunnee15b3582010-06-16 15:17:21 -07003516 private final TextView mView;
Gilles Debunne5f059e42011-01-12 17:49:12 -08003517 private int mPopupInlineErrorBackgroundId = 0;
3518 private int mPopupInlineErrorAboveBackgroundId = 0;
The Android Open Source Project10592532009-03-18 17:39:46 -07003519
3520 ErrorPopup(TextView v, int width, int height) {
3521 super(v, width, height);
3522 mView = v;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003523 // Make sure the TextView has a background set as it will be used the first time it is
3524 // shown and positionned. Initialized with below background, which should have
3525 // dimensions identical to the above version for this to work (and is more likely).
3526 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3527 com.android.internal.R.styleable.Theme_errorMessageBackground);
3528 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
The Android Open Source Project10592532009-03-18 17:39:46 -07003529 }
3530
3531 void fixDirection(boolean above) {
3532 mAbove = above;
3533
3534 if (above) {
Gilles Debunne5f059e42011-01-12 17:49:12 -08003535 mPopupInlineErrorAboveBackgroundId =
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003536 getResourceId(mPopupInlineErrorAboveBackgroundId,
3537 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07003538 } else {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003539 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3540 com.android.internal.R.styleable.Theme_errorMessageBackground);
The Android Open Source Project10592532009-03-18 17:39:46 -07003541 }
Gilles Debunne5f059e42011-01-12 17:49:12 -08003542
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003543 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3544 mPopupInlineErrorBackgroundId);
Gilles Debunne5f059e42011-01-12 17:49:12 -08003545 }
3546
3547 private int getResourceId(int currentId, int index) {
3548 if (currentId == 0) {
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003549 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3550 R.styleable.Theme);
Gilles Debunne5f059e42011-01-12 17:49:12 -08003551 currentId = styledAttributes.getResourceId(index, 0);
3552 styledAttributes.recycle();
3553 }
3554 return currentId;
The Android Open Source Project10592532009-03-18 17:39:46 -07003555 }
3556
3557 @Override
3558 public void update(int x, int y, int w, int h, boolean force) {
3559 super.update(x, y, w, h, force);
3560
3561 boolean above = isAboveAnchor();
3562 if (above != mAbove) {
3563 fixDirection(above);
3564 }
3565 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003566 }
3567
3568 /**
3569 * Returns the Y offset to make the pointy top of the error point
3570 * at the middle of the error icon.
3571 */
3572 private int getErrorX() {
3573 /*
3574 * The "25" is the distance between the point and the right edge
3575 * of the background
3576 */
Romain Guy9bc9fa12009-07-21 16:57:29 -07003577 final float scale = getResources().getDisplayMetrics().density;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003578
3579 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003580 return getWidth() - mPopup.getWidth() - getPaddingRight() -
3581 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003582 }
3583
3584 /**
3585 * Returns the Y offset to make the pointy top of the error point
3586 * at the bottom of the error icon.
3587 */
3588 private int getErrorY() {
3589 /*
3590 * Compound, not extended, because the icon is not clipped
3591 * if the text height is smaller.
3592 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003593 final int compoundPaddingTop = getCompoundPaddingTop();
3594 int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003595
3596 final Drawables dr = mDrawables;
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003597 int icontop = compoundPaddingTop +
3598 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003599
3600 /*
3601 * The "2" is the distance between the point and the top edge
3602 * of the background.
3603 */
Gilles Debunnef1f409a2011-01-27 17:31:00 -08003604 final float scale = getResources().getDisplayMetrics().density;
3605 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
3606 (int) (2 * scale + 0.5f);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003607 }
3608
3609 private void hideError() {
3610 if (mPopup != null) {
3611 if (mPopup.isShowing()) {
3612 mPopup.dismiss();
3613 }
3614 }
3615
3616 mShowErrorAfterAttach = false;
3617 }
3618
3619 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
3620 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
3621 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
3622
3623 /*
3624 * Figure out how big the text would be if we laid it out to the
3625 * full width of this view minus the border.
3626 */
3627 int cap = getWidth() - wid;
3628 if (cap < 0) {
3629 cap = 200; // We must not be measured yet -- setFrame() will fix it.
3630 }
3631
3632 Layout l = new StaticLayout(text, tv.getPaint(), cap,
3633 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
3634 float max = 0;
3635 for (int i = 0; i < l.getLineCount(); i++) {
3636 max = Math.max(max, l.getLineWidth(i));
3637 }
3638
3639 /*
3640 * Now set the popup size to be big enough for the text plus the border.
3641 */
3642 pop.setWidth(wid + (int) Math.ceil(max));
3643 pop.setHeight(ht + l.getHeight());
3644 }
3645
3646
3647 @Override
3648 protected boolean setFrame(int l, int t, int r, int b) {
3649 boolean result = super.setFrame(l, t, r, b);
3650
3651 if (mPopup != null) {
3652 TextView tv = (TextView) mPopup.getContentView();
3653 chooseSize(mPopup, mError, tv);
Eric Fischerfa0d2532009-09-17 17:01:59 -07003654 mPopup.update(this, getErrorX(), getErrorY(),
3655 mPopup.getWidth(), mPopup.getHeight());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003656 }
3657
Romain Guy986003d2009-03-25 17:42:35 -07003658 restartMarqueeIfNeeded();
3659
3660 return result;
3661 }
3662
3663 private void restartMarqueeIfNeeded() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003664 if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
3665 mRestartMarquee = false;
3666 startMarquee();
3667 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003668 }
3669
3670 /**
3671 * Sets the list of input filters that will be used if the buffer is
3672 * Editable. Has no effect otherwise.
3673 *
3674 * @attr ref android.R.styleable#TextView_maxLength
3675 */
3676 public void setFilters(InputFilter[] filters) {
3677 if (filters == null) {
3678 throw new IllegalArgumentException();
3679 }
3680
3681 mFilters = filters;
3682
3683 if (mText instanceof Editable) {
3684 setFilters((Editable) mText, filters);
3685 }
3686 }
3687
3688 /**
3689 * Sets the list of input filters on the specified Editable,
3690 * and includes mInput in the list if it is an InputFilter.
3691 */
3692 private void setFilters(Editable e, InputFilter[] filters) {
3693 if (mInput instanceof InputFilter) {
3694 InputFilter[] nf = new InputFilter[filters.length + 1];
3695
3696 System.arraycopy(filters, 0, nf, 0, filters.length);
3697 nf[filters.length] = (InputFilter) mInput;
3698
3699 e.setFilters(nf);
3700 } else {
3701 e.setFilters(filters);
3702 }
3703 }
3704
3705 /**
3706 * Returns the current list of input filters.
3707 */
3708 public InputFilter[] getFilters() {
3709 return mFilters;
3710 }
3711
3712 /////////////////////////////////////////////////////////////////////////
3713
3714 private int getVerticalOffset(boolean forceNormal) {
3715 int voffset = 0;
3716 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3717
3718 Layout l = mLayout;
3719 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3720 l = mHintLayout;
3721 }
3722
3723 if (gravity != Gravity.TOP) {
3724 int boxht;
3725
3726 if (l == mHintLayout) {
3727 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3728 getCompoundPaddingBottom();
3729 } else {
3730 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3731 getExtendedPaddingBottom();
3732 }
3733 int textht = l.getHeight();
3734
3735 if (textht < boxht) {
3736 if (gravity == Gravity.BOTTOM)
3737 voffset = boxht - textht;
3738 else // (gravity == Gravity.CENTER_VERTICAL)
3739 voffset = (boxht - textht) >> 1;
3740 }
3741 }
3742 return voffset;
3743 }
3744
3745 private int getBottomVerticalOffset(boolean forceNormal) {
3746 int voffset = 0;
3747 final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3748
3749 Layout l = mLayout;
3750 if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3751 l = mHintLayout;
3752 }
3753
3754 if (gravity != Gravity.BOTTOM) {
3755 int boxht;
3756
3757 if (l == mHintLayout) {
3758 boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3759 getCompoundPaddingBottom();
3760 } else {
3761 boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3762 getExtendedPaddingBottom();
3763 }
3764 int textht = l.getHeight();
3765
3766 if (textht < boxht) {
3767 if (gravity == Gravity.TOP)
3768 voffset = boxht - textht;
3769 else // (gravity == Gravity.CENTER_VERTICAL)
3770 voffset = (boxht - textht) >> 1;
3771 }
3772 }
3773 return voffset;
3774 }
3775
3776 private void invalidateCursorPath() {
3777 if (mHighlightPathBogus) {
3778 invalidateCursor();
3779 } else {
Gilles Debunnef75c97e2011-02-10 16:09:53 -08003780 final int horizontalPadding = getCompoundPaddingLeft();
3781 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003782
Gilles Debunnef75c97e2011-02-10 16:09:53 -08003783 if (mCursorCount == 0) {
3784 synchronized (sTempRect) {
3785 /*
3786 * The reason for this concern about the thickness of the
3787 * cursor and doing the floor/ceil on the coordinates is that
3788 * some EditTexts (notably textfields in the Browser) have
3789 * anti-aliased text where not all the characters are
3790 * necessarily at integer-multiple locations. This should
3791 * make sure the entire cursor gets invalidated instead of
3792 * sometimes missing half a pixel.
3793 */
3794 float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
3795 if (thick < 1.0f) {
3796 thick = 1.0f;
3797 }
3798
3799 thick /= 2.0f;
3800
3801 mHighlightPath.computeBounds(sTempRect, false);
3802
3803 invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
3804 (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
3805 (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
3806 (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003807 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08003808 } else {
3809 for (int i = 0; i < mCursorCount; i++) {
3810 Rect bounds = mCursorDrawable[i].getBounds();
3811 invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
3812 bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
3813 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003814 }
3815 }
3816 }
3817
3818 private void invalidateCursor() {
Gilles Debunne05336272010-07-09 20:13:45 -07003819 int where = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003820
3821 invalidateCursor(where, where, where);
3822 }
3823
3824 private void invalidateCursor(int a, int b, int c) {
3825 if (mLayout == null) {
3826 invalidate();
3827 } else {
3828 if (a >= 0 || b >= 0 || c >= 0) {
3829 int first = Math.min(Math.min(a, b), c);
3830 int last = Math.max(Math.max(a, b), c);
3831
3832 int line = mLayout.getLineForOffset(first);
3833 int top = mLayout.getLineTop(line);
3834
3835 // This is ridiculous, but the descent from the line above
3836 // can hang down into the line we really want to redraw,
3837 // so we have to invalidate part of the line above to make
3838 // sure everything that needs to be redrawn really is.
3839 // (But not the whole line above, because that would cause
3840 // the same problem with the descenders on the line above it!)
3841 if (line > 0) {
3842 top -= mLayout.getLineDescent(line - 1);
3843 }
3844
3845 int line2;
3846
3847 if (first == last)
3848 line2 = line;
3849 else
3850 line2 = mLayout.getLineForOffset(last);
3851
3852 int bottom = mLayout.getLineTop(line2 + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003853
Gilles Debunnef75c97e2011-02-10 16:09:53 -08003854 final int horizontalPadding = getCompoundPaddingLeft();
3855 final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
3856
3857 // If used, the cursor drawables can have an arbitrary dimension that can go beyond
3858 // the invalidated lines specified above.
3859 for (int i = 0; i < mCursorCount; i++) {
3860 Rect bounds = mCursorDrawable[i].getBounds();
3861 top = Math.min(top, bounds.top);
3862 bottom = Math.max(bottom, bounds.bottom);
3863 // Horizontal bounds are already full width, no need to update
3864 }
3865
3866 invalidate(horizontalPadding + mScrollX, top + verticalPadding,
3867 horizontalPadding + mScrollX + getWidth() -
3868 getCompoundPaddingLeft() - getCompoundPaddingRight(),
3869 bottom + verticalPadding);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003870 }
3871 }
3872 }
3873
3874 private void registerForPreDraw() {
3875 final ViewTreeObserver observer = getViewTreeObserver();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003876
3877 if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
3878 observer.addOnPreDrawListener(this);
3879 mPreDrawState = PREDRAW_PENDING;
3880 } else if (mPreDrawState == PREDRAW_DONE) {
3881 mPreDrawState = PREDRAW_PENDING;
3882 }
3883
3884 // else state is PREDRAW_PENDING, so keep waiting.
3885 }
3886
3887 /**
3888 * {@inheritDoc}
3889 */
3890 public boolean onPreDraw() {
3891 if (mPreDrawState != PREDRAW_PENDING) {
3892 return true;
3893 }
3894
3895 if (mLayout == null) {
3896 assumeLayout();
3897 }
3898
3899 boolean changed = false;
3900
3901 if (mMovement != null) {
Gilles Debunne05336272010-07-09 20:13:45 -07003902 /* This code also provides auto-scrolling when a cursor is moved using a
3903 * CursorController (insertion point or selection limits).
3904 * For selection, ensure start or end is visible depending on controller's state.
3905 */
3906 int curs = getSelectionEnd();
Gilles Debunnee587d832010-11-23 20:20:11 -08003907 // Do not create the controller if it is not already created.
3908 if (mSelectionModifierCursorController != null &&
3909 mSelectionModifierCursorController.isSelectionStartDragged()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07003910 curs = getSelectionStart();
Gilles Debunne05336272010-07-09 20:13:45 -07003911 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003912
3913 /*
3914 * TODO: This should really only keep the end in view if
3915 * it already was before the text changed. I'm not sure
3916 * of a good way to tell from here if it was.
3917 */
3918 if (curs < 0 &&
3919 (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
3920 curs = mText.length();
3921 }
3922
3923 if (curs >= 0) {
3924 changed = bringPointIntoView(curs);
3925 }
3926 } else {
3927 changed = bringTextIntoView();
3928 }
3929
Gilles Debunne64e54a62010-09-07 19:07:17 -07003930 // This has to be checked here since:
3931 // - onFocusChanged cannot start it when focus is given to a view with selected text (after
3932 // a screen rotation) since layout is not yet initialized at that point.
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08003933 if (mCreatedWithASelection) {
3934 startSelectionActionMode();
3935 mCreatedWithASelection = false;
3936 }
3937
3938 // Phone specific code (there is no ExtractEditText on tablets).
3939 // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
3940 // not be set. Do the test here instead.
3941 if (this instanceof ExtractEditText && hasSelection()) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07003942 startSelectionActionMode();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07003943 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07003944
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003945 mPreDrawState = PREDRAW_DONE;
3946 return !changed;
3947 }
3948
3949 @Override
3950 protected void onAttachedToWindow() {
3951 super.onAttachedToWindow();
3952
3953 mTemporaryDetach = false;
3954
3955 if (mShowErrorAfterAttach) {
3956 showError();
3957 mShowErrorAfterAttach = false;
3958 }
Adam Powell624380a2010-10-02 18:12:02 -07003959
3960 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08003961 // No need to create the controller.
3962 // The get method will add the listener on controller creation.
3963 if (mInsertionPointCursorController != null) {
3964 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
3965 }
3966 if (mSelectionModifierCursorController != null) {
3967 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell624380a2010-10-02 18:12:02 -07003968 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003969 }
3970
3971 @Override
3972 protected void onDetachedFromWindow() {
3973 super.onDetachedFromWindow();
3974
Adam Powell624380a2010-10-02 18:12:02 -07003975 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08003976 if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
3977 observer.removeOnPreDrawListener(this);
3978 mPreDrawState = PREDRAW_NOT_REGISTERED;
3979 }
3980 // No need to create the controller, as getXXController would.
3981 if (mInsertionPointCursorController != null) {
3982 observer.removeOnTouchModeChangeListener(mInsertionPointCursorController);
3983 }
3984 if (mSelectionModifierCursorController != null) {
3985 observer.removeOnTouchModeChangeListener(mSelectionModifierCursorController);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08003986 }
3987
3988 if (mError != null) {
3989 hideError();
3990 }
Adam Powellba0a2c32010-09-28 17:41:23 -07003991
Gilles Debunnef48e83b2010-12-06 18:36:08 -08003992 if (mBlink != null) {
Gilles Debunne3d010062011-02-18 14:16:41 -08003993 mBlink.removeCallbacks(mBlink);
Gilles Debunnef48e83b2010-12-06 18:36:08 -08003994 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08003995
3996 if (mInsertionPointCursorController != null) {
3997 mInsertionPointCursorController.onDetached();
3998 }
3999
4000 if (mSelectionModifierCursorController != null) {
4001 mSelectionModifierCursorController.onDetached();
4002 }
4003
Adam Powellba0a2c32010-09-28 17:41:23 -07004004 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004005 }
4006
4007 @Override
4008 protected boolean isPaddingOffsetRequired() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004009 return mShadowRadius != 0 || mDrawables != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004010 }
4011
4012 @Override
4013 protected int getLeftPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004014 return getCompoundPaddingLeft() - mPaddingLeft +
4015 (int) Math.min(0, mShadowDx - mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004016 }
4017
4018 @Override
4019 protected int getTopPaddingOffset() {
4020 return (int) Math.min(0, mShadowDy - mShadowRadius);
4021 }
4022
4023 @Override
4024 protected int getBottomPaddingOffset() {
4025 return (int) Math.max(0, mShadowDy + mShadowRadius);
4026 }
4027
4028 @Override
4029 protected int getRightPaddingOffset() {
Romain Guy076dc9f2009-06-24 17:17:51 -07004030 return -(getCompoundPaddingRight() - mPaddingRight) +
4031 (int) Math.max(0, mShadowDx + mShadowRadius);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004032 }
4033
4034 @Override
4035 protected boolean verifyDrawable(Drawable who) {
4036 final boolean verified = super.verifyDrawable(who);
4037 if (!verified && mDrawables != null) {
4038 return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
4039 who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom;
4040 }
4041 return verified;
4042 }
4043
4044 @Override
Dianne Hackborne2136772010-11-04 15:08:59 -07004045 public void jumpDrawablesToCurrentState() {
4046 super.jumpDrawablesToCurrentState();
4047 if (mDrawables != null) {
4048 if (mDrawables.mDrawableLeft != null) {
4049 mDrawables.mDrawableLeft.jumpToCurrentState();
4050 }
4051 if (mDrawables.mDrawableTop != null) {
4052 mDrawables.mDrawableTop.jumpToCurrentState();
4053 }
4054 if (mDrawables.mDrawableRight != null) {
4055 mDrawables.mDrawableRight.jumpToCurrentState();
4056 }
4057 if (mDrawables.mDrawableBottom != null) {
4058 mDrawables.mDrawableBottom.jumpToCurrentState();
4059 }
4060 }
4061 }
4062
4063 @Override
Romain Guy3c77d392009-05-20 11:26:50 -07004064 public void invalidateDrawable(Drawable drawable) {
4065 if (verifyDrawable(drawable)) {
4066 final Rect dirty = drawable.getBounds();
4067 int scrollX = mScrollX;
4068 int scrollY = mScrollY;
4069
4070 // IMPORTANT: The coordinates below are based on the coordinates computed
4071 // for each compound drawable in onDraw(). Make sure to update each section
4072 // accordingly.
4073 final TextView.Drawables drawables = mDrawables;
Romain Guya6cd4e02009-05-20 15:09:21 -07004074 if (drawables != null) {
4075 if (drawable == drawables.mDrawableLeft) {
4076 final int compoundPaddingTop = getCompoundPaddingTop();
4077 final int compoundPaddingBottom = getCompoundPaddingBottom();
4078 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004079
Romain Guya6cd4e02009-05-20 15:09:21 -07004080 scrollX += mPaddingLeft;
4081 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4082 } else if (drawable == drawables.mDrawableRight) {
4083 final int compoundPaddingTop = getCompoundPaddingTop();
4084 final int compoundPaddingBottom = getCompoundPaddingBottom();
4085 final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
Romain Guy3c77d392009-05-20 11:26:50 -07004086
Romain Guya6cd4e02009-05-20 15:09:21 -07004087 scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4088 scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4089 } else if (drawable == drawables.mDrawableTop) {
4090 final int compoundPaddingLeft = getCompoundPaddingLeft();
4091 final int compoundPaddingRight = getCompoundPaddingRight();
4092 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004093
Romain Guya6cd4e02009-05-20 15:09:21 -07004094 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4095 scrollY += mPaddingTop;
4096 } else if (drawable == drawables.mDrawableBottom) {
4097 final int compoundPaddingLeft = getCompoundPaddingLeft();
4098 final int compoundPaddingRight = getCompoundPaddingRight();
4099 final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
Romain Guy3c77d392009-05-20 11:26:50 -07004100
Romain Guya6cd4e02009-05-20 15:09:21 -07004101 scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4102 scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4103 }
Romain Guy3c77d392009-05-20 11:26:50 -07004104 }
4105
4106 invalidate(dirty.left + scrollX, dirty.top + scrollY,
4107 dirty.right + scrollX, dirty.bottom + scrollY);
4108 }
4109 }
4110
4111 @Override
Romain Guyc4d8eb62010-08-18 20:48:33 -07004112 protected boolean onSetAlpha(int alpha) {
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004113 // Alpha is supported if and only if the drawing can be done in one pass.
4114 // TODO text with spans with a background color currently do not respect this alpha.
4115 if (getBackground() == null) {
Romain Guyc4d8eb62010-08-18 20:48:33 -07004116 mCurrentAlpha = alpha;
4117 final Drawables dr = mDrawables;
4118 if (dr != null) {
Michael Jurka406f0522010-09-15 18:48:48 -07004119 if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4120 if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4121 if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4122 if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
Romain Guyc4d8eb62010-08-18 20:48:33 -07004123 }
4124 return true;
4125 }
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004126
4127 mCurrentAlpha = 255;
Romain Guyc4d8eb62010-08-18 20:48:33 -07004128 return false;
4129 }
4130
Gilles Debunne86b9c782010-11-11 10:43:48 -08004131 /**
4132 * When a TextView is used to display a useful piece of information to the user (such as a
4133 * contact's address), it should be made selectable, so that the user can select and copy this
4134 * content.
4135 *
4136 * Use {@link #setTextIsSelectable(boolean)} or the
4137 * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
Gilles Debunnee12f9992010-12-17 11:04:55 -08004138 * selectable (text is not selectable by default).
Gilles Debunne6f100f32010-12-13 18:04:20 -08004139 *
4140 * Note that the content of an EditText is always selectable.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004141 *
4142 * @return True if the text displayed in this TextView can be selected by the user.
4143 *
4144 * @attr ref android.R.styleable#TextView_textIsSelectable
4145 */
4146 public boolean isTextSelectable() {
4147 return mTextIsSelectable;
4148 }
4149
4150 /**
4151 * Sets whether or not (default) the content of this view is selectable by the user.
Gilles Debunne6f100f32010-12-13 18:04:20 -08004152 *
Gilles Debunnee12f9992010-12-17 11:04:55 -08004153 * Note that this methods affect the {@link #setFocusable(boolean)},
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004154 * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4155 * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4156 * customized.
Gilles Debunne86b9c782010-11-11 10:43:48 -08004157 *
4158 * See {@link #isTextSelectable} for details.
4159 *
4160 * @param selectable Whether or not the content of this TextView should be selectable.
4161 */
4162 public void setTextIsSelectable(boolean selectable) {
4163 if (mTextIsSelectable == selectable) return;
4164
4165 mTextIsSelectable = selectable;
4166
Gilles Debunnecbcb3452010-12-17 15:31:02 -08004167 setFocusableInTouchMode(selectable);
Gilles Debunne86b9c782010-11-11 10:43:48 -08004168 setFocusable(selectable);
4169 setClickable(selectable);
4170 setLongClickable(selectable);
4171
4172 // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4173
4174 setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4175 setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4176
4177 // Called by setText above, but safer in case of future code changes
4178 prepareCursorControllers();
4179 }
4180
4181 @Override
4182 protected int[] onCreateDrawableState(int extraSpace) {
Gilles Debunnefb817032011-01-13 13:52:49 -08004183 final int[] drawableState;
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004184
Gilles Debunnefb817032011-01-13 13:52:49 -08004185 if (mSingleLine) {
4186 drawableState = super.onCreateDrawableState(extraSpace);
4187 } else {
4188 drawableState = super.onCreateDrawableState(extraSpace + 1);
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004189 mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4190 }
4191
Gilles Debunne86b9c782010-11-11 10:43:48 -08004192 if (mTextIsSelectable) {
4193 // Disable pressed state, which was introduced when TextView was made clickable.
4194 // Prevents text color change.
4195 // setClickable(false) would have a similar effect, but it also disables focus changes
4196 // and long press actions, which are both needed by text selection.
4197 final int length = drawableState.length;
4198 for (int i = 0; i < length; i++) {
4199 if (drawableState[i] == R.attr.state_pressed) {
4200 final int[] nonPressedState = new int[length - 1];
4201 System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4202 System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4203 return nonPressedState;
4204 }
4205 }
4206 }
Gilles Debunneda0a3f02010-12-22 10:07:15 -08004207
Gilles Debunne86b9c782010-11-11 10:43:48 -08004208 return drawableState;
4209 }
4210
Romain Guyc4d8eb62010-08-18 20:48:33 -07004211 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004212 protected void onDraw(Canvas canvas) {
Romain Guy909cbaf2010-10-13 18:19:48 -07004213 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4214
Romain Guy986003d2009-03-25 17:42:35 -07004215 restartMarqueeIfNeeded();
4216
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004217 // Draw the background for this view
4218 super.onDraw(canvas);
4219
4220 final int compoundPaddingLeft = getCompoundPaddingLeft();
4221 final int compoundPaddingTop = getCompoundPaddingTop();
4222 final int compoundPaddingRight = getCompoundPaddingRight();
4223 final int compoundPaddingBottom = getCompoundPaddingBottom();
4224 final int scrollX = mScrollX;
4225 final int scrollY = mScrollY;
4226 final int right = mRight;
4227 final int left = mLeft;
4228 final int bottom = mBottom;
4229 final int top = mTop;
4230
4231 final Drawables dr = mDrawables;
4232 if (dr != null) {
4233 /*
4234 * Compound, not extended, because the icon is not clipped
4235 * if the text height is smaller.
4236 */
4237
4238 int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4239 int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4240
Romain Guy3c77d392009-05-20 11:26:50 -07004241 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4242 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004243 if (dr.mDrawableLeft != null) {
4244 canvas.save();
4245 canvas.translate(scrollX + mPaddingLeft,
4246 scrollY + compoundPaddingTop +
4247 (vspace - dr.mDrawableHeightLeft) / 2);
4248 dr.mDrawableLeft.draw(canvas);
4249 canvas.restore();
4250 }
4251
Romain Guy3c77d392009-05-20 11:26:50 -07004252 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4253 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004254 if (dr.mDrawableRight != null) {
4255 canvas.save();
4256 canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4257 scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4258 dr.mDrawableRight.draw(canvas);
4259 canvas.restore();
4260 }
4261
Romain Guy3c77d392009-05-20 11:26:50 -07004262 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4263 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004264 if (dr.mDrawableTop != null) {
4265 canvas.save();
4266 canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4267 scrollY + mPaddingTop);
4268 dr.mDrawableTop.draw(canvas);
4269 canvas.restore();
4270 }
4271
Romain Guy3c77d392009-05-20 11:26:50 -07004272 // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4273 // Make sure to update invalidateDrawable() when changing this code.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004274 if (dr.mDrawableBottom != null) {
4275 canvas.save();
4276 canvas.translate(scrollX + compoundPaddingLeft +
4277 (hspace - dr.mDrawableWidthBottom) / 2,
4278 scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4279 dr.mDrawableBottom.draw(canvas);
4280 canvas.restore();
4281 }
4282 }
4283
4284 if (mPreDrawState == PREDRAW_DONE) {
4285 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08004286 observer.removeOnPreDrawListener(this);
4287 mPreDrawState = PREDRAW_NOT_REGISTERED;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004288 }
4289
4290 int color = mCurTextColor;
4291
4292 if (mLayout == null) {
4293 assumeLayout();
4294 }
4295
4296 Layout layout = mLayout;
4297 int cursorcolor = color;
4298
4299 if (mHint != null && mText.length() == 0) {
4300 if (mHintTextColor != null) {
4301 color = mCurHintTextColor;
4302 }
4303
4304 layout = mHintLayout;
4305 }
4306
4307 mTextPaint.setColor(color);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004308 if (mCurrentAlpha != 255) {
4309 // If set, the alpha will override the color's alpha. Multiply the alphas.
4310 mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4311 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004312 mTextPaint.drawableState = getDrawableState();
4313
4314 canvas.save();
4315 /* Would be faster if we didn't have to do this. Can we chop the
4316 (displayable) text so that we don't need to do this ever?
4317 */
4318
4319 int extendedPaddingTop = getExtendedPaddingTop();
4320 int extendedPaddingBottom = getExtendedPaddingBottom();
4321
4322 float clipLeft = compoundPaddingLeft + scrollX;
4323 float clipTop = extendedPaddingTop + scrollY;
4324 float clipRight = right - left - compoundPaddingRight + scrollX;
4325 float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4326
4327 if (mShadowRadius != 0) {
4328 clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4329 clipRight += Math.max(0, mShadowDx + mShadowRadius);
4330
4331 clipTop += Math.min(0, mShadowDy - mShadowRadius);
4332 clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4333 }
4334
4335 canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4336
4337 int voffsetText = 0;
4338 int voffsetCursor = 0;
4339
4340 // translate in by our padding
4341 {
4342 /* shortcircuit calling getVerticaOffset() */
4343 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4344 voffsetText = getVerticalOffset(false);
4345 voffsetCursor = getVerticalOffset(true);
4346 }
4347 canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4348 }
4349
4350 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4351 if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
4352 (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
4353 canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4354 getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4355 }
4356
4357 if (mMarquee != null && mMarquee.isRunning()) {
4358 canvas.translate(-mMarquee.mScroll, 0.0f);
4359 }
4360 }
4361
4362 Path highlight = null;
4363 int selStart = -1, selEnd = -1;
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004364 boolean drawCursor = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004365
4366 // If there is no movement method, then there can be no selection.
4367 // Check that first and attempt to skip everything having to do with
4368 // the cursor.
4369 // XXX This is not strictly true -- a program could set the
4370 // selection manually if it really wanted to.
4371 if (mMovement != null && (isFocused() || isPressed())) {
Gilles Debunne05336272010-07-09 20:13:45 -07004372 selStart = getSelectionStart();
4373 selEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004374
Gilles Debunne98dbfd42011-01-24 12:54:10 -08004375 if ((isCursorVisible() || mTextIsSelectable) && selStart >= 0 && isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004376 if (mHighlightPath == null)
4377 mHighlightPath = new Path();
4378
4379 if (selStart == selEnd) {
Gilles Debunne86b9c782010-11-11 10:43:48 -08004380 if (!mTextIsSelectable &&
4381 (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004382 if (mHighlightPathBogus) {
4383 mHighlightPath.reset();
4384 mLayout.getCursorPath(selStart, mHighlightPath, mText);
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004385 updateCursorsPositions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004386 mHighlightPathBogus = false;
4387 }
4388
4389 // XXX should pass to skin instead of drawing directly
4390 mHighlightPaint.setColor(cursorcolor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004391 if (mCurrentAlpha != 255) {
4392 mHighlightPaint.setAlpha(
4393 (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4394 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004395 mHighlightPaint.setStyle(Paint.Style.STROKE);
Gilles Debunne46b7d442011-02-17 16:03:10 -08004396 highlight = mHighlightPath;
Gilles Debunneeca97a32011-02-23 17:48:28 -08004397 drawCursor = mCursorCount > 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004398 }
4399 } else {
4400 if (mHighlightPathBogus) {
4401 mHighlightPath.reset();
4402 mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4403 mHighlightPathBogus = false;
4404 }
4405
4406 // XXX should pass to skin instead of drawing directly
4407 mHighlightPaint.setColor(mHighlightColor);
Gilles Debunne3dbf55c2010-12-16 10:31:51 -08004408 if (mCurrentAlpha != 255) {
4409 mHighlightPaint.setAlpha(
4410 (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4411 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004412 mHighlightPaint.setStyle(Paint.Style.FILL);
4413
4414 highlight = mHighlightPath;
4415 }
4416 }
4417 }
4418
4419 /* Comment out until we decide what to do about animations
4420 boolean isLinearTextOn = false;
4421 if (currentTransformation != null) {
4422 isLinearTextOn = mTextPaint.isLinearTextOn();
4423 Matrix m = currentTransformation.getMatrix();
4424 if (!m.isIdentity()) {
4425 // mTextPaint.setLinearTextOn(true);
4426 }
4427 }
4428 */
4429
4430 final InputMethodState ims = mInputMethodState;
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004431 final int cursorOffsetVertical = voffsetCursor - voffsetText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004432 if (ims != null && ims.mBatchEditNesting == 0) {
4433 InputMethodManager imm = InputMethodManager.peekInstance();
4434 if (imm != null) {
4435 if (imm.isActive(this)) {
4436 boolean reported = false;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07004437 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004438 // We are in extract mode and the content has changed
4439 // in some way... just report complete new text to the
4440 // input method.
4441 reported = reportExtractedText();
4442 }
4443 if (!reported && highlight != null) {
4444 int candStart = -1;
4445 int candEnd = -1;
4446 if (mText instanceof Spannable) {
4447 Spannable sp = (Spannable)mText;
4448 candStart = EditableInputConnection.getComposingSpanStart(sp);
4449 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4450 }
4451 imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4452 }
4453 }
4454
4455 if (imm.isWatchingCursor(this) && highlight != null) {
4456 highlight.computeBounds(ims.mTmpRectF, true);
4457 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4458
4459 canvas.getMatrix().mapPoints(ims.mTmpOffset);
4460 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4461
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004462 ims.mTmpRectF.offset(0, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004463
4464 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4465 (int)(ims.mTmpRectF.top + 0.5),
4466 (int)(ims.mTmpRectF.right + 0.5),
4467 (int)(ims.mTmpRectF.bottom + 0.5));
4468
4469 imm.updateCursor(this,
4470 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4471 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4472 }
4473 }
4474 }
4475
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004476 if (mCorrectionHighlighter != null) {
4477 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
4478 }
4479
Gilles Debunne46b7d442011-02-17 16:03:10 -08004480 if (drawCursor) {
4481 drawCursor(canvas, cursorOffsetVertical);
4482 // Rely on the drawable entirely, do not draw the cursor line.
4483 // Has to be done after the IMM related code above which relies on the highlight.
4484 highlight = null;
4485 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004486
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004487 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004488
Romain Guyc2303192009-04-03 17:37:18 -07004489 if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4490 canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
Gilles Debunne12d91ce2010-12-10 11:36:29 -08004491 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
Romain Guyc2303192009-04-03 17:37:18 -07004492 }
4493
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004494 /* Comment out until we decide what to do about animations
4495 if (currentTransformation != null) {
4496 mTextPaint.setLinearTextOn(isLinearTextOn);
4497 }
4498 */
4499
4500 canvas.restore();
Leon Scroggins56426252010-11-01 15:45:37 -04004501 }
4502
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004503 private void updateCursorsPositions() {
Gilles Debunneeca97a32011-02-23 17:48:28 -08004504 if (mCursorDrawableRes == 0) {
4505 mCursorCount = 0;
4506 return;
4507 }
Gilles Debunnef75c97e2011-02-10 16:09:53 -08004508
4509 final int offset = getSelectionStart();
4510 final int line = mLayout.getLineForOffset(offset);
4511 final int top = mLayout.getLineTop(line);
4512 final int bottom = mLayout.getLineTop(line + 1);
4513
4514 mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
4515
4516 int middle = bottom;
4517 if (mCursorCount == 2) {
4518 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
4519 middle = (top + bottom) >> 1;
4520 }
4521
4522 updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
4523
4524 if (mCursorCount == 2) {
4525 updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
4526 }
4527 }
4528
4529 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
4530 if (mCursorDrawable[cursorIndex] == null)
4531 mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
4532
4533 if (mTempRect == null) mTempRect = new Rect();
4534
4535 mCursorDrawable[cursorIndex].getPadding(mTempRect);
4536 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
4537 horizontal = Math.max(0.5f, horizontal - 0.5f);
4538 final int left = (int) (horizontal) - mTempRect.left;
4539 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
4540 bottom + mTempRect.bottom);
4541 }
4542
4543 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
4544 final boolean translate = cursorOffsetVertical != 0;
4545 if (translate) canvas.translate(0, cursorOffsetVertical);
4546 for (int i = 0; i < mCursorCount; i++) {
4547 mCursorDrawable[i].draw(canvas);
4548 }
4549 if (translate) canvas.translate(0, -cursorOffsetVertical);
4550 }
4551
Leon Scroggins56426252010-11-01 15:45:37 -04004552 /**
4553 * Update the positions of the CursorControllers. Needed by WebTextView,
4554 * which does not draw.
4555 * @hide
4556 */
4557 protected void updateCursorControllerPositions() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08004558 // TODO remove
Adam Powellb08013c2010-09-16 16:28:11 -07004559 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07004560
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004561 @Override
4562 public void getFocusedRect(Rect r) {
4563 if (mLayout == null) {
4564 super.getFocusedRect(r);
4565 return;
4566 }
4567
4568 int sel = getSelectionEnd();
4569 if (sel < 0) {
4570 super.getFocusedRect(r);
4571 return;
4572 }
4573
4574 int line = mLayout.getLineForOffset(sel);
4575 r.top = mLayout.getLineTop(line);
4576 r.bottom = mLayout.getLineBottom(line);
4577
4578 r.left = (int) mLayout.getPrimaryHorizontal(sel);
4579 r.right = r.left + 1;
4580
4581 // Adjust for padding and gravity.
4582 int paddingLeft = getCompoundPaddingLeft();
4583 int paddingTop = getExtendedPaddingTop();
4584 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4585 paddingTop += getVerticalOffset(false);
4586 }
4587 r.offset(paddingLeft, paddingTop);
4588 }
4589
4590 /**
4591 * Return the number of lines of text, or 0 if the internal Layout has not
4592 * been built.
4593 */
4594 public int getLineCount() {
4595 return mLayout != null ? mLayout.getLineCount() : 0;
4596 }
4597
4598 /**
4599 * Return the baseline for the specified line (0...getLineCount() - 1)
4600 * If bounds is not null, return the top, left, right, bottom extents
4601 * of the specified line in it. If the internal Layout has not been built,
4602 * return 0 and set bounds to (0, 0, 0, 0)
4603 * @param line which line to examine (0..getLineCount() - 1)
4604 * @param bounds Optional. If not null, it returns the extent of the line
4605 * @return the Y-coordinate of the baseline
4606 */
4607 public int getLineBounds(int line, Rect bounds) {
4608 if (mLayout == null) {
4609 if (bounds != null) {
4610 bounds.set(0, 0, 0, 0);
4611 }
4612 return 0;
4613 }
4614 else {
4615 int baseline = mLayout.getLineBounds(line, bounds);
4616
4617 int voffset = getExtendedPaddingTop();
4618 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4619 voffset += getVerticalOffset(true);
4620 }
4621 if (bounds != null) {
4622 bounds.offset(getCompoundPaddingLeft(), voffset);
4623 }
4624 return baseline + voffset;
4625 }
4626 }
4627
4628 @Override
4629 public int getBaseline() {
4630 if (mLayout == null) {
4631 return super.getBaseline();
4632 }
4633
4634 int voffset = 0;
4635 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4636 voffset = getVerticalOffset(true);
4637 }
4638
4639 return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
4640 }
4641
4642 @Override
4643 public boolean onKeyDown(int keyCode, KeyEvent event) {
4644 int which = doKeyDown(keyCode, event, null);
4645 if (which == 0) {
4646 // Go through default dispatching.
4647 return super.onKeyDown(keyCode, event);
4648 }
4649
4650 return true;
4651 }
4652
4653 @Override
4654 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
The Android Open Source Project10592532009-03-18 17:39:46 -07004655 KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004656
4657 int which = doKeyDown(keyCode, down, event);
4658 if (which == 0) {
4659 // Go through default dispatching.
4660 return super.onKeyMultiple(keyCode, repeatCount, event);
4661 }
4662 if (which == -1) {
4663 // Consumed the whole thing.
4664 return true;
4665 }
4666
4667 repeatCount--;
4668
4669 // We are going to dispatch the remaining events to either the input
4670 // or movement method. To do this, we will just send a repeated stream
4671 // of down and up events until we have done the complete repeatCount.
4672 // It would be nice if those interfaces had an onKeyMultiple() method,
4673 // but adding that is a more complicated change.
The Android Open Source Project10592532009-03-18 17:39:46 -07004674 KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004675 if (which == 1) {
4676 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4677 while (--repeatCount > 0) {
4678 mInput.onKeyDown(this, (Editable)mText, keyCode, down);
4679 mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4680 }
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08004681 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004682
4683 } else if (which == 2) {
4684 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4685 while (--repeatCount > 0) {
4686 mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
4687 mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4688 }
4689 }
4690
4691 return true;
4692 }
4693
4694 /**
4695 * Returns true if pressing ENTER in this field advances focus instead
4696 * of inserting the character. This is true mostly in single-line fields,
4697 * but also in mail addresses and subjects which will display on multiple
4698 * lines but where it doesn't make sense to insert newlines.
4699 */
The Android Open Source Project4df24232009-03-05 14:34:35 -08004700 private boolean shouldAdvanceFocusOnEnter() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004701 if (mInput == null) {
4702 return false;
4703 }
4704
4705 if (mSingleLine) {
4706 return true;
4707 }
4708
4709 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4710 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
Jeff Brown4e6319b2010-12-13 10:36:51 -08004711 if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
4712 || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004713 return true;
4714 }
4715 }
4716
4717 return false;
4718 }
4719
Jeff Brown4e6319b2010-12-13 10:36:51 -08004720 /**
4721 * Returns true if pressing TAB in this field advances focus instead
4722 * of inserting the character. Insert tabs only in multi-line editors.
4723 */
4724 private boolean shouldAdvanceFocusOnTab() {
4725 if (mInput != null && !mSingleLine) {
4726 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4727 int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
4728 if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
4729 || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
4730 return false;
4731 }
4732 }
4733 }
4734 return true;
4735 }
4736
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004737 private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
4738 if (!isEnabled()) {
4739 return 0;
4740 }
4741
4742 switch (keyCode) {
4743 case KeyEvent.KEYCODE_ENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07004744 mEnterKeyIsDown = true;
Jeff Brown4e6319b2010-12-13 10:36:51 -08004745 if (event.hasNoModifiers()) {
The Android Open Source Project10592532009-03-18 17:39:46 -07004746 // When mInputContentType is set, we know that we are
4747 // running in a "modern" cupcake environment, so don't need
4748 // to worry about the application trying to capture
4749 // enter key events.
4750 if (mInputContentType != null) {
The Android Open Source Project10592532009-03-18 17:39:46 -07004751 // If there is an action listener, given them a
4752 // chance to consume the event.
4753 if (mInputContentType.onEditorActionListener != null &&
4754 mInputContentType.onEditorActionListener.onEditorAction(
4755 this, EditorInfo.IME_NULL, event)) {
4756 mInputContentType.enterDown = true;
4757 // We are consuming the enter key for them.
4758 return -1;
4759 }
4760 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08004761
The Android Open Source Project10592532009-03-18 17:39:46 -07004762 // If our editor should move focus when enter is pressed, or
4763 // this is a generated event from an IME action button, then
4764 // don't let it be inserted into the text.
Jeff Brown4e6319b2010-12-13 10:36:51 -08004765 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
The Android Open Source Project10592532009-03-18 17:39:46 -07004766 || shouldAdvanceFocusOnEnter()) {
Leon Scroggins7014b122011-01-11 15:17:34 -05004767 if (mOnClickListener != null) {
4768 return 0;
4769 }
The Android Open Source Project10592532009-03-18 17:39:46 -07004770 return -1;
4771 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004772 }
The Android Open Source Project10592532009-03-18 17:39:46 -07004773 break;
4774
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004775 case KeyEvent.KEYCODE_DPAD_CENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07004776 mDPadCenterIsDown = true;
Jeff Brown4e6319b2010-12-13 10:36:51 -08004777 if (event.hasNoModifiers()) {
4778 if (shouldAdvanceFocusOnEnter()) {
4779 return 0;
4780 }
4781 }
4782 break;
4783
4784 case KeyEvent.KEYCODE_TAB:
4785 if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
4786 if (shouldAdvanceFocusOnTab()) {
4787 return 0;
4788 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004789 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07004790 break;
4791
4792 // Has to be done on key down (and not on key up) to correctly be intercepted.
4793 case KeyEvent.KEYCODE_BACK:
4794 if (mSelectionActionMode != null) {
4795 stopSelectionActionMode();
4796 return -1;
4797 }
4798 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004799 }
4800
4801 if (mInput != null) {
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08004802 resetErrorChangedFlag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004803
4804 boolean doDown = true;
4805 if (otherEvent != null) {
4806 try {
4807 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08004808 final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08004809 hideErrorIfUnchanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004810 doDown = false;
4811 if (handled) {
4812 return -1;
4813 }
4814 } catch (AbstractMethodError e) {
4815 // onKeyOther was added after 1.0, so if it isn't
4816 // implemented we need to try to dispatch as a regular down.
4817 } finally {
4818 endBatchEdit();
4819 }
4820 }
4821
4822 if (doDown) {
4823 beginBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08004824 final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004825 endBatchEdit();
Gilles Debunne12ab6452011-01-30 12:08:25 -08004826 hideErrorIfUnchanged();
4827 if (handled) return 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004828 }
4829 }
4830
4831 // bug 650865: sometimes we get a key event before a layout.
4832 // don't try to move around if we don't know the layout.
4833
4834 if (mMovement != null && mLayout != null) {
4835 boolean doDown = true;
4836 if (otherEvent != null) {
4837 try {
4838 boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
4839 otherEvent);
4840 doDown = false;
4841 if (handled) {
4842 return -1;
4843 }
4844 } catch (AbstractMethodError e) {
4845 // onKeyOther was added after 1.0, so if it isn't
4846 // implemented we need to try to dispatch as a regular down.
4847 }
4848 }
4849 if (doDown) {
4850 if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
4851 return 2;
4852 }
4853 }
4854
4855 return 0;
4856 }
4857
Gilles Debunneb7fc63f2011-01-27 15:55:29 -08004858 /**
4859 * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
4860 * can be recorded.
4861 * @hide
4862 */
4863 public void resetErrorChangedFlag() {
4864 /*
4865 * Keep track of what the error was before doing the input
4866 * so that if an input filter changed the error, we leave
4867 * that error showing. Otherwise, we take down whatever
4868 * error was showing when the user types something.
4869 */
4870 mErrorWasChanged = false;
4871 }
4872
4873 /**
4874 * @hide
4875 */
4876 public void hideErrorIfUnchanged() {
4877 if (mError != null && !mErrorWasChanged) {
4878 setError(null, null);
4879 }
4880 }
4881
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004882 @Override
4883 public boolean onKeyUp(int keyCode, KeyEvent event) {
4884 if (!isEnabled()) {
4885 return super.onKeyUp(keyCode, event);
4886 }
4887
4888 switch (keyCode) {
4889 case KeyEvent.KEYCODE_DPAD_CENTER:
Gilles Debunnecf1e9252010-10-07 20:46:03 -07004890 mDPadCenterIsDown = false;
Jeff Brown4e6319b2010-12-13 10:36:51 -08004891 if (event.hasNoModifiers()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004892 /*
4893 * If there is a click listener, just call through to
4894 * super, which will invoke it.
4895 *
Jeff Brown4e6319b2010-12-13 10:36:51 -08004896 * If there isn't a click listener, try to show the soft
4897 * input method. (It will also
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004898 * call performClick(), but that won't do anything in
4899 * this case.)
4900 */
4901 if (mOnClickListener == null) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08004902 if (mMovement != null && mText instanceof Editable
4903 && mLayout != null && onCheckIsTextEditor()) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08004904 InputMethodManager imm = InputMethodManager.peekInstance();
4905 if (imm != null) imm.showSoftInput(this, 0);
Jeff Brown4e6319b2010-12-13 10:36:51 -08004906 }
4907 }
4908 }
4909 return super.onKeyUp(keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004910
Jeff Brown4e6319b2010-12-13 10:36:51 -08004911 case KeyEvent.KEYCODE_ENTER:
4912 mEnterKeyIsDown = false;
4913 if (event.hasNoModifiers()) {
4914 if (mInputContentType != null
4915 && mInputContentType.onEditorActionListener != null
4916 && mInputContentType.enterDown) {
4917 mInputContentType.enterDown = false;
4918 if (mInputContentType.onEditorActionListener.onEditorAction(
4919 this, EditorInfo.IME_NULL, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004920 return true;
4921 }
4922 }
4923
Jeff Brown4e6319b2010-12-13 10:36:51 -08004924 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
4925 || shouldAdvanceFocusOnEnter()) {
4926 /*
4927 * If there is a click listener, just call through to
4928 * super, which will invoke it.
4929 *
4930 * If there isn't a click listener, try to advance focus,
4931 * but still call through to super, which will reset the
4932 * pressed state and longpress state. (It will also
4933 * call performClick(), but that won't do anything in
4934 * this case.)
4935 */
4936 if (mOnClickListener == null) {
4937 View v = focusSearch(FOCUS_DOWN);
4938
4939 if (v != null) {
4940 if (!v.requestFocus(FOCUS_DOWN)) {
4941 throw new IllegalStateException(
4942 "focus search returned a view " +
4943 "that wasn't able to take focus!");
4944 }
4945
4946 /*
4947 * Return true because we handled the key; super
4948 * will return false because there was no click
4949 * listener.
4950 */
4951 super.onKeyUp(keyCode, event);
4952 return true;
4953 } else if ((event.getFlags()
4954 & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
4955 // No target for next focus, but make sure the IME
4956 // if this came from it.
4957 InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunne17d31de2011-01-27 11:02:18 -08004958 if (imm != null && imm.isActive(this)) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08004959 imm.hideSoftInputFromWindow(getWindowToken(), 0);
4960 }
4961 }
4962 }
4963 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004964 return super.onKeyUp(keyCode, event);
4965 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07004966 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004967 }
4968
4969 if (mInput != null)
4970 if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
4971 return true;
4972
4973 if (mMovement != null && mLayout != null)
4974 if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
4975 return true;
4976
4977 return super.onKeyUp(keyCode, event);
4978 }
4979
4980 @Override public boolean onCheckIsTextEditor() {
4981 return mInputType != EditorInfo.TYPE_NULL;
4982 }
4983
4984 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
Janos Levai042856c2010-10-15 02:53:58 +03004985 if (onCheckIsTextEditor() && isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004986 if (mInputMethodState == null) {
4987 mInputMethodState = new InputMethodState();
4988 }
4989 outAttrs.inputType = mInputType;
4990 if (mInputContentType != null) {
4991 outAttrs.imeOptions = mInputContentType.imeOptions;
4992 outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
4993 outAttrs.actionLabel = mInputContentType.imeActionLabel;
4994 outAttrs.actionId = mInputContentType.imeActionId;
4995 outAttrs.extras = mInputContentType.extras;
4996 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07004997 outAttrs.imeOptions = EditorInfo.IME_NULL;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08004998 }
Dianne Hackborndea3ef72010-10-28 14:24:22 -07004999 if (focusSearch(FOCUS_DOWN) != null) {
5000 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5001 }
5002 if (focusSearch(FOCUS_UP) != null) {
5003 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5004 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005005 if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5006 == EditorInfo.IME_ACTION_UNSPECIFIED) {
Dianne Hackborndea3ef72010-10-28 14:24:22 -07005007 if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005008 // An action has not been set, but the enter key will move to
5009 // the next focus, so set the action to that.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005010 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005011 } else {
5012 // An action has not been set, and there is no focus to move
5013 // to, so let's just supply a "done" action.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005014 outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
The Android Open Source Project4df24232009-03-05 14:34:35 -08005015 }
5016 if (!shouldAdvanceFocusOnEnter()) {
5017 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005018 }
5019 }
Gilles Debunne91a08cf2010-11-08 17:34:49 -08005020 if (isMultilineInputType(outAttrs.inputType)) {
The Android Open Source Project10592532009-03-18 17:39:46 -07005021 // Multi-line text editors should always show an enter key.
5022 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5023 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005024 outAttrs.hintText = mHint;
5025 if (mText instanceof Editable) {
5026 InputConnection ic = new EditableInputConnection(this);
Gilles Debunne05336272010-07-09 20:13:45 -07005027 outAttrs.initialSelStart = getSelectionStart();
5028 outAttrs.initialSelEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005029 outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5030 return ic;
5031 }
5032 }
5033 return null;
5034 }
5035
5036 /**
5037 * If this TextView contains editable content, extract a portion of it
5038 * based on the information in <var>request</var> in to <var>outText</var>.
5039 * @return Returns true if the text was successfully extracted, else false.
5040 */
5041 public boolean extractText(ExtractedTextRequest request,
5042 ExtractedText outText) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005043 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5044 EXTRACT_UNKNOWN, outText);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005045 }
5046
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005047 static final int EXTRACT_NOTHING = -2;
5048 static final int EXTRACT_UNKNOWN = -1;
5049
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005050 boolean extractTextInternal(ExtractedTextRequest request,
5051 int partialStartOffset, int partialEndOffset, int delta,
5052 ExtractedText outText) {
5053 final CharSequence content = mText;
5054 if (content != null) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005055 if (partialStartOffset != EXTRACT_NOTHING) {
5056 final int N = content.length();
5057 if (partialStartOffset < 0) {
5058 outText.partialStartOffset = outText.partialEndOffset = -1;
5059 partialStartOffset = 0;
5060 partialEndOffset = N;
5061 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01005062 // Now use the delta to determine the actual amount of text
5063 // we need.
5064 partialEndOffset += delta;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005065 // Adjust offsets to ensure we contain full spans.
5066 if (content instanceof Spanned) {
5067 Spanned spanned = (Spanned)content;
5068 Object[] spans = spanned.getSpans(partialStartOffset,
5069 partialEndOffset, ParcelableSpan.class);
5070 int i = spans.length;
5071 while (i > 0) {
5072 i--;
5073 int j = spanned.getSpanStart(spans[i]);
5074 if (j < partialStartOffset) partialStartOffset = j;
5075 j = spanned.getSpanEnd(spans[i]);
5076 if (j > partialEndOffset) partialEndOffset = j;
5077 }
5078 }
5079 outText.partialStartOffset = partialStartOffset;
Viktor Yakovel964be412010-02-17 08:35:57 +01005080 outText.partialEndOffset = partialEndOffset - delta;
5081
Eric Fischer32929412009-12-14 17:33:11 -08005082 if (partialStartOffset > N) {
5083 partialStartOffset = N;
5084 } else if (partialStartOffset < 0) {
5085 partialStartOffset = 0;
5086 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005087 if (partialEndOffset > N) {
5088 partialEndOffset = N;
5089 } else if (partialEndOffset < 0) {
5090 partialEndOffset = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005091 }
5092 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005093 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5094 outText.text = content.subSequence(partialStartOffset,
5095 partialEndOffset);
5096 } else {
5097 outText.text = TextUtils.substring(content, partialStartOffset,
5098 partialEndOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005099 }
Viktor Yakovel970a138c92010-02-12 16:00:41 +01005100 } else {
5101 outText.partialStartOffset = 0;
5102 outText.partialEndOffset = 0;
5103 outText.text = "";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005104 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005105 outText.flags = 0;
5106 if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5107 outText.flags |= ExtractedText.FLAG_SELECTING;
5108 }
5109 if (mSingleLine) {
5110 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005111 }
5112 outText.startOffset = 0;
Gilles Debunne05336272010-07-09 20:13:45 -07005113 outText.selectionStart = getSelectionStart();
5114 outText.selectionEnd = getSelectionEnd();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005115 return true;
5116 }
5117 return false;
5118 }
5119
5120 boolean reportExtractedText() {
5121 final InputMethodState ims = mInputMethodState;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005122 if (ims != null) {
5123 final boolean contentChanged = ims.mContentChanged;
5124 if (contentChanged || ims.mSelectionModeChanged) {
5125 ims.mContentChanged = false;
5126 ims.mSelectionModeChanged = false;
5127 final ExtractedTextRequest req = mInputMethodState.mExtracting;
5128 if (req != null) {
5129 InputMethodManager imm = InputMethodManager.peekInstance();
5130 if (imm != null) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005131 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005132 + ims.mChangedStart + " end=" + ims.mChangedEnd
5133 + " delta=" + ims.mChangedDelta);
5134 if (ims.mChangedStart < 0 && !contentChanged) {
5135 ims.mChangedStart = EXTRACT_NOTHING;
5136 }
5137 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5138 ims.mChangedDelta, ims.mTmpExtracted)) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005139 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
Marco Nelissen9ea92312009-05-14 15:11:23 -07005140 + ims.mTmpExtracted.partialStartOffset
5141 + " end=" + ims.mTmpExtracted.partialEndOffset
5142 + ": " + ims.mTmpExtracted.text);
5143 imm.updateExtractedText(this, req.token,
5144 mInputMethodState.mTmpExtracted);
Viktor Yakovel964be412010-02-17 08:35:57 +01005145 ims.mChangedStart = EXTRACT_UNKNOWN;
5146 ims.mChangedEnd = EXTRACT_UNKNOWN;
5147 ims.mChangedDelta = 0;
5148 ims.mContentChanged = false;
Marco Nelissen9ea92312009-05-14 15:11:23 -07005149 return true;
5150 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005151 }
5152 }
5153 }
5154 }
5155 return false;
5156 }
5157
5158 /**
5159 * This is used to remove all style-impacting spans from text before new
5160 * extracted text is being replaced into it, so that we don't have any
5161 * lingering spans applied during the replace.
5162 */
5163 static void removeParcelableSpans(Spannable spannable, int start, int end) {
5164 Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5165 int i = spans.length;
5166 while (i > 0) {
5167 i--;
5168 spannable.removeSpan(spans[i]);
5169 }
5170 }
5171
5172 /**
5173 * Apply to this text view the given extracted text, as previously
5174 * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5175 */
5176 public void setExtractedText(ExtractedText text) {
5177 Editable content = getEditableText();
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005178 if (text.text != null) {
5179 if (content == null) {
5180 setText(text.text, TextView.BufferType.EDITABLE);
5181 } else if (text.partialStartOffset < 0) {
5182 removeParcelableSpans(content, 0, content.length());
5183 content.replace(0, content.length(), text.text);
5184 } else {
5185 final int N = content.length();
5186 int start = text.partialStartOffset;
5187 if (start > N) start = N;
5188 int end = text.partialEndOffset;
5189 if (end > N) end = N;
5190 removeParcelableSpans(content, start, end);
5191 content.replace(start, end, text.text);
5192 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005193 }
5194
5195 // Now set the selection position... make sure it is in range, to
5196 // avoid crashes. If this is a partial update, it is possible that
5197 // the underlying text may have changed, causing us problems here.
5198 // Also we just don't want to trust clients to do the right thing.
5199 Spannable sp = (Spannable)getText();
5200 final int N = sp.length();
5201 int start = text.selectionStart;
5202 if (start < 0) start = 0;
5203 else if (start > N) start = N;
5204 int end = text.selectionEnd;
5205 if (end < 0) end = 0;
5206 else if (end > N) end = N;
5207 Selection.setSelection(sp, start, end);
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005208
5209 // Finally, update the selection mode.
5210 if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5211 MetaKeyKeyListener.startSelecting(this, sp);
5212 } else {
5213 MetaKeyKeyListener.stopSelecting(this, sp);
5214 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005215 }
5216
5217 /**
5218 * @hide
5219 */
5220 public void setExtracting(ExtractedTextRequest req) {
5221 if (mInputMethodState != null) {
5222 mInputMethodState.mExtracting = req;
5223 }
Gilles Debunned94f8c52011-01-10 11:29:15 -08005224 // This stops a possible text selection mode. Maybe not intended.
Adam Powellba0a2c32010-09-28 17:41:23 -07005225 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005226 }
5227
5228 /**
5229 * Called by the framework in response to a text completion from
5230 * the current input method, provided by it calling
5231 * {@link InputConnection#commitCompletion
5232 * InputConnection.commitCompletion()}. The default implementation does
5233 * nothing; text views that are supporting auto-completion should override
5234 * this to do their desired behavior.
5235 *
5236 * @param text The auto complete text the user has selected.
5237 */
5238 public void onCommitCompletion(CompletionInfo text) {
5239 }
5240
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005241 /**
5242 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5243 * a dictionnary) from the current input method, provided by it calling
5244 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5245 * implementation flashes the background of the corrected word to provide feedback to the user.
5246 *
5247 * @param info The auto correct info about the text that was corrected.
5248 */
5249 public void onCommitCorrection(CorrectionInfo info) {
Gilles Debunne12d91ce2010-12-10 11:36:29 -08005250 if (mCorrectionHighlighter == null) {
5251 mCorrectionHighlighter = new CorrectionHighlighter();
5252 } else {
5253 mCorrectionHighlighter.invalidate(false);
5254 }
5255
5256 mCorrectionHighlighter.highlight(info);
5257 }
5258
5259 private class CorrectionHighlighter {
5260 private final Path mPath = new Path();
5261 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5262 private int mStart, mEnd;
5263 private long mFadingStartTime;
5264 private final static int FADE_OUT_DURATION = 400;
5265
5266 public CorrectionHighlighter() {
5267 mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5268 mPaint.setStyle(Paint.Style.FILL);
5269 }
5270
5271 public void highlight(CorrectionInfo info) {
5272 mStart = info.getOffset();
5273 mEnd = mStart + info.getNewText().length();
5274 mFadingStartTime = SystemClock.uptimeMillis();
5275
5276 if (mStart < 0 || mEnd < 0) {
5277 stopAnimation();
5278 }
5279 }
5280
5281 public void draw(Canvas canvas, int cursorOffsetVertical) {
5282 if (updatePath() && updatePaint()) {
5283 if (cursorOffsetVertical != 0) {
5284 canvas.translate(0, cursorOffsetVertical);
5285 }
5286
5287 canvas.drawPath(mPath, mPaint);
5288
5289 if (cursorOffsetVertical != 0) {
5290 canvas.translate(0, -cursorOffsetVertical);
5291 }
5292 invalidate(true);
5293 } else {
5294 stopAnimation();
5295 invalidate(false);
5296 }
5297 }
5298
5299 private boolean updatePaint() {
5300 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5301 if (duration > FADE_OUT_DURATION) return false;
5302
5303 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5304 final int highlightColorAlpha = Color.alpha(mHighlightColor);
5305 final int color = (mHighlightColor & 0x00FFFFFF) +
5306 ((int) (highlightColorAlpha * coef) << 24);
5307 mPaint.setColor(color);
5308 return true;
5309 }
5310
5311 private boolean updatePath() {
5312 final Layout layout = TextView.this.mLayout;
5313 if (layout == null) return false;
5314
5315 // Update in case text is edited while the animation is run
5316 final int length = mText.length();
5317 int start = Math.min(length, mStart);
5318 int end = Math.min(length, mEnd);
5319
5320 mPath.reset();
5321 TextView.this.mLayout.getSelectionPath(start, end, mPath);
5322 return true;
5323 }
5324
5325 private void invalidate(boolean delayed) {
5326 if (TextView.this.mLayout == null) return;
5327
5328 synchronized (sTempRect) {
5329 mPath.computeBounds(sTempRect, false);
5330
5331 int left = getCompoundPaddingLeft();
5332 int top = getExtendedPaddingTop() + getVerticalOffset(true);
5333
5334 if (delayed) {
5335 TextView.this.postInvalidateDelayed(16, // 60 Hz update
5336 left + (int) sTempRect.left, top + (int) sTempRect.top,
5337 left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5338 } else {
5339 TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5340 (int) sTempRect.right, (int) sTempRect.bottom);
5341 }
5342 }
5343 }
5344
5345 private void stopAnimation() {
5346 TextView.this.mCorrectionHighlighter = null;
5347 }
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08005348 }
5349
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005350 public void beginBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005351 mInBatchEditControllers = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005352 final InputMethodState ims = mInputMethodState;
5353 if (ims != null) {
5354 int nesting = ++ims.mBatchEditNesting;
5355 if (nesting == 1) {
5356 ims.mCursorChanged = false;
5357 ims.mChangedDelta = 0;
5358 if (ims.mContentChanged) {
5359 // We already have a pending change from somewhere else,
5360 // so turn this into a full update.
5361 ims.mChangedStart = 0;
5362 ims.mChangedEnd = mText.length();
5363 } else {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005364 ims.mChangedStart = EXTRACT_UNKNOWN;
5365 ims.mChangedEnd = EXTRACT_UNKNOWN;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005366 ims.mContentChanged = false;
5367 }
5368 onBeginBatchEdit();
5369 }
5370 }
5371 }
5372
5373 public void endBatchEdit() {
Adam Powell965b9692010-10-21 18:44:32 -07005374 mInBatchEditControllers = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005375 final InputMethodState ims = mInputMethodState;
5376 if (ims != null) {
5377 int nesting = --ims.mBatchEditNesting;
5378 if (nesting == 0) {
5379 finishBatchEdit(ims);
5380 }
5381 }
5382 }
5383
5384 void ensureEndedBatchEdit() {
5385 final InputMethodState ims = mInputMethodState;
5386 if (ims != null && ims.mBatchEditNesting != 0) {
5387 ims.mBatchEditNesting = 0;
5388 finishBatchEdit(ims);
5389 }
5390 }
5391
5392 void finishBatchEdit(final InputMethodState ims) {
5393 onEndBatchEdit();
5394
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07005395 if (ims.mContentChanged || ims.mSelectionModeChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005396 updateAfterEdit();
5397 reportExtractedText();
5398 } else if (ims.mCursorChanged) {
5399 // Cheezy way to get us to report the current cursor location.
5400 invalidateCursor();
5401 }
5402 }
5403
5404 void updateAfterEdit() {
5405 invalidate();
Gilles Debunne05336272010-07-09 20:13:45 -07005406 int curs = getSelectionStart();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005407
Gilles Debunne3d010062011-02-18 14:16:41 -08005408 if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005409 registerForPreDraw();
5410 }
5411
5412 if (curs >= 0) {
5413 mHighlightPathBogus = true;
Gilles Debunne3d010062011-02-18 14:16:41 -08005414 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005415 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07005416
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005417 checkForResize();
5418 }
5419
5420 /**
5421 * Called by the framework in response to a request to begin a batch
5422 * of edit operations through a call to link {@link #beginBatchEdit()}.
5423 */
5424 public void onBeginBatchEdit() {
5425 }
5426
5427 /**
5428 * Called by the framework in response to a request to end a batch
5429 * of edit operations through a call to link {@link #endBatchEdit}.
5430 */
5431 public void onEndBatchEdit() {
5432 }
5433
5434 /**
5435 * Called by the framework in response to a private command from the
5436 * current method, provided by it calling
5437 * {@link InputConnection#performPrivateCommand
5438 * InputConnection.performPrivateCommand()}.
5439 *
5440 * @param action The action name of the command.
5441 * @param data Any additional data for the command. This may be null.
5442 * @return Return true if you handled the command, else false.
5443 */
5444 public boolean onPrivateIMECommand(String action, Bundle data) {
5445 return false;
5446 }
5447
5448 private void nullLayouts() {
5449 if (mLayout instanceof BoringLayout && mSavedLayout == null) {
5450 mSavedLayout = (BoringLayout) mLayout;
5451 }
5452 if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
5453 mSavedHintLayout = (BoringLayout) mHintLayout;
5454 }
5455
5456 mLayout = mHintLayout = null;
Gilles Debunne77f18b02010-10-22 14:28:25 -07005457
5458 // Since it depends on the value of mLayout
5459 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005460 }
5461
5462 /**
5463 * Make a new Layout based on the already-measured size of the view,
5464 * on the assumption that it was measured correctly at some point.
5465 */
5466 private void assumeLayout() {
5467 int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5468
5469 if (width < 1) {
5470 width = 0;
5471 }
5472
5473 int physicalWidth = width;
5474
5475 if (mHorizontallyScrolling) {
5476 width = VERY_WIDE;
5477 }
5478
5479 makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
5480 physicalWidth, false);
5481 }
5482
5483 /**
5484 * The width passed in is now the desired layout width,
5485 * not the full view width with padding.
5486 * {@hide}
5487 */
5488 protected void makeNewLayout(int w, int hintWidth,
5489 BoringLayout.Metrics boring,
5490 BoringLayout.Metrics hintBoring,
5491 int ellipsisWidth, boolean bringIntoView) {
5492 stopMarquee();
5493
5494 mHighlightPathBogus = true;
5495
5496 if (w < 0) {
5497 w = 0;
5498 }
5499 if (hintWidth < 0) {
5500 hintWidth = 0;
5501 }
5502
5503 Layout.Alignment alignment;
5504 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
5505 case Gravity.CENTER_HORIZONTAL:
5506 alignment = Layout.Alignment.ALIGN_CENTER;
5507 break;
5508
5509 case Gravity.RIGHT:
Doug Feltc982f602010-05-25 11:51:40 -07005510 // Note, Layout resolves ALIGN_OPPOSITE to left or
5511 // right based on the paragraph direction.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005512 alignment = Layout.Alignment.ALIGN_OPPOSITE;
5513 break;
5514
5515 default:
5516 alignment = Layout.Alignment.ALIGN_NORMAL;
5517 }
5518
Romain Guy4dc4f732009-06-19 15:16:40 -07005519 boolean shouldEllipsize = mEllipsize != null && mInput == null;
5520
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005521 if (mText instanceof Spannable) {
5522 mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
5523 alignment, mSpacingMult,
Romain Guy4dc4f732009-06-19 15:16:40 -07005524 mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005525 ellipsisWidth);
5526 } else {
5527 if (boring == UNKNOWN_BORING) {
Gilles Debunne81f08082011-02-17 14:07:19 -08005528 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005529 if (boring != null) {
5530 mBoring = boring;
5531 }
5532 }
5533
5534 if (boring != null) {
5535 if (boring.width <= w &&
5536 (mEllipsize == null || boring.width <= ellipsisWidth)) {
5537 if (mSavedLayout != null) {
5538 mLayout = mSavedLayout.
5539 replaceOrMake(mTransformed, mTextPaint,
5540 w, alignment, mSpacingMult, mSpacingAdd,
5541 boring, mIncludePad);
5542 } else {
5543 mLayout = BoringLayout.make(mTransformed, mTextPaint,
5544 w, alignment, mSpacingMult, mSpacingAdd,
5545 boring, mIncludePad);
5546 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005547
5548 mSavedLayout = (BoringLayout) mLayout;
Romain Guy4dc4f732009-06-19 15:16:40 -07005549 } else if (shouldEllipsize && boring.width <= w) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005550 if (mSavedLayout != null) {
5551 mLayout = mSavedLayout.
5552 replaceOrMake(mTransformed, mTextPaint,
5553 w, alignment, mSpacingMult, mSpacingAdd,
5554 boring, mIncludePad, mEllipsize,
5555 ellipsisWidth);
5556 } else {
5557 mLayout = BoringLayout.make(mTransformed, mTextPaint,
5558 w, alignment, mSpacingMult, mSpacingAdd,
5559 boring, mIncludePad, mEllipsize,
5560 ellipsisWidth);
5561 }
Romain Guy4dc4f732009-06-19 15:16:40 -07005562 } else if (shouldEllipsize) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005563 mLayout = new StaticLayout(mTransformed,
5564 0, mTransformed.length(),
5565 mTextPaint, w, alignment, mSpacingMult,
5566 mSpacingAdd, mIncludePad, mEllipsize,
5567 ellipsisWidth);
5568 } else {
5569 mLayout = new StaticLayout(mTransformed, mTextPaint,
5570 w, alignment, mSpacingMult, mSpacingAdd,
5571 mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005572 }
Romain Guy4dc4f732009-06-19 15:16:40 -07005573 } else if (shouldEllipsize) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005574 mLayout = new StaticLayout(mTransformed,
5575 0, mTransformed.length(),
5576 mTextPaint, w, alignment, mSpacingMult,
5577 mSpacingAdd, mIncludePad, mEllipsize,
5578 ellipsisWidth);
5579 } else {
5580 mLayout = new StaticLayout(mTransformed, mTextPaint,
5581 w, alignment, mSpacingMult, mSpacingAdd,
5582 mIncludePad);
5583 }
5584 }
5585
Romain Guy4dc4f732009-06-19 15:16:40 -07005586 shouldEllipsize = mEllipsize != null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005587 mHintLayout = null;
5588
5589 if (mHint != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005590 if (shouldEllipsize) hintWidth = w;
5591
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005592 if (hintBoring == UNKNOWN_BORING) {
5593 hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
5594 mHintBoring);
5595 if (hintBoring != null) {
5596 mHintBoring = hintBoring;
5597 }
5598 }
5599
5600 if (hintBoring != null) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005601 if (hintBoring.width <= hintWidth &&
5602 (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005603 if (mSavedHintLayout != null) {
5604 mHintLayout = mSavedHintLayout.
5605 replaceOrMake(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07005606 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5607 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005608 } else {
5609 mHintLayout = BoringLayout.make(mHint, mTextPaint,
Romain Guy4dc4f732009-06-19 15:16:40 -07005610 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5611 hintBoring, mIncludePad);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005612 }
5613
5614 mSavedHintLayout = (BoringLayout) mHintLayout;
Romain Guy4dc4f732009-06-19 15:16:40 -07005615 } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
5616 if (mSavedHintLayout != null) {
5617 mHintLayout = mSavedHintLayout.
5618 replaceOrMake(mHint, mTextPaint,
5619 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5620 hintBoring, mIncludePad, mEllipsize,
5621 ellipsisWidth);
5622 } else {
5623 mHintLayout = BoringLayout.make(mHint, mTextPaint,
5624 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5625 hintBoring, mIncludePad, mEllipsize,
5626 ellipsisWidth);
5627 }
5628 } else if (shouldEllipsize) {
5629 mHintLayout = new StaticLayout(mHint,
5630 0, mHint.length(),
5631 mTextPaint, hintWidth, alignment, mSpacingMult,
5632 mSpacingAdd, mIncludePad, mEllipsize,
5633 ellipsisWidth);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005634 } else {
5635 mHintLayout = new StaticLayout(mHint, mTextPaint,
5636 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5637 mIncludePad);
5638 }
Romain Guy4dc4f732009-06-19 15:16:40 -07005639 } else if (shouldEllipsize) {
5640 mHintLayout = new StaticLayout(mHint,
5641 0, mHint.length(),
5642 mTextPaint, hintWidth, alignment, mSpacingMult,
5643 mSpacingAdd, mIncludePad, mEllipsize,
5644 ellipsisWidth);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005645 } else {
5646 mHintLayout = new StaticLayout(mHint, mTextPaint,
5647 hintWidth, alignment, mSpacingMult, mSpacingAdd,
5648 mIncludePad);
5649 }
5650 }
5651
5652 if (bringIntoView) {
5653 registerForPreDraw();
5654 }
5655
5656 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
Romain Guy939151f2009-04-08 14:22:40 -07005657 if (!compressText(ellipsisWidth)) {
5658 final int height = mLayoutParams.height;
5659 // If the size of the view does not depend on the size of the text, try to
5660 // start the marquee immediately
Romain Guy980a9382010-01-08 15:06:28 -08005661 if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
Romain Guy939151f2009-04-08 14:22:40 -07005662 startMarquee();
5663 } else {
5664 // Defer the start of the marquee until we know our width (see setFrame())
5665 mRestartMarquee = true;
5666 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005667 }
5668 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07005669
5670 // CursorControllers need a non-null mLayout
5671 prepareCursorControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005672 }
5673
Romain Guy939151f2009-04-08 14:22:40 -07005674 private boolean compressText(float width) {
Romain Guy2bffd262010-09-12 17:40:02 -07005675 if (isHardwareAccelerated()) return false;
5676
Romain Guy3373ed62009-05-04 14:13:32 -07005677 // Only compress the text if it hasn't been compressed by the previous pass
5678 if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
5679 mTextPaint.getTextScaleX() == 1.0f) {
Romain Guy939151f2009-04-08 14:22:40 -07005680 final float textWidth = mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07005681 final float overflow = (textWidth + 1.0f - width) / width;
Romain Guy939151f2009-04-08 14:22:40 -07005682 if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
5683 mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
5684 post(new Runnable() {
5685 public void run() {
5686 requestLayout();
5687 }
5688 });
5689 return true;
5690 }
5691 }
5692
5693 return false;
5694 }
5695
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005696 private static int desired(Layout layout) {
5697 int n = layout.getLineCount();
5698 CharSequence text = layout.getText();
5699 float max = 0;
5700
5701 // if any line was wrapped, we can't use it.
5702 // but it's ok for the last line not to have a newline
5703
5704 for (int i = 0; i < n - 1; i++) {
5705 if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
5706 return -1;
5707 }
5708
5709 for (int i = 0; i < n; i++) {
5710 max = Math.max(max, layout.getLineWidth(i));
5711 }
5712
5713 return (int) FloatMath.ceil(max);
5714 }
5715
5716 /**
5717 * Set whether the TextView includes extra top and bottom padding to make
5718 * room for accents that go above the normal ascent and descent.
5719 * The default is true.
5720 *
5721 * @attr ref android.R.styleable#TextView_includeFontPadding
5722 */
5723 public void setIncludeFontPadding(boolean includepad) {
5724 mIncludePad = includepad;
5725
5726 if (mLayout != null) {
5727 nullLayouts();
5728 requestLayout();
5729 invalidate();
5730 }
5731 }
5732
Romain Guy4dc4f732009-06-19 15:16:40 -07005733 private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005734
5735 @Override
5736 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5737 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
5738 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
5739 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
5740 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
5741
5742 int width;
5743 int height;
5744
5745 BoringLayout.Metrics boring = UNKNOWN_BORING;
5746 BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
5747
5748 int des = -1;
5749 boolean fromexisting = false;
5750
5751 if (widthMode == MeasureSpec.EXACTLY) {
5752 // Parent has told us how big to be. So be it.
5753 width = widthSize;
5754 } else {
5755 if (mLayout != null && mEllipsize == null) {
5756 des = desired(mLayout);
5757 }
5758
5759 if (des < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005760 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005761 if (boring != null) {
5762 mBoring = boring;
5763 }
5764 } else {
5765 fromexisting = true;
5766 }
5767
5768 if (boring == null || boring == UNKNOWN_BORING) {
5769 if (des < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005770 des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005771 }
5772
5773 width = des;
5774 } else {
5775 width = boring.width;
5776 }
5777
5778 final Drawables dr = mDrawables;
5779 if (dr != null) {
5780 width = Math.max(width, dr.mDrawableWidthTop);
5781 width = Math.max(width, dr.mDrawableWidthBottom);
5782 }
5783
5784 if (mHint != null) {
5785 int hintDes = -1;
5786 int hintWidth;
5787
Romain Guy4dc4f732009-06-19 15:16:40 -07005788 if (mHintLayout != null && mEllipsize == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005789 hintDes = desired(mHintLayout);
5790 }
5791
5792 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005793 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005794 if (hintBoring != null) {
5795 mHintBoring = hintBoring;
5796 }
5797 }
5798
5799 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
5800 if (hintDes < 0) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005801 hintDes = (int) FloatMath.ceil(
5802 Layout.getDesiredWidth(mHint, mTextPaint));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005803 }
5804
5805 hintWidth = hintDes;
5806 } else {
5807 hintWidth = hintBoring.width;
5808 }
5809
5810 if (hintWidth > width) {
5811 width = hintWidth;
5812 }
5813 }
5814
5815 width += getCompoundPaddingLeft() + getCompoundPaddingRight();
5816
5817 if (mMaxWidthMode == EMS) {
5818 width = Math.min(width, mMaxWidth * getLineHeight());
5819 } else {
5820 width = Math.min(width, mMaxWidth);
5821 }
5822
5823 if (mMinWidthMode == EMS) {
5824 width = Math.max(width, mMinWidth * getLineHeight());
5825 } else {
5826 width = Math.max(width, mMinWidth);
5827 }
5828
5829 // Check against our minimum width
5830 width = Math.max(width, getSuggestedMinimumWidth());
5831
5832 if (widthMode == MeasureSpec.AT_MOST) {
5833 width = Math.min(widthSize, width);
5834 }
5835 }
5836
5837 int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
5838 int unpaddedWidth = want;
Gilles Debunne9a80a652011-01-31 12:56:07 -08005839
5840 if (mHorizontallyScrolling) want = VERY_WIDE;
5841
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005842 int hintWant = want;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005843 int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
5844
5845 if (mLayout == null) {
5846 makeNewLayout(want, hintWant, boring, hintBoring,
Romain Guy4dc4f732009-06-19 15:16:40 -07005847 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005848 } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
5849 (mLayout.getEllipsizedWidth() !=
5850 width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
5851 if (mHint == null && mEllipsize == null &&
5852 want > mLayout.getWidth() &&
5853 (mLayout instanceof BoringLayout ||
Romain Guy4dc4f732009-06-19 15:16:40 -07005854 (fromexisting && des >= 0 && des <= want))) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005855 mLayout.increaseWidthTo(want);
5856 } else {
5857 makeNewLayout(want, hintWant, boring, hintBoring,
Romain Guy4dc4f732009-06-19 15:16:40 -07005858 width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005859 }
5860 } else {
5861 // Width has not changed.
5862 }
5863
5864 if (heightMode == MeasureSpec.EXACTLY) {
5865 // Parent has told us how big to be. So be it.
5866 height = heightSize;
5867 mDesiredHeightAtMeasure = -1;
5868 } else {
5869 int desired = getDesiredHeight();
5870
5871 height = desired;
5872 mDesiredHeightAtMeasure = desired;
5873
5874 if (heightMode == MeasureSpec.AT_MOST) {
Christoffer Gurell1d05c7c2009-10-12 15:53:39 +02005875 height = Math.min(desired, heightSize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005876 }
5877 }
5878
Romain Guy4dc4f732009-06-19 15:16:40 -07005879 int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005880 if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
Romain Guy4dc4f732009-06-19 15:16:40 -07005881 unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005882 }
5883
5884 /*
5885 * We didn't let makeNewLayout() register to bring the cursor into view,
5886 * so do it here if there is any possibility that it is needed.
5887 */
5888 if (mMovement != null ||
5889 mLayout.getWidth() > unpaddedWidth ||
5890 mLayout.getHeight() > unpaddedHeight) {
5891 registerForPreDraw();
5892 } else {
5893 scrollTo(0, 0);
5894 }
5895
5896 setMeasuredDimension(width, height);
5897 }
5898
5899 private int getDesiredHeight() {
Romain Guy4dc4f732009-06-19 15:16:40 -07005900 return Math.max(
5901 getDesiredHeight(mLayout, true),
5902 getDesiredHeight(mHintLayout, mEllipsize != null));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005903 }
5904
5905 private int getDesiredHeight(Layout layout, boolean cap) {
5906 if (layout == null) {
5907 return 0;
5908 }
5909
5910 int linecount = layout.getLineCount();
5911 int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
5912 int desired = layout.getLineTop(linecount);
5913
5914 final Drawables dr = mDrawables;
5915 if (dr != null) {
5916 desired = Math.max(desired, dr.mDrawableHeightLeft);
5917 desired = Math.max(desired, dr.mDrawableHeightRight);
5918 }
5919
5920 desired += pad;
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08005921 layout.setMaximumVisibleLineCount(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005922
5923 if (mMaxMode == LINES) {
5924 /*
5925 * Don't cap the hint to a certain number of lines.
5926 * (Do cap it, though, if we have a maximum pixel height.)
5927 */
5928 if (cap) {
5929 if (linecount > mMaximum) {
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08005930 layout.setMaximumVisibleLineCount(mMaximum);
5931 desired = layout.getLineTop(mMaximum);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005932
5933 if (dr != null) {
5934 desired = Math.max(desired, dr.mDrawableHeightLeft);
5935 desired = Math.max(desired, dr.mDrawableHeightRight);
5936 }
5937
5938 desired += pad;
5939 linecount = mMaximum;
5940 }
5941 }
5942 } else {
5943 desired = Math.min(desired, mMaximum);
5944 }
5945
5946 if (mMinMode == LINES) {
5947 if (linecount < mMinimum) {
5948 desired += getLineHeight() * (mMinimum - linecount);
5949 }
5950 } else {
5951 desired = Math.max(desired, mMinimum);
5952 }
5953
5954 // Check against our minimum height
5955 desired = Math.max(desired, getSuggestedMinimumHeight());
5956
5957 return desired;
5958 }
5959
5960 /**
5961 * Check whether a change to the existing text layout requires a
5962 * new view layout.
5963 */
5964 private void checkForResize() {
5965 boolean sizeChanged = false;
5966
5967 if (mLayout != null) {
5968 // Check if our width changed
5969 if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
5970 sizeChanged = true;
5971 invalidate();
5972 }
5973
5974 // Check if our height changed
5975 if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
5976 int desiredHeight = getDesiredHeight();
5977
5978 if (desiredHeight != this.getHeight()) {
5979 sizeChanged = true;
5980 }
Romain Guy980a9382010-01-08 15:06:28 -08005981 } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08005982 if (mDesiredHeightAtMeasure >= 0) {
5983 int desiredHeight = getDesiredHeight();
5984
5985 if (desiredHeight != mDesiredHeightAtMeasure) {
5986 sizeChanged = true;
5987 }
5988 }
5989 }
5990 }
5991
5992 if (sizeChanged) {
5993 requestLayout();
5994 // caller will have already invalidated
5995 }
5996 }
5997
5998 /**
5999 * Check whether entirely new text requires a new view layout
6000 * or merely a new text layout.
6001 */
6002 private void checkForRelayout() {
6003 // If we have a fixed width, we can just swap in a new text layout
6004 // if the text height stays the same or if the view height is fixed.
6005
6006 if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6007 (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6008 (mHint == null || mHintLayout != null) &&
6009 (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6010 // Static width, so try making a new text layout.
6011
6012 int oldht = mLayout.getHeight();
6013 int want = mLayout.getWidth();
6014 int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6015
6016 /*
6017 * No need to bring the text into view, since the size is not
6018 * changing (unless we do the requestLayout(), in which case it
6019 * will happen at measure).
6020 */
6021 makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
Romain Guye1e0dc82009-11-03 17:21:04 -08006022 mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6023 false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006024
Romain Guye1e0dc82009-11-03 17:21:04 -08006025 if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6026 // In a fixed-height view, so use our new text layout.
6027 if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
Romain Guy980a9382010-01-08 15:06:28 -08006028 mLayoutParams.height != LayoutParams.MATCH_PARENT) {
Romain Guye1e0dc82009-11-03 17:21:04 -08006029 invalidate();
6030 return;
6031 }
6032
6033 // Dynamic height, but height has stayed the same,
6034 // so use our new text layout.
6035 if (mLayout.getHeight() == oldht &&
6036 (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6037 invalidate();
6038 return;
6039 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006040 }
6041
6042 // We lose: the height has changed and we have a dynamic height.
6043 // Request a new view layout using our new text layout.
6044 requestLayout();
6045 invalidate();
6046 } else {
6047 // Dynamic width, so we have no choice but to request a new
6048 // view layout with a new text layout.
6049
6050 nullLayouts();
6051 requestLayout();
6052 invalidate();
6053 }
6054 }
6055
6056 /**
6057 * Returns true if anything changed.
6058 */
6059 private boolean bringTextIntoView() {
6060 int line = 0;
6061 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6062 line = mLayout.getLineCount() - 1;
6063 }
6064
6065 Layout.Alignment a = mLayout.getParagraphAlignment(line);
6066 int dir = mLayout.getParagraphDirection(line);
6067 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6068 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6069 int ht = mLayout.getHeight();
6070
6071 int scrollx, scrolly;
6072
6073 if (a == Layout.Alignment.ALIGN_CENTER) {
6074 /*
6075 * Keep centered if possible, or, if it is too wide to fit,
6076 * keep leading edge in view.
6077 */
6078
6079 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6080 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6081
6082 if (right - left < hspace) {
6083 scrollx = (right + left) / 2 - hspace / 2;
6084 } else {
6085 if (dir < 0) {
6086 scrollx = right - hspace;
6087 } else {
6088 scrollx = left;
6089 }
6090 }
6091 } else if (a == Layout.Alignment.ALIGN_NORMAL) {
6092 /*
6093 * Keep leading edge in view.
6094 */
6095
6096 if (dir < 0) {
6097 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6098 scrollx = right - hspace;
6099 } else {
6100 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6101 }
6102 } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
6103 /*
6104 * Keep trailing edge in view.
6105 */
6106
6107 if (dir < 0) {
6108 scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6109 } else {
6110 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6111 scrollx = right - hspace;
6112 }
6113 }
6114
6115 if (ht < vspace) {
6116 scrolly = 0;
6117 } else {
6118 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6119 scrolly = ht - vspace;
6120 } else {
6121 scrolly = 0;
6122 }
6123 }
6124
6125 if (scrollx != mScrollX || scrolly != mScrollY) {
6126 scrollTo(scrollx, scrolly);
6127 return true;
6128 } else {
6129 return false;
6130 }
6131 }
6132
6133 /**
6134 * Move the point, specified by the offset, into the view if it is needed.
6135 * This has to be called after layout. Returns true if anything changed.
6136 */
6137 public boolean bringPointIntoView(int offset) {
6138 boolean changed = false;
6139
6140 int line = mLayout.getLineForOffset(offset);
6141
6142 // FIXME: Is it okay to truncate this, or should we round?
6143 final int x = (int)mLayout.getPrimaryHorizontal(offset);
6144 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006145 final int bottom = mLayout.getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006146
6147 int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6148 int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6149 int ht = mLayout.getHeight();
6150
6151 int grav;
6152
6153 switch (mLayout.getParagraphAlignment(line)) {
6154 case ALIGN_NORMAL:
6155 grav = 1;
6156 break;
6157
6158 case ALIGN_OPPOSITE:
6159 grav = -1;
6160 break;
6161
6162 default:
6163 grav = 0;
6164 }
6165
6166 grav *= mLayout.getParagraphDirection(line);
6167
6168 int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6169 int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6170
6171 int hslack = (bottom - top) / 2;
6172 int vslack = hslack;
6173
6174 if (vslack > vspace / 4)
6175 vslack = vspace / 4;
6176 if (hslack > hspace / 4)
6177 hslack = hspace / 4;
6178
6179 int hs = mScrollX;
6180 int vs = mScrollY;
6181
6182 if (top - vs < vslack)
6183 vs = top - vslack;
6184 if (bottom - vs > vspace - vslack)
6185 vs = bottom - (vspace - vslack);
6186 if (ht - vs < vspace)
6187 vs = ht - vspace;
6188 if (0 - vs > 0)
6189 vs = 0;
6190
6191 if (grav != 0) {
6192 if (x - hs < hslack) {
6193 hs = x - hslack;
6194 }
6195 if (x - hs > hspace - hslack) {
6196 hs = x - (hspace - hslack);
6197 }
6198 }
6199
6200 if (grav < 0) {
6201 if (left - hs > 0)
6202 hs = left;
6203 if (right - hs < hspace)
6204 hs = right - hspace;
6205 } else if (grav > 0) {
6206 if (right - hs < hspace)
6207 hs = right - hspace;
6208 if (left - hs > 0)
6209 hs = left;
6210 } else /* grav == 0 */ {
6211 if (right - left <= hspace) {
6212 /*
6213 * If the entire text fits, center it exactly.
6214 */
6215 hs = left - (hspace - (right - left)) / 2;
6216 } else if (x > right - hslack) {
6217 /*
6218 * If we are near the right edge, keep the right edge
6219 * at the edge of the view.
6220 */
6221 hs = right - hspace;
6222 } else if (x < left + hslack) {
6223 /*
6224 * If we are near the left edge, keep the left edge
6225 * at the edge of the view.
6226 */
6227 hs = left;
6228 } else if (left > hs) {
6229 /*
6230 * Is there whitespace visible at the left? Fix it if so.
6231 */
6232 hs = left;
6233 } else if (right < hs + hspace) {
6234 /*
6235 * Is there whitespace visible at the right? Fix it if so.
6236 */
6237 hs = right - hspace;
6238 } else {
6239 /*
6240 * Otherwise, float as needed.
6241 */
6242 if (x - hs < hslack) {
6243 hs = x - hslack;
6244 }
6245 if (x - hs > hspace - hslack) {
6246 hs = x - (hspace - hslack);
6247 }
6248 }
6249 }
6250
6251 if (hs != mScrollX || vs != mScrollY) {
6252 if (mScroller == null) {
6253 scrollTo(hs, vs);
6254 } else {
6255 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6256 int dx = hs - mScrollX;
6257 int dy = vs - mScrollY;
6258
6259 if (duration > ANIMATED_SCROLL_GAP) {
6260 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
Mike Cleronf116bf82009-09-27 19:14:12 -07006261 awakenScrollBars(mScroller.getDuration());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006262 invalidate();
6263 } else {
6264 if (!mScroller.isFinished()) {
6265 mScroller.abortAnimation();
6266 }
6267
6268 scrollBy(dx, dy);
6269 }
6270
6271 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6272 }
6273
6274 changed = true;
6275 }
6276
6277 if (isFocused()) {
6278 // This offsets because getInterestingRect() is in terms of
6279 // viewport coordinates, but requestRectangleOnScreen()
6280 // is in terms of content coordinates.
6281
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006282 Rect r = new Rect(x, top, x + 1, bottom);
6283 getInterestingRect(r, line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006284 r.offset(mScrollX, mScrollY);
6285
6286 if (requestRectangleOnScreen(r)) {
6287 changed = true;
6288 }
6289 }
6290
6291 return changed;
6292 }
6293
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006294 /**
6295 * Move the cursor, if needed, so that it is at an offset that is visible
6296 * to the user. This will not move the cursor if it represents more than
6297 * one character (a selection range). This will only work if the
6298 * TextView contains spannable text; otherwise it will do nothing.
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07006299 *
Gilles Debunne57f4e5b2010-06-21 16:21:51 -07006300 * @return True if the cursor was actually moved, false otherwise.
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006301 */
6302 public boolean moveCursorToVisibleOffset() {
6303 if (!(mText instanceof Spannable)) {
6304 return false;
6305 }
Gilles Debunne05336272010-07-09 20:13:45 -07006306 int start = getSelectionStart();
6307 int end = getSelectionEnd();
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006308 if (start != end) {
6309 return false;
6310 }
6311
6312 // First: make sure the line is visible on screen:
6313
6314 int line = mLayout.getLineForOffset(start);
6315
6316 final int top = mLayout.getLineTop(line);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006317 final int bottom = mLayout.getLineTop(line + 1);
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006318 final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6319 int vslack = (bottom - top) / 2;
6320 if (vslack > vspace / 4)
6321 vslack = vspace / 4;
6322 final int vs = mScrollY;
6323
6324 if (top < (vs+vslack)) {
6325 line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
6326 } else if (bottom > (vspace+vs-vslack)) {
6327 line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
6328 }
6329
6330 // Next: make sure the character is visible on screen:
6331
6332 final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6333 final int hs = mScrollX;
6334 final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
6335 final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
6336
Doug Feltc982f602010-05-25 11:51:40 -07006337 // line might contain bidirectional text
6338 final int lowChar = leftChar < rightChar ? leftChar : rightChar;
6339 final int highChar = leftChar > rightChar ? leftChar : rightChar;
6340
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006341 int newStart = start;
Doug Feltc982f602010-05-25 11:51:40 -07006342 if (newStart < lowChar) {
6343 newStart = lowChar;
6344 } else if (newStart > highChar) {
6345 newStart = highChar;
Dianne Hackborn38e98fc2009-03-24 18:18:24 -07006346 }
6347
6348 if (newStart != start) {
6349 Selection.setSelection((Spannable)mText, newStart);
6350 return true;
6351 }
6352
6353 return false;
6354 }
6355
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006356 @Override
6357 public void computeScroll() {
6358 if (mScroller != null) {
6359 if (mScroller.computeScrollOffset()) {
6360 mScrollX = mScroller.getCurrX();
6361 mScrollY = mScroller.getCurrY();
Romain Guy0fd89bf2011-01-26 15:41:30 -08006362 invalidateParentCaches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006363 postInvalidate(); // So we draw again
6364 }
6365 }
6366 }
6367
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006368 private void getInterestingRect(Rect r, int line) {
6369 convertFromViewportToContentCoordinates(r);
6370
6371 // Rectangle can can be expanded on first and last line to take
6372 // padding into account.
6373 // TODO Take left/right padding into account too?
6374 if (line == 0) r.top -= getExtendedPaddingTop();
6375 if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
6376 }
6377
6378 private void convertFromViewportToContentCoordinates(Rect r) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07006379 final int horizontalOffset = viewportToContentHorizontalOffset();
6380 r.left += horizontalOffset;
6381 r.right += horizontalOffset;
6382
6383 final int verticalOffset = viewportToContentVerticalOffset();
6384 r.top += verticalOffset;
6385 r.bottom += verticalOffset;
6386 }
6387
6388 private int viewportToContentHorizontalOffset() {
6389 return getCompoundPaddingLeft() - mScrollX;
6390 }
6391
6392 private int viewportToContentVerticalOffset() {
6393 int offset = getExtendedPaddingTop() - mScrollY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006394 if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07006395 offset += getVerticalOffset(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006396 }
Gilles Debunne44c1e4c2010-09-08 19:33:20 -07006397 return offset;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006398 }
6399
6400 @Override
6401 public void debug(int depth) {
6402 super.debug(depth);
6403
6404 String output = debugIndent(depth);
6405 output += "frame={" + mLeft + ", " + mTop + ", " + mRight
6406 + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
6407 + "} ";
6408
6409 if (mText != null) {
6410
6411 output += "mText=\"" + mText + "\" ";
6412 if (mLayout != null) {
6413 output += "mLayout width=" + mLayout.getWidth()
6414 + " height=" + mLayout.getHeight();
6415 }
6416 } else {
6417 output += "mText=NULL";
6418 }
6419 Log.d(VIEW_LOG_TAG, output);
6420 }
6421
6422 /**
6423 * Convenience for {@link Selection#getSelectionStart}.
6424 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07006425 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006426 public int getSelectionStart() {
6427 return Selection.getSelectionStart(getText());
6428 }
6429
6430 /**
6431 * Convenience for {@link Selection#getSelectionEnd}.
6432 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -07006433 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006434 public int getSelectionEnd() {
6435 return Selection.getSelectionEnd(getText());
6436 }
6437
6438 /**
6439 * Return true iff there is a selection inside this text view.
6440 */
6441 public boolean hasSelection() {
Gilles Debunne03789e82010-09-07 19:07:17 -07006442 final int selectionStart = getSelectionStart();
6443 final int selectionEnd = getSelectionEnd();
6444
6445 return selectionStart >= 0 && selectionStart != selectionEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006446 }
6447
6448 /**
6449 * Sets the properties of this field (lines, horizontally scrolling,
6450 * transformation method) to be for a single-line input.
6451 *
6452 * @attr ref android.R.styleable#TextView_singleLine
6453 */
6454 public void setSingleLine() {
6455 setSingleLine(true);
6456 }
6457
6458 /**
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08006459 * If true, sets the properties of this field (number of lines, horizontally scrolling,
6460 * transformation method) to be for a single-line input; if false, restores these to the default
6461 * conditions.
6462 *
6463 * Note that the default conditions are not necessarily those that were in effect prior this
6464 * method, and you may want to reset these properties to your custom values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006465 *
6466 * @attr ref android.R.styleable#TextView_singleLine
6467 */
6468 @android.view.RemotableViewMethod
6469 public void setSingleLine(boolean singleLine) {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08006470 // Could be used, but may break backward compatibility.
6471 // if (mSingleLine == singleLine) return;
Gilles Debunned7483bf2010-11-10 10:47:45 -08006472 setInputTypeSingleLine(singleLine);
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08006473 applySingleLine(singleLine, true, true);
Gilles Debunned7483bf2010-11-10 10:47:45 -08006474 }
6475
6476 /**
6477 * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
6478 * @param singleLine
6479 */
6480 private void setInputTypeSingleLine(boolean singleLine) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08006481 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006482 if (singleLine) {
6483 mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6484 } else {
6485 mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6486 }
6487 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006488 }
6489
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08006490 private void applySingleLine(boolean singleLine, boolean applyTransformation,
6491 boolean changeMaxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006492 mSingleLine = singleLine;
6493 if (singleLine) {
6494 setLines(1);
6495 setHorizontallyScrolling(true);
6496 if (applyTransformation) {
Gilles Debunne91a08cf2010-11-08 17:34:49 -08006497 setTransformationMethod(SingleLineTransformationMethod.getInstance());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006498 }
6499 } else {
Gilles Debunnea3ae4a02010-12-13 17:22:39 -08006500 if (changeMaxLines) {
6501 setMaxLines(Integer.MAX_VALUE);
6502 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006503 setHorizontallyScrolling(false);
6504 if (applyTransformation) {
6505 setTransformationMethod(null);
6506 }
6507 }
6508 }
Gilles Debunneb2316962010-12-21 17:32:43 -08006509
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006510 /**
6511 * Causes words in the text that are longer than the view is wide
6512 * to be ellipsized instead of broken in the middle. You may also
6513 * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
Kenny Roote855d132009-06-11 11:00:42 -05006514 * to constrain the text to a single line. Use <code>null</code>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006515 * to turn off ellipsizing.
6516 *
6517 * @attr ref android.R.styleable#TextView_ellipsize
6518 */
6519 public void setEllipsize(TextUtils.TruncateAt where) {
6520 mEllipsize = where;
6521
6522 if (mLayout != null) {
6523 nullLayouts();
6524 requestLayout();
6525 invalidate();
6526 }
6527 }
6528
6529 /**
6530 * Sets how many times to repeat the marquee animation. Only applied if the
6531 * TextView has marquee enabled. Set to -1 to repeat indefinitely.
6532 *
6533 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
6534 */
6535 public void setMarqueeRepeatLimit(int marqueeLimit) {
6536 mMarqueeRepeatLimit = marqueeLimit;
6537 }
6538
6539 /**
6540 * Returns where, if anywhere, words that are longer than the view
6541 * is wide should be ellipsized.
6542 */
6543 @ViewDebug.ExportedProperty
6544 public TextUtils.TruncateAt getEllipsize() {
6545 return mEllipsize;
6546 }
6547
6548 /**
6549 * Set the TextView so that when it takes focus, all the text is
6550 * selected.
6551 *
6552 * @attr ref android.R.styleable#TextView_selectAllOnFocus
6553 */
6554 @android.view.RemotableViewMethod
6555 public void setSelectAllOnFocus(boolean selectAllOnFocus) {
6556 mSelectAllOnFocus = selectAllOnFocus;
6557
6558 if (selectAllOnFocus && !(mText instanceof Spannable)) {
6559 setText(mText, BufferType.SPANNABLE);
6560 }
6561 }
6562
6563 /**
6564 * Set whether the cursor is visible. The default is true.
6565 *
6566 * @attr ref android.R.styleable#TextView_cursorVisible
6567 */
6568 @android.view.RemotableViewMethod
6569 public void setCursorVisible(boolean visible) {
Gilles Debunne3d010062011-02-18 14:16:41 -08006570 if (mCursorVisible != visible) {
6571 mCursorVisible = visible;
6572 invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006573
Gilles Debunne3d010062011-02-18 14:16:41 -08006574 makeBlink();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07006575
Gilles Debunne3d010062011-02-18 14:16:41 -08006576 // InsertionPointCursorController depends on mCursorVisible
6577 prepareCursorControllers();
6578 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006579 }
6580
Gilles Debunne98dbfd42011-01-24 12:54:10 -08006581 private boolean isCursorVisible() {
6582 return mCursorVisible && isTextEditable();
6583 }
6584
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006585 private boolean canMarquee() {
6586 int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
6587 return width > 0 && mLayout.getLineWidth(0) > width;
6588 }
6589
6590 private void startMarquee() {
Romain Guy4dc4f732009-06-19 15:16:40 -07006591 // Do not ellipsize EditText
6592 if (mInput != null) return;
6593
Romain Guy939151f2009-04-08 14:22:40 -07006594 if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
6595 return;
6596 }
6597
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006598 if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
6599 getLineCount() == 1 && canMarquee()) {
Romain Guy939151f2009-04-08 14:22:40 -07006600
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006601 if (mMarquee == null) mMarquee = new Marquee(this);
6602 mMarquee.start(mMarqueeRepeatLimit);
6603 }
6604 }
6605
6606 private void stopMarquee() {
6607 if (mMarquee != null && !mMarquee.isStopped()) {
6608 mMarquee.stop();
6609 }
6610 }
6611
6612 private void startStopMarquee(boolean start) {
6613 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6614 if (start) {
6615 startMarquee();
6616 } else {
6617 stopMarquee();
6618 }
6619 }
6620 }
6621
6622 private static final class Marquee extends Handler {
6623 // TODO: Add an option to configure this
Romain Guy939151f2009-04-08 14:22:40 -07006624 private static final float MARQUEE_DELTA_MAX = 0.07f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006625 private static final int MARQUEE_DELAY = 1200;
6626 private static final int MARQUEE_RESTART_DELAY = 1200;
6627 private static final int MARQUEE_RESOLUTION = 1000 / 30;
6628 private static final int MARQUEE_PIXELS_PER_SECOND = 30;
6629
6630 private static final byte MARQUEE_STOPPED = 0x0;
6631 private static final byte MARQUEE_STARTING = 0x1;
6632 private static final byte MARQUEE_RUNNING = 0x2;
6633
6634 private static final int MESSAGE_START = 0x1;
6635 private static final int MESSAGE_TICK = 0x2;
6636 private static final int MESSAGE_RESTART = 0x3;
6637
6638 private final WeakReference<TextView> mView;
6639
6640 private byte mStatus = MARQUEE_STOPPED;
Gilles Debunnee15b3582010-06-16 15:17:21 -07006641 private final float mScrollUnit;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006642 private float mMaxScroll;
Romain Guyc2303192009-04-03 17:37:18 -07006643 float mMaxFadeScroll;
6644 private float mGhostStart;
6645 private float mGhostOffset;
6646 private float mFadeStop;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006647 private int mRepeatLimit;
6648
6649 float mScroll;
6650
6651 Marquee(TextView v) {
6652 final float density = v.getContext().getResources().getDisplayMetrics().density;
Gilles Debunnee15b3582010-06-16 15:17:21 -07006653 mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006654 mView = new WeakReference<TextView>(v);
6655 }
6656
6657 @Override
6658 public void handleMessage(Message msg) {
6659 switch (msg.what) {
6660 case MESSAGE_START:
6661 mStatus = MARQUEE_RUNNING;
6662 tick();
6663 break;
6664 case MESSAGE_TICK:
6665 tick();
6666 break;
6667 case MESSAGE_RESTART:
6668 if (mStatus == MARQUEE_RUNNING) {
6669 if (mRepeatLimit >= 0) {
6670 mRepeatLimit--;
6671 }
6672 start(mRepeatLimit);
6673 }
6674 break;
6675 }
6676 }
6677
6678 void tick() {
6679 if (mStatus != MARQUEE_RUNNING) {
6680 return;
6681 }
6682
6683 removeMessages(MESSAGE_TICK);
6684
6685 final TextView textView = mView.get();
6686 if (textView != null && (textView.isFocused() || textView.isSelected())) {
6687 mScroll += mScrollUnit;
6688 if (mScroll > mMaxScroll) {
6689 mScroll = mMaxScroll;
6690 sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
6691 } else {
6692 sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
6693 }
6694 textView.invalidate();
6695 }
6696 }
6697
6698 void stop() {
6699 mStatus = MARQUEE_STOPPED;
6700 removeMessages(MESSAGE_START);
6701 removeMessages(MESSAGE_RESTART);
6702 removeMessages(MESSAGE_TICK);
6703 resetScroll();
6704 }
6705
6706 private void resetScroll() {
6707 mScroll = 0.0f;
6708 final TextView textView = mView.get();
6709 if (textView != null) textView.invalidate();
6710 }
6711
6712 void start(int repeatLimit) {
6713 if (repeatLimit == 0) {
6714 stop();
6715 return;
6716 }
6717 mRepeatLimit = repeatLimit;
6718 final TextView textView = mView.get();
6719 if (textView != null && textView.mLayout != null) {
6720 mStatus = MARQUEE_STARTING;
6721 mScroll = 0.0f;
Romain Guyc2303192009-04-03 17:37:18 -07006722 final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
6723 textView.getCompoundPaddingRight();
6724 final float lineWidth = textView.mLayout.getLineWidth(0);
Romain Guy3373ed62009-05-04 14:13:32 -07006725 final float gap = textWidth / 3.0f;
6726 mGhostStart = lineWidth - textWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07006727 mMaxScroll = mGhostStart + textWidth;
Romain Guy3373ed62009-05-04 14:13:32 -07006728 mGhostOffset = lineWidth + gap;
Romain Guyc2303192009-04-03 17:37:18 -07006729 mFadeStop = lineWidth + textWidth / 6.0f;
6730 mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
6731
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006732 textView.invalidate();
6733 sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
6734 }
6735 }
6736
Romain Guyc2303192009-04-03 17:37:18 -07006737 float getGhostOffset() {
6738 return mGhostOffset;
6739 }
6740
6741 boolean shouldDrawLeftFade() {
6742 return mScroll <= mFadeStop;
6743 }
6744
6745 boolean shouldDrawGhost() {
6746 return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
6747 }
6748
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006749 boolean isRunning() {
6750 return mStatus == MARQUEE_RUNNING;
6751 }
6752
6753 boolean isStopped() {
6754 return mStatus == MARQUEE_STOPPED;
6755 }
6756 }
6757
6758 /**
6759 * This method is called when the text is changed, in case any
6760 * subclasses would like to know.
6761 *
6762 * @param text The text the TextView is displaying.
6763 * @param start The offset of the start of the range of the text
6764 * that was modified.
6765 * @param before The offset of the former end of the range of the
6766 * text that was modified. If text was simply inserted,
6767 * this will be the same as <code>start</code>.
6768 * If text was replaced with new text or deleted, the
6769 * length of the old text was <code>before-start</code>.
6770 * @param after The offset of the end of the range of the text
6771 * that was modified. If text was simply deleted,
6772 * this will be the same as <code>start</code>.
6773 * If text was replaced with new text or inserted,
6774 * the length of the new text is <code>after-start</code>.
6775 */
6776 protected void onTextChanged(CharSequence text,
6777 int start, int before, int after) {
6778 }
6779
6780 /**
6781 * This method is called when the selection has changed, in case any
6782 * subclasses would like to know.
6783 *
6784 * @param selStart The new selection start location.
6785 * @param selEnd The new selection end location.
6786 */
6787 protected void onSelectionChanged(int selStart, int selEnd) {
6788 }
6789
6790 /**
6791 * Adds a TextWatcher to the list of those whose methods are called
6792 * whenever this TextView's text changes.
6793 * <p>
6794 * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
6795 * not called after {@link #setText} calls. Now, doing {@link #setText}
6796 * if there are any text changed listeners forces the buffer type to
6797 * Editable if it would not otherwise be and does call this method.
6798 */
6799 public void addTextChangedListener(TextWatcher watcher) {
6800 if (mListeners == null) {
6801 mListeners = new ArrayList<TextWatcher>();
6802 }
6803
6804 mListeners.add(watcher);
6805 }
6806
6807 /**
6808 * Removes the specified TextWatcher from the list of those whose
6809 * methods are called
6810 * whenever this TextView's text changes.
6811 */
6812 public void removeTextChangedListener(TextWatcher watcher) {
6813 if (mListeners != null) {
6814 int i = mListeners.indexOf(watcher);
6815
6816 if (i >= 0) {
6817 mListeners.remove(i);
6818 }
6819 }
6820 }
6821
6822 private void sendBeforeTextChanged(CharSequence text, int start, int before,
6823 int after) {
6824 if (mListeners != null) {
6825 final ArrayList<TextWatcher> list = mListeners;
6826 final int count = list.size();
6827 for (int i = 0; i < count; i++) {
6828 list.get(i).beforeTextChanged(text, start, before, after);
6829 }
6830 }
6831 }
6832
6833 /**
6834 * Not private so it can be called from an inner class without going
6835 * through a thunk.
6836 */
6837 void sendOnTextChanged(CharSequence text, int start, int before,
6838 int after) {
6839 if (mListeners != null) {
6840 final ArrayList<TextWatcher> list = mListeners;
6841 final int count = list.size();
6842 for (int i = 0; i < count; i++) {
6843 list.get(i).onTextChanged(text, start, before, after);
6844 }
6845 }
6846 }
6847
6848 /**
6849 * Not private so it can be called from an inner class without going
6850 * through a thunk.
6851 */
6852 void sendAfterTextChanged(Editable text) {
6853 if (mListeners != null) {
6854 final ArrayList<TextWatcher> list = mListeners;
6855 final int count = list.size();
6856 for (int i = 0; i < count; i++) {
6857 list.get(i).afterTextChanged(text);
6858 }
6859 }
6860 }
6861
6862 /**
6863 * Not private so it can be called from an inner class without going
6864 * through a thunk.
6865 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08006866 void handleTextChanged(CharSequence buffer, int start, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006867 final InputMethodState ims = mInputMethodState;
6868 if (ims == null || ims.mBatchEditNesting == 0) {
6869 updateAfterEdit();
6870 }
6871 if (ims != null) {
6872 ims.mContentChanged = true;
6873 if (ims.mChangedStart < 0) {
6874 ims.mChangedStart = start;
6875 ims.mChangedEnd = start+before;
6876 } else {
Viktor Yakovel964be412010-02-17 08:35:57 +01006877 ims.mChangedStart = Math.min(ims.mChangedStart, start);
6878 ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006879 }
6880 ims.mChangedDelta += after-before;
6881 }
6882
6883 sendOnTextChanged(buffer, start, before, after);
6884 onTextChanged(buffer, start, before, after);
Adam Powellba0a2c32010-09-28 17:41:23 -07006885
Gilles Debunned94f8c52011-01-10 11:29:15 -08006886 // Hide the controllers if the amount of content changed
Adam Powellba0a2c32010-09-28 17:41:23 -07006887 if (before != after) {
6888 hideControllers();
6889 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006890 }
6891
6892 /**
6893 * Not private so it can be called from an inner class without going
6894 * through a thunk.
6895 */
Gilles Debunnecf9cf2f2010-12-08 17:43:58 -08006896 void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006897 // XXX Make the start and end move together if this ends up
6898 // spending too much time invalidating.
6899
6900 boolean selChanged = false;
6901 int newSelStart=-1, newSelEnd=-1;
6902
6903 final InputMethodState ims = mInputMethodState;
6904
6905 if (what == Selection.SELECTION_END) {
6906 mHighlightPathBogus = true;
6907 selChanged = true;
6908 newSelEnd = newStart;
6909
6910 if (!isFocused()) {
6911 mSelectionMoved = true;
6912 }
6913
6914 if (oldStart >= 0 || newStart >= 0) {
6915 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
6916 registerForPreDraw();
Gilles Debunne3d010062011-02-18 14:16:41 -08006917 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006918 }
6919 }
6920
6921 if (what == Selection.SELECTION_START) {
6922 mHighlightPathBogus = true;
6923 selChanged = true;
6924 newSelStart = newStart;
6925
6926 if (!isFocused()) {
6927 mSelectionMoved = true;
6928 }
6929
6930 if (oldStart >= 0 || newStart >= 0) {
6931 int end = Selection.getSelectionEnd(buf);
6932 invalidateCursor(end, oldStart, newStart);
6933 }
6934 }
6935
6936 if (selChanged) {
6937 if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
6938 if (newSelStart < 0) {
6939 newSelStart = Selection.getSelectionStart(buf);
6940 }
6941 if (newSelEnd < 0) {
6942 newSelEnd = Selection.getSelectionEnd(buf);
6943 }
6944 onSelectionChanged(newSelStart, newSelEnd);
6945 }
6946 }
6947
6948 if (what instanceof UpdateAppearance ||
6949 what instanceof ParagraphStyle) {
6950 if (ims == null || ims.mBatchEditNesting == 0) {
6951 invalidate();
6952 mHighlightPathBogus = true;
6953 checkForResize();
6954 } else {
6955 ims.mContentChanged = true;
6956 }
6957 }
6958
6959 if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
6960 mHighlightPathBogus = true;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07006961 if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
6962 ims.mSelectionModeChanged = true;
6963 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006964
6965 if (Selection.getSelectionStart(buf) >= 0) {
6966 if (ims == null || ims.mBatchEditNesting == 0) {
6967 invalidateCursor();
6968 } else {
6969 ims.mCursorChanged = true;
6970 }
6971 }
6972 }
6973
6974 if (what instanceof ParcelableSpan) {
6975 // If this is a span that can be sent to a remote process,
6976 // the current extract editor would be interested in it.
6977 if (ims != null && ims.mExtracting != null) {
6978 if (ims.mBatchEditNesting != 0) {
6979 if (oldStart >= 0) {
6980 if (ims.mChangedStart > oldStart) {
6981 ims.mChangedStart = oldStart;
6982 }
6983 if (ims.mChangedStart > oldEnd) {
6984 ims.mChangedStart = oldEnd;
6985 }
6986 }
6987 if (newStart >= 0) {
6988 if (ims.mChangedStart > newStart) {
6989 ims.mChangedStart = newStart;
6990 }
6991 if (ims.mChangedStart > newEnd) {
6992 ims.mChangedStart = newEnd;
6993 }
6994 }
6995 } else {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07006996 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08006997 + oldStart + "-" + oldEnd + ","
6998 + newStart + "-" + newEnd + what);
6999 ims.mContentChanged = true;
7000 }
7001 }
7002 }
7003 }
7004
7005 private class ChangeWatcher
7006 implements TextWatcher, SpanWatcher {
svetoslavganov75986cf2009-05-14 22:28:01 -07007007
7008 private CharSequence mBeforeText;
7009
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007010 public void beforeTextChanged(CharSequence buffer, int start,
7011 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007012 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007013 + " before=" + before + " after=" + after + ": " + buffer);
svetoslavganov75986cf2009-05-14 22:28:01 -07007014
Amith Yamasani91ccdb52010-01-14 18:56:02 -08007015 if (AccessibilityManager.getInstance(mContext).isEnabled()
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08007016 && !isPasswordInputType(mInputType)
7017 && !hasPasswordTransformationMethod()) {
svetoslavganov75986cf2009-05-14 22:28:01 -07007018 mBeforeText = buffer.toString();
7019 }
7020
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007021 TextView.this.sendBeforeTextChanged(buffer, start, before, after);
7022 }
7023
7024 public void onTextChanged(CharSequence buffer, int start,
7025 int before, int after) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007026 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007027 + " before=" + before + " after=" + after + ": " + buffer);
7028 TextView.this.handleTextChanged(buffer, start, before, after);
svetoslavganov75986cf2009-05-14 22:28:01 -07007029
7030 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
7031 (isFocused() || isSelected() &&
7032 isShown())) {
7033 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
7034 mBeforeText = null;
7035 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007036 }
7037
7038 public void afterTextChanged(Editable buffer) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007039 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007040 TextView.this.sendAfterTextChanged(buffer);
7041
7042 if (MetaKeyKeyListener.getMetaState(buffer,
7043 MetaKeyKeyListener.META_SELECTING) != 0) {
7044 MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
7045 }
7046 }
7047
7048 public void onSpanChanged(Spannable buf,
7049 Object what, int s, int e, int st, int en) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007050 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007051 + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
7052 TextView.this.spanChange(buf, what, s, st, e, en);
7053 }
7054
7055 public void onSpanAdded(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007056 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007057 + " what=" + what + ": " + buf);
7058 TextView.this.spanChange(buf, what, -1, s, -1, e);
7059 }
7060
7061 public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007062 if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007063 + " what=" + what + ": " + buf);
7064 TextView.this.spanChange(buf, what, s, -1, e, -1);
7065 }
7066 }
7067
Romain Guydcc490f2010-02-24 17:59:35 -08007068 /**
7069 * @hide
7070 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007071 @Override
Romain Guya440b002010-02-24 15:57:54 -08007072 public void dispatchFinishTemporaryDetach() {
7073 mDispatchTemporaryDetach = true;
7074 super.dispatchFinishTemporaryDetach();
7075 mDispatchTemporaryDetach = false;
7076 }
7077
7078 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007079 public void onStartTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08007080 super.onStartTemporaryDetach();
7081 // Only track when onStartTemporaryDetach() is called directly,
7082 // usually because this instance is an editable field in a list
7083 if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
Gilles Debunne4b2274f2011-02-25 15:18:03 -08007084
7085 // Because of View recycling in ListView, there is no easy way to know when a TextView with
7086 // selection becomes visible again. Until a better solution is found, stop text selection
7087 // mode (if any) as soon as this TextView is recycled.
7088 stopSelectionActionMode();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007089 }
7090
7091 @Override
7092 public void onFinishTemporaryDetach() {
Romain Guya440b002010-02-24 15:57:54 -08007093 super.onFinishTemporaryDetach();
7094 // Only track when onStartTemporaryDetach() is called directly,
7095 // usually because this instance is an editable field in a list
7096 if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007097 }
7098
7099 @Override
7100 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
7101 if (mTemporaryDetach) {
7102 // If we are temporarily in the detach state, then do nothing.
7103 super.onFocusChanged(focused, direction, previouslyFocusedRect);
7104 return;
7105 }
7106
7107 mShowCursor = SystemClock.uptimeMillis();
7108
7109 ensureEndedBatchEdit();
Gilles Debunne03789e82010-09-07 19:07:17 -07007110
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007111 if (focused) {
7112 int selStart = getSelectionStart();
7113 int selEnd = getSelectionEnd();
7114
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08007115 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
7116 // mode for these, unless there was a specific selection already started.
7117 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
7118 selEnd == mText.length();
7119 mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
7120
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007121 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
Gilles Debunne528c64882010-10-08 11:56:13 -07007122 // If a tap was used to give focus to that view, move cursor at tap position.
Gilles Debunne64e54a62010-09-07 19:07:17 -07007123 // Has to be done before onTakeFocus, which can be overloaded.
Gilles Debunne380b6042010-10-08 16:12:11 -07007124 final int lastTapPosition = getLastTapPosition();
7125 if (lastTapPosition >= 0) {
7126 Selection.setSelection((Spannable) mText, lastTapPosition);
7127 }
Gilles Debunne2703a422010-08-23 15:14:03 -07007128
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007129 if (mMovement != null) {
7130 mMovement.onTakeFocus(this, (Spannable) mText, direction);
7131 }
7132
Gilles Debunne64e54a62010-09-07 19:07:17 -07007133 // The DecorView does not have focus when the 'Done' ExtractEditText button is
7134 // pressed. Since it is the ViewRoot's mView, it requests focus before
7135 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
7136 // This special case ensure that we keep current selection in that case.
7137 // It would be better to know why the DecorView does not have focus at that time.
Gilles Debunne47fa8e82010-09-07 18:50:07 -07007138 if (((this instanceof ExtractEditText) || mSelectionMoved) &&
7139 selStart >= 0 && selEnd >= 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007140 /*
7141 * Someone intentionally set the selection, so let them
7142 * do whatever it is that they wanted to do instead of
7143 * the default on-focus behavior. We reset the selection
7144 * here instead of just skipping the onTakeFocus() call
7145 * because some movement methods do something other than
7146 * just setting the selection in theirs and we still
7147 * need to go through that path.
7148 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007149 Selection.setSelection((Spannable) mText, selStart, selEnd);
7150 }
Gilles Debunnef170a342010-11-11 11:08:59 -08007151
7152 if (mSelectAllOnFocus) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08007153 selectAll();
Gilles Debunnef170a342010-11-11 11:08:59 -08007154 }
7155
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007156 mTouchFocusSelected = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007157 }
7158
7159 mFrozenWithFocus = false;
7160 mSelectionMoved = false;
7161
7162 if (mText instanceof Spannable) {
7163 Spannable sp = (Spannable) mText;
7164 MetaKeyKeyListener.resetMetaState(sp);
7165 }
7166
7167 makeBlink();
7168
7169 if (mError != null) {
7170 showError();
7171 }
7172 } else {
7173 if (mError != null) {
7174 hideError();
7175 }
7176 // Don't leave us in the middle of a batch edit.
7177 onEndBatchEdit();
Gilles Debunne05336272010-07-09 20:13:45 -07007178
Gilles Debunne64e54a62010-09-07 19:07:17 -07007179 if (this instanceof ExtractEditText) {
7180 // terminateTextSelectionMode removes selection, which we want to keep when
7181 // ExtractEditText goes out of focus.
7182 final int selStart = getSelectionStart();
7183 final int selEnd = getSelectionEnd();
Gilles Debunneb7012e842011-02-24 15:40:38 -08007184 hideControllers();
Gilles Debunne64e54a62010-09-07 19:07:17 -07007185 Selection.setSelection((Spannable) mText, selStart, selEnd);
7186 } else {
Gilles Debunneb7012e842011-02-24 15:40:38 -08007187 hideControllers();
Gilles Debunne64e54a62010-09-07 19:07:17 -07007188 }
Gilles Debunne380b6042010-10-08 16:12:11 -07007189
Gilles Debunnee587d832010-11-23 20:20:11 -08007190 // No need to create the controller
Gilles Debunne380b6042010-10-08 16:12:11 -07007191 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08007192 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunne380b6042010-10-08 16:12:11 -07007193 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007194 }
7195
7196 startStopMarquee(focused);
7197
7198 if (mTransformation != null) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07007199 mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007200 }
7201
7202 super.onFocusChanged(focused, direction, previouslyFocusedRect);
Gilles Debunnec3e85a72011-01-21 08:46:06 -08007203
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08007204 // Performed after super.onFocusChanged so that this TextView is registered and can ask for
7205 // the IME. Showing the IME while focus is moved using the D-Pad is a bad idea, however this
7206 // does not happen in that case (using the arrows on a bluetooth keyboard).
7207 if (focused && isTextEditable()) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08007208 final InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunnead8484b2011-02-17 17:37:51 -08007209 if (imm != null) imm.showSoftInput(this, 0);
Gilles Debunnec3e85a72011-01-21 08:46:06 -08007210 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007211 }
7212
Gilles Debunne380b6042010-10-08 16:12:11 -07007213 private int getLastTapPosition() {
Gilles Debunnee587d832010-11-23 20:20:11 -08007214 // No need to create the controller at that point, no last tap position saved
Gilles Debunne528c64882010-10-08 11:56:13 -07007215 if (mSelectionModifierCursorController != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08007216 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
Gilles Debunne380b6042010-10-08 16:12:11 -07007217 if (lastTapPosition >= 0) {
Gilles Debunne528c64882010-10-08 11:56:13 -07007218 // Safety check, should not be possible.
Gilles Debunne380b6042010-10-08 16:12:11 -07007219 if (lastTapPosition > mText.length()) {
7220 Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
Gilles Debunne528c64882010-10-08 11:56:13 -07007221 + mText.length() + ")");
Gilles Debunne380b6042010-10-08 16:12:11 -07007222 lastTapPosition = mText.length();
Gilles Debunne528c64882010-10-08 11:56:13 -07007223 }
Gilles Debunne380b6042010-10-08 16:12:11 -07007224 return lastTapPosition;
Gilles Debunne528c64882010-10-08 11:56:13 -07007225 }
7226 }
Gilles Debunne380b6042010-10-08 16:12:11 -07007227
7228 return -1;
Gilles Debunne528c64882010-10-08 11:56:13 -07007229 }
7230
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007231 @Override
7232 public void onWindowFocusChanged(boolean hasWindowFocus) {
7233 super.onWindowFocusChanged(hasWindowFocus);
7234
7235 if (hasWindowFocus) {
7236 if (mBlink != null) {
7237 mBlink.uncancel();
Gilles Debunne3d010062011-02-18 14:16:41 -08007238 makeBlink();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007239 }
7240 } else {
7241 if (mBlink != null) {
7242 mBlink.cancel();
7243 }
7244 // Don't leave us in the middle of a batch edit.
7245 onEndBatchEdit();
7246 if (mInputContentType != null) {
7247 mInputContentType.enterDown = false;
7248 }
Gilles Debunnee507a9e2010-10-10 12:44:18 -07007249 hideControllers();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007250 }
7251
7252 startStopMarquee(hasWindowFocus);
7253 }
7254
Adam Powellba0a2c32010-09-28 17:41:23 -07007255 @Override
7256 protected void onVisibilityChanged(View changedView, int visibility) {
7257 super.onVisibilityChanged(changedView, visibility);
7258 if (visibility != VISIBLE) {
Gilles Debunnee507a9e2010-10-10 12:44:18 -07007259 hideControllers();
Adam Powellba0a2c32010-09-28 17:41:23 -07007260 }
7261 }
7262
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007263 /**
7264 * Use {@link BaseInputConnection#removeComposingSpans
7265 * BaseInputConnection.removeComposingSpans()} to remove any IME composing
7266 * state from this text view.
7267 */
7268 public void clearComposingText() {
7269 if (mText instanceof Spannable) {
7270 BaseInputConnection.removeComposingSpans((Spannable)mText);
7271 }
7272 }
7273
7274 @Override
7275 public void setSelected(boolean selected) {
7276 boolean wasSelected = isSelected();
7277
7278 super.setSelected(selected);
7279
7280 if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7281 if (selected) {
7282 startMarquee();
7283 } else {
7284 stopMarquee();
7285 }
7286 }
7287 }
7288
7289 @Override
7290 public boolean onTouchEvent(MotionEvent event) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07007291 final int action = event.getActionMasked();
Adam Powell965b9692010-10-21 18:44:32 -07007292
7293 if (hasInsertionController()) {
7294 getInsertionController().onTouchEvent(event);
Gilles Debunne5347c582010-10-27 14:22:35 -07007295 }
Adam Powell965b9692010-10-21 18:44:32 -07007296 if (hasSelectionController()) {
7297 getSelectionController().onTouchEvent(event);
Gilles Debunne5347c582010-10-27 14:22:35 -07007298 }
Adam Powellb08013c2010-09-16 16:28:11 -07007299
Gilles Debunne5347c582010-10-27 14:22:35 -07007300 if (action == MotionEvent.ACTION_DOWN) {
Gilles Debunne9948ad72010-11-24 14:00:46 -08007301 mLastDownPositionX = (int) event.getX();
7302 mLastDownPositionY = (int) event.getY();
7303
The Android Open Source Project4df24232009-03-05 14:34:35 -08007304 // Reset this state; it will be re-set if super.onTouchEvent
7305 // causes focus to move to the view.
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007306 mTouchFocusSelected = false;
Gilles Debunne0eb704c2010-11-30 12:50:54 -08007307 mIgnoreActionUpEvent = false;
The Android Open Source Project4df24232009-03-05 14:34:35 -08007308 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07007309
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007310 final boolean superResult = super.onTouchEvent(event);
7311
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007312 /*
7313 * Don't handle the release after a long press, because it will
7314 * move the selection away from whatever the menu action was
7315 * trying to affect.
7316 */
Gilles Debunne0eb704c2010-11-30 12:50:54 -08007317 if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
7318 mDiscardNextActionUp = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007319 return superResult;
7320 }
7321
Gilles Debunnec3e85a72011-01-21 08:46:06 -08007322 final boolean touchIsFinished = action == MotionEvent.ACTION_UP && !mIgnoreActionUpEvent &&
7323 isFocused();
7324
Janos Levai042856c2010-10-15 02:53:58 +03007325 if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
7326 && mText instanceof Spannable && mLayout != null) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007327 boolean handled = false;
7328
Adam Powell879fb6b2010-09-20 11:23:56 -07007329 final int oldScrollX = mScrollX;
7330 final int oldScrollY = mScrollY;
Gilles Debunne5347c582010-10-27 14:22:35 -07007331
The Android Open Source Projectba87e3e2009-03-13 13:04:22 -07007332 if (mMovement != null) {
7333 handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
7334 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007335
Gilles Debunnec3e85a72011-01-21 08:46:06 -08007336 if (mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable && touchIsFinished) {
Gilles Debunnef3895ed2010-12-21 12:53:58 -08007337 // The LinkMovementMethod which should handle taps on links has not been installed
7338 // to support text selection. We reproduce its behavior here to open links.
7339 ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
7340 getSelectionEnd(), ClickableSpan.class);
7341
7342 if (links.length != 0) {
7343 links[0].onClick(this);
7344 handled = true;
7345 }
7346 }
7347
Gilles Debunnef076eeb2010-11-29 11:32:53 -08007348 if (isTextEditable() || mTextIsSelectable) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08007349 if (mScrollX != oldScrollX || mScrollY != oldScrollY) { // TODO remove
Adam Powell879fb6b2010-09-20 11:23:56 -07007350 // Hide insertion anchor while scrolling. Leave selection.
Gilles Debunnecfc22c52011-03-07 15:50:47 -08007351 hideInsertionPointCursorController(); // TODO any motion should hide it
Adam Powell879fb6b2010-09-20 11:23:56 -07007352 }
Gilles Debunnebbb5d6e2010-06-21 16:21:51 -07007353
Gilles Debunnec3e85a72011-01-21 08:46:06 -08007354 if (touchIsFinished) {
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08007355 // Show the IME, except when selecting in read-only text.
7356 if (!mTextIsSelectable) {
Gilles Debunne17d31de2011-01-27 11:02:18 -08007357 final InputMethodManager imm = InputMethodManager.peekInstance();
Gilles Debunnead8484b2011-02-17 17:37:51 -08007358 handled |= imm != null && imm.showSoftInput(this, 0);
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08007359 }
7360
Gilles Debunnead8484b2011-02-17 17:37:51 -08007361 boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
7362 if (!selectAllGotFocus && hasSelection()) {
7363 startSelectionActionMode();
7364 } else {
7365 stopSelectionActionMode();
7366 if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) {
7367 getInsertionController().show();
7368 }
Gilles Debunne8cbb4c62011-01-24 12:33:56 -08007369 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007370 }
7371 }
7372
The Android Open Source Project4df24232009-03-05 14:34:35 -08007373 if (handled) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007374 return true;
7375 }
7376 }
7377
7378 return superResult;
7379 }
7380
Jeff Brown8f345672011-02-26 13:29:53 -08007381 @Override
7382 public boolean onGenericMotionEvent(MotionEvent event) {
7383 if (mMovement != null && mText instanceof Spannable && mLayout != null) {
7384 try {
7385 if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
7386 return true;
7387 }
7388 } catch (AbstractMethodError ex) {
7389 // onGenericMotionEvent was added to the MovementMethod interface in API 12.
7390 // Ignore its absence in case third party applications implemented the
7391 // interface directly.
7392 }
7393 }
7394 return super.onGenericMotionEvent(event);
7395 }
7396
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007397 private void prepareCursorControllers() {
Adam Powell8c8293b2010-10-12 14:45:12 -07007398 boolean windowSupportsHandles = false;
7399
7400 ViewGroup.LayoutParams params = getRootView().getLayoutParams();
7401 if (params instanceof WindowManager.LayoutParams) {
7402 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
7403 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
7404 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
7405 }
7406
Gilles Debunne98dbfd42011-01-24 12:54:10 -08007407 mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
Adam Powell965b9692010-10-21 18:44:32 -07007408 mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
7409 mLayout != null;
7410
7411 if (!mInsertionControllerEnabled) {
Gilles Debunnef48e83b2010-12-06 18:36:08 -08007412 hideInsertionPointCursorController();
Adam Powell65a1de92011-01-30 15:47:29 -08007413 if (mInsertionPointCursorController != null) {
7414 mInsertionPointCursorController.onDetached();
7415 mInsertionPointCursorController = null;
7416 }
Gilles Debunne05336272010-07-09 20:13:45 -07007417 }
7418
Adam Powell965b9692010-10-21 18:44:32 -07007419 if (!mSelectionControllerEnabled) {
Gilles Debunnee587d832010-11-23 20:20:11 -08007420 stopSelectionActionMode();
Adam Powell65a1de92011-01-30 15:47:29 -08007421 if (mSelectionModifierCursorController != null) {
7422 mSelectionModifierCursorController.onDetached();
7423 mSelectionModifierCursorController = null;
7424 }
Gilles Debunne05336272010-07-09 20:13:45 -07007425 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007426 }
7427
7428 /**
Gilles Debunne86b9c782010-11-11 10:43:48 -08007429 * @return True iff this TextView contains a text that can be edited, or if this is
7430 * a selectable TextView.
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007431 */
7432 private boolean isTextEditable() {
Gilles Debunnef076eeb2010-11-29 11:32:53 -08007433 return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07007434 }
7435
The Android Open Source Project4df24232009-03-05 14:34:35 -08007436 /**
7437 * Returns true, only while processing a touch gesture, if the initial
7438 * 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 -07007439 * its selection changed. Only valid while processing the touch gesture
7440 * of interest.
The Android Open Source Project4df24232009-03-05 14:34:35 -08007441 */
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007442 public boolean didTouchFocusSelect() {
7443 return mTouchFocusSelected;
The Android Open Source Project4df24232009-03-05 14:34:35 -08007444 }
7445
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007446 @Override
7447 public void cancelLongPress() {
7448 super.cancelLongPress();
Gilles Debunne0eb704c2010-11-30 12:50:54 -08007449 mIgnoreActionUpEvent = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007450 }
7451
7452 @Override
7453 public boolean onTrackballEvent(MotionEvent event) {
7454 if (mMovement != null && mText instanceof Spannable &&
7455 mLayout != null) {
7456 if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
7457 return true;
7458 }
7459 }
7460
7461 return super.onTrackballEvent(event);
7462 }
7463
7464 public void setScroller(Scroller s) {
7465 mScroller = s;
7466 }
7467
7468 private static class Blink extends Handler implements Runnable {
Gilles Debunnee15b3582010-06-16 15:17:21 -07007469 private final WeakReference<TextView> mView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007470 private boolean mCancelled;
7471
7472 public Blink(TextView v) {
7473 mView = new WeakReference<TextView>(v);
7474 }
7475
7476 public void run() {
7477 if (mCancelled) {
7478 return;
7479 }
7480
7481 removeCallbacks(Blink.this);
7482
7483 TextView tv = mView.get();
7484
Gilles Debunne3d010062011-02-18 14:16:41 -08007485 if (tv != null && tv.shouldBlink()) {
7486 if (tv.mLayout != null) {
7487 tv.invalidateCursorPath();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007488 }
Gilles Debunne3d010062011-02-18 14:16:41 -08007489
7490 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007491 }
7492 }
7493
7494 void cancel() {
7495 if (!mCancelled) {
7496 removeCallbacks(Blink.this);
7497 mCancelled = true;
7498 }
7499 }
7500
7501 void uncancel() {
7502 mCancelled = false;
7503 }
7504 }
7505
Gilles Debunne3d010062011-02-18 14:16:41 -08007506 /**
7507 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
7508 */
7509 private boolean shouldBlink() {
7510 if (!isFocused()) return false;
7511
7512 final int start = getSelectionStart();
7513 if (start < 0) return false;
7514
7515 final int end = getSelectionEnd();
7516 if (end < 0) return false;
7517
7518 return start == end;
7519 }
7520
7521 private void makeBlink() {
7522 if (isCursorVisible()) {
7523 if (shouldBlink()) {
7524 mShowCursor = SystemClock.uptimeMillis();
7525 if (mBlink == null) mBlink = new Blink(this);
7526 mBlink.removeCallbacks(mBlink);
7527 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
7528 }
7529 } else {
7530 if (mBlink != null) mBlink.removeCallbacks(mBlink);
7531 }
7532 }
7533
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007534 @Override
7535 protected float getLeftFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07007536 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007537 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7538 if (mMarquee != null && !mMarquee.isStopped()) {
7539 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07007540 if (marquee.shouldDrawLeftFade()) {
7541 return marquee.mScroll / getHorizontalFadingEdgeLength();
7542 } else {
7543 return 0.0f;
7544 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007545 } else if (getLineCount() == 1) {
7546 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7547 case Gravity.LEFT:
7548 return 0.0f;
7549 case Gravity.RIGHT:
7550 return (mLayout.getLineRight(0) - (mRight - mLeft) -
7551 getCompoundPaddingLeft() - getCompoundPaddingRight() -
7552 mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
7553 case Gravity.CENTER_HORIZONTAL:
7554 return 0.0f;
7555 }
7556 }
7557 }
7558 return super.getLeftFadingEdgeStrength();
7559 }
7560
7561 @Override
7562 protected float getRightFadingEdgeStrength() {
Romain Guy909cbaf2010-10-13 18:19:48 -07007563 if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007564 if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7565 if (mMarquee != null && !mMarquee.isStopped()) {
7566 final Marquee marquee = mMarquee;
Romain Guyc2303192009-04-03 17:37:18 -07007567 return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007568 } else if (getLineCount() == 1) {
7569 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7570 case Gravity.LEFT:
Romain Guy076dc9f2009-06-24 17:17:51 -07007571 final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
7572 getCompoundPaddingRight();
7573 final float lineWidth = mLayout.getLineWidth(0);
7574 return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007575 case Gravity.RIGHT:
7576 return 0.0f;
7577 case Gravity.CENTER_HORIZONTAL:
Gilles Debunne44c14732010-10-19 11:56:59 -07007578 case Gravity.FILL_HORIZONTAL:
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007579 return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
7580 getCompoundPaddingLeft() - getCompoundPaddingRight())) /
7581 getHorizontalFadingEdgeLength();
7582 }
7583 }
7584 }
7585 return super.getRightFadingEdgeStrength();
7586 }
7587
7588 @Override
7589 protected int computeHorizontalScrollRange() {
Romain Guydac5f9f2010-07-08 11:40:54 -07007590 if (mLayout != null) {
7591 return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
7592 (int) mLayout.getLineWidth(0) : mLayout.getWidth();
7593 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007594
7595 return super.computeHorizontalScrollRange();
7596 }
7597
7598 @Override
7599 protected int computeVerticalScrollRange() {
7600 if (mLayout != null)
7601 return mLayout.getHeight();
7602
7603 return super.computeVerticalScrollRange();
7604 }
7605
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007606 @Override
7607 protected int computeVerticalScrollExtent() {
7608 return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
7609 }
7610
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007611 public enum BufferType {
7612 NORMAL, SPANNABLE, EDITABLE,
7613 }
7614
7615 /**
7616 * Returns the TextView_textColor attribute from the
7617 * Resources.StyledAttributes, if set, or the TextAppearance_textColor
7618 * from the TextView_textAppearance attribute, if TextView_textColor
7619 * was not set directly.
7620 */
7621 public static ColorStateList getTextColors(Context context, TypedArray attrs) {
7622 ColorStateList colors;
7623 colors = attrs.getColorStateList(com.android.internal.R.styleable.
7624 TextView_textColor);
7625
7626 if (colors == null) {
7627 int ap = attrs.getResourceId(com.android.internal.R.styleable.
7628 TextView_textAppearance, -1);
7629 if (ap != -1) {
7630 TypedArray appearance;
7631 appearance = context.obtainStyledAttributes(ap,
7632 com.android.internal.R.styleable.TextAppearance);
7633 colors = appearance.getColorStateList(com.android.internal.R.styleable.
7634 TextAppearance_textColor);
7635 appearance.recycle();
7636 }
7637 }
7638
7639 return colors;
7640 }
7641
7642 /**
7643 * Returns the default color from the TextView_textColor attribute
7644 * from the AttributeSet, if set, or the default color from the
7645 * TextAppearance_textColor from the TextView_textAppearance attribute,
7646 * if TextView_textColor was not set directly.
7647 */
7648 public static int getTextColor(Context context,
7649 TypedArray attrs,
7650 int def) {
7651 ColorStateList colors = getTextColors(context, attrs);
7652
7653 if (colors == null) {
7654 return def;
7655 } else {
7656 return colors.getDefaultColor();
7657 }
7658 }
7659
7660 @Override
7661 public boolean onKeyShortcut(int keyCode, KeyEvent event) {
Jeff Brownc1df9072010-12-21 16:38:50 -08007662 final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
7663 if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
7664 switch (keyCode) {
7665 case KeyEvent.KEYCODE_A:
7666 if (canSelectText()) {
7667 return onTextContextMenuItem(ID_SELECT_ALL);
7668 }
7669 break;
7670 case KeyEvent.KEYCODE_X:
7671 if (canCut()) {
7672 return onTextContextMenuItem(ID_CUT);
7673 }
7674 break;
7675 case KeyEvent.KEYCODE_C:
7676 if (canCopy()) {
7677 return onTextContextMenuItem(ID_COPY);
7678 }
7679 break;
7680 case KeyEvent.KEYCODE_V:
7681 if (canPaste()) {
7682 return onTextContextMenuItem(ID_PASTE);
7683 }
7684 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007685 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007686 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007687 return super.onKeyShortcut(keyCode, event);
7688 }
7689
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007690 /**
7691 * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
7692 * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
7693 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
7694 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07007695 private boolean canSelectText() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08007696 return hasSelectionController() && mText.length() != 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007697 }
7698
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007699 /**
7700 * Test based on the <i>intrinsic</i> charateristics of the TextView.
7701 * The text must be spannable and the movement method must allow for arbitary selection.
7702 *
7703 * See also {@link #canSelectText()}.
7704 */
Gilles Debunnebaaace52010-10-01 15:47:13 -07007705 private boolean textCanBeSelected() {
Gilles Debunne05336272010-07-09 20:13:45 -07007706 // prepareCursorController() relies on this method.
7707 // If you change this condition, make sure prepareCursorController is called anywhere
7708 // the value of this condition might be changed.
Gilles Debunne6da7e932010-12-07 14:28:14 -08007709 return mText instanceof Spannable && mMovement != null && mMovement.canSelectArbitrarily();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007710 }
7711
7712 private boolean canCut() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07007713 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007714 return false;
7715 }
7716
Gilles Debunnef4dceb12010-12-01 15:54:20 -08007717 if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
7718 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007719 }
7720
7721 return false;
7722 }
7723
7724 private boolean canCopy() {
Gilles Debunne0dcad2b2010-10-15 16:29:25 -07007725 if (hasPasswordTransformationMethod()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007726 return false;
7727 }
7728
Gilles Debunne03789e82010-09-07 19:07:17 -07007729 if (mText.length() > 0 && hasSelection()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007730 return true;
7731 }
7732
7733 return false;
7734 }
7735
7736 private boolean canPaste() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007737 return (mText instanceof Editable &&
7738 mInput != null &&
7739 getSelectionStart() >= 0 &&
7740 getSelectionEnd() >= 0 &&
7741 ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07007742 hasPrimaryClip());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007743 }
7744
Gilles Debunne79ff9142011-01-07 10:16:21 -08007745 private boolean isWordCharacter(int c, int type) {
Gilles Debunne8e06a632010-11-30 12:05:55 -08007746 return (c == '\'' || c == '"' ||
7747 type == Character.UPPERCASE_LETTER ||
7748 type == Character.LOWERCASE_LETTER ||
7749 type == Character.TITLECASE_LETTER ||
7750 type == Character.MODIFIER_LETTER ||
Gilles Debunne87380bc2011-01-04 13:24:54 -08007751 type == Character.OTHER_LETTER || // Should handle asian characters
Gilles Debunne8e06a632010-11-30 12:05:55 -08007752 type == Character.DECIMAL_DIGIT_NUMBER);
7753 }
7754
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007755 /**
Gilles Debunne05336272010-07-09 20:13:45 -07007756 * Returns the offsets delimiting the 'word' located at position offset.
7757 *
7758 * @param offset An offset in the text.
7759 * @return The offsets for the start and end of the word located at <code>offset</code>.
Gilles Debunne87380bc2011-01-04 13:24:54 -08007760 * The two ints offsets are packed in a long using {@link #packRangeInLong(int, int)}.
7761 * Returns -1 if no valid word was found.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007762 */
Gilles Debunne05336272010-07-09 20:13:45 -07007763 private long getWordLimitsAt(int offset) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007764 int klass = mInputType & InputType.TYPE_MASK_CLASS;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007765 int variation = mInputType & InputType.TYPE_MASK_VARIATION;
Gilles Debunne8e06a632010-11-30 12:05:55 -08007766
7767 // Text selection is not permitted in password fields
Gilles Debunne87380bc2011-01-04 13:24:54 -08007768 if (hasPasswordTransformationMethod()) {
Gilles Debunne05336272010-07-09 20:13:45 -07007769 return -1;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07007770 }
7771
Gilles Debunne8e06a632010-11-30 12:05:55 -08007772 final int len = mText.length();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007773
Gilles Debunne8e06a632010-11-30 12:05:55 -08007774 // Specific text fields: always select the entire text
7775 if (klass == InputType.TYPE_CLASS_NUMBER ||
7776 klass == InputType.TYPE_CLASS_PHONE ||
7777 klass == InputType.TYPE_CLASS_DATETIME ||
7778 variation == InputType.TYPE_TEXT_VARIATION_URI ||
7779 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
7780 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
7781 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
7782 return len > 0 ? packRangeInLong(0, len) : -1;
7783 }
7784
7785 int end = Math.min(offset, len);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007786 if (end < 0) {
Gilles Debunne05336272010-07-09 20:13:45 -07007787 return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007788 }
7789
Gilles Debunne8e06a632010-11-30 12:05:55 -08007790 final int MAX_LENGTH = 48;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007791 int start = end;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007792
7793 for (; start > 0; start--) {
Gilles Debunne79ff9142011-01-07 10:16:21 -08007794 final char c = mTransformed.charAt(start - 1);
7795 final int type = Character.getType(c);
7796 if (start == end && type == Character.OTHER_PUNCTUATION) {
Gilles Debunne07a11f42010-12-10 12:25:52 -08007797 // Cases where the text ends with a '.' and we select from the end of the line
7798 // (right after the dot), or when we select from the space character in "aaa, bbb".
Gilles Debunne79ff9142011-01-07 10:16:21 -08007799 continue;
7800 }
7801 if (type == Character.SURROGATE) { // Two Character codepoint
7802 end = start - 1; // Recheck as a pair when scanning forward
7803 continue;
Gilles Debunne07a11f42010-12-10 12:25:52 -08007804 }
Gilles Debunne79ff9142011-01-07 10:16:21 -08007805 if (!isWordCharacter(c, type)) break;
Gilles Debunne8e06a632010-11-30 12:05:55 -08007806 if ((end - start) > MAX_LENGTH) return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007807 }
7808
7809 for (; end < len; end++) {
Gilles Debunne79ff9142011-01-07 10:16:21 -08007810 final int c = Character.codePointAt(mTransformed, end);
7811 final int type = Character.getType(c);
7812 if (!isWordCharacter(c, type)) break;
Gilles Debunne8e06a632010-11-30 12:05:55 -08007813 if ((end - start) > MAX_LENGTH) return -1;
Gilles Debunne79ff9142011-01-07 10:16:21 -08007814 if (c > 0xFFFF) { // Two Character codepoint
7815 end++;
Eric Fischer56cf7882009-07-29 15:52:44 -07007816 }
7817 }
Gilles Debunne05336272010-07-09 20:13:45 -07007818
Gilles Debunne79ff9142011-01-07 10:16:21 -08007819 if (start == end) {
Gilles Debunne05336272010-07-09 20:13:45 -07007820 return -1;
Eric Fischer56cf7882009-07-29 15:52:44 -07007821 }
7822
Gilles Debunne05336272010-07-09 20:13:45 -07007823 // Two ints packed in a long
Gilles Debunnecf1e9252010-10-07 20:46:03 -07007824 return packRangeInLong(start, end);
7825 }
7826
7827 private static long packRangeInLong(int start, int end) {
Gilles Debunne05336272010-07-09 20:13:45 -07007828 return (((long) start) << 32) | end;
7829 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007830
Gilles Debunnecf1e9252010-10-07 20:46:03 -07007831 private static int extractRangeStartFromLong(long range) {
7832 return (int) (range >>> 32);
7833 }
7834
7835 private static int extractRangeEndFromLong(long range) {
7836 return (int) (range & 0x00000000FFFFFFFFL);
7837 }
Gilles Debunnecbfbb522010-10-07 16:57:31 -07007838
Gilles Debunnef4dceb12010-12-01 15:54:20 -08007839 private void selectAll() {
7840 Selection.setSelection((Spannable) mText, 0, mText.length());
7841 }
7842
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07007843 private void selectCurrentWord() {
Gilles Debunne6da7e932010-12-07 14:28:14 -08007844 if (!canSelectText()) {
7845 return;
7846 }
7847
Gilles Debunne710a9102010-11-23 16:50:28 -08007848 if (hasPasswordTransformationMethod()) {
Gilles Debunne87380bc2011-01-04 13:24:54 -08007849 // Always select all on a password field.
Gilles Debunne4dfe0862010-12-17 15:46:28 -08007850 // Cut/copy menu entries are not available for passwords, but being able to select all
7851 // is however useful to delete or paste to replace the entire content.
Gilles Debunnef4dceb12010-12-01 15:54:20 -08007852 selectAll();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07007853 return;
7854 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007855
Gilles Debunne87380bc2011-01-04 13:24:54 -08007856 long lastTouchOffsets = getLastTouchOffsets();
7857 final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
7858 final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
Gilles Debunne4dfe0862010-12-17 15:46:28 -08007859
7860 int selectionStart, selectionEnd;
7861
7862 // If a URLSpan (web address, email, phone...) is found at that position, select it.
7863 URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
7864 if (urlSpans.length == 1) {
7865 URLSpan url = urlSpans[0];
7866 selectionStart = ((Spanned) mText).getSpanStart(url);
7867 selectionEnd = ((Spanned) mText).getSpanEnd(url);
7868 } else {
7869 long wordLimits = getWordLimitsAt(minOffset);
7870 if (wordLimits >= 0) {
7871 selectionStart = extractRangeStartFromLong(wordLimits);
7872 } else {
7873 selectionStart = Math.max(minOffset - 5, 0);
7874 }
7875
7876 wordLimits = getWordLimitsAt(maxOffset);
7877 if (wordLimits >= 0) {
7878 selectionEnd = extractRangeEndFromLong(wordLimits);
7879 } else {
7880 selectionEnd = Math.min(maxOffset + 5, mText.length());
7881 }
7882 }
7883
7884 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
7885 }
7886
7887 private long getLastTouchOffsets() {
Gilles Debunnecf1e9252010-10-07 20:46:03 -07007888 int minOffset, maxOffset;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07007889
Gilles Debunne528c64882010-10-08 11:56:13 -07007890 if (mContextMenuTriggeredByKey) {
Gilles Debunnecf1e9252010-10-07 20:46:03 -07007891 minOffset = getSelectionStart();
7892 maxOffset = getSelectionEnd();
7893 } else {
Gilles Debunnee587d832010-11-23 20:20:11 -08007894 SelectionModifierCursorController selectionController = getSelectionController();
7895 minOffset = selectionController.getMinTouchOffset();
7896 maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne03789e82010-09-07 19:07:17 -07007897 }
7898
Gilles Debunne4dfe0862010-12-17 15:46:28 -08007899 return packRangeInLong(minOffset, maxOffset);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07007900 }
Gilles Debunnedf4ee432010-08-25 19:13:48 -07007901
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007902 @Override
svetoslavganov75986cf2009-05-14 22:28:01 -07007903 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganovbe7565942010-03-10 11:51:20 -08007904 if (!isShown()) {
7905 return false;
7906 }
7907
Svetoslav Ganov1d1e1102010-11-16 16:44:03 -08007908 final boolean isPassword = hasPasswordTransformationMethod();
svetoslavganov75986cf2009-05-14 22:28:01 -07007909
7910 if (!isPassword) {
7911 CharSequence text = getText();
7912 if (TextUtils.isEmpty(text)) {
7913 text = getHint();
7914 }
7915 if (!TextUtils.isEmpty(text)) {
7916 if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) {
7917 text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1);
7918 }
7919 event.getText().add(text);
7920 }
7921 } else {
7922 event.setPassword(isPassword);
7923 }
7924 return false;
7925 }
7926
7927 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
7928 int fromIndex, int removedCount, int addedCount) {
7929 AccessibilityEvent event =
7930 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
7931 event.setFromIndex(fromIndex);
7932 event.setRemovedCount(removedCount);
7933 event.setAddedCount(addedCount);
7934 event.setBeforeText(beforeText);
7935 sendAccessibilityEventUnchecked(event);
7936 }
7937
7938 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007939 protected void onCreateContextMenu(ContextMenu menu) {
7940 super.onCreateContextMenu(menu);
7941 boolean added = false;
Gilles Debunne528c64882010-10-08 11:56:13 -07007942 mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
7943 // Problem with context menu on long press: the menu appears while the key in down and when
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007944 // the key is released, the view does not receive the key_up event.
7945 // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
7946 // events. We cannot simply clear these flags in onTextContextMenuItem since
Gilles Debunne528c64882010-10-08 11:56:13 -07007947 // it may not be called (if the user/ discards the context menu with the back key).
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007948 // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
7949 // available in onTextContextMenuItem.
Gilles Debunne528c64882010-10-08 11:56:13 -07007950 mDPadCenterIsDown = mEnterKeyIsDown = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007951
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007952 MenuHandler handler = new MenuHandler();
7953
Gilles Debunneb0db5942011-01-04 13:58:54 -08007954 if (mText instanceof Spanned && hasSelectionController()) {
Gilles Debunne4dfe0862010-12-17 15:46:28 -08007955 long lastTouchOffset = getLastTouchOffsets();
7956 final int selStart = extractRangeStartFromLong(lastTouchOffset);
7957 final int selEnd = extractRangeEndFromLong(lastTouchOffset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007958
Gilles Debunne4dfe0862010-12-17 15:46:28 -08007959 URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007960 if (urls.length > 0) {
7961 menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
7962 setOnMenuItemClickListener(handler);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007963
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007964 added = true;
7965 }
7966 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007967
7968 // The context menu is not empty, which will prevent the selection mode from starting.
7969 // Add a entry to start it in the context menu.
7970 // TODO Does not handle the case where a subclass does not call super.thisMethod or
7971 // populates the menu AFTER this call.
7972 if (menu.size() > 0) {
7973 menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
Gilles Debunnecbcb3452010-12-17 15:31:02 -08007974 setOnMenuItemClickListener(handler);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007975 added = true;
7976 }
7977
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007978 if (added) {
7979 menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
7980 }
7981 }
7982
7983 /**
7984 * Returns whether this text view is a current input method target. The
7985 * default implementation just checks with {@link InputMethodManager}.
7986 */
7987 public boolean isInputMethodTarget() {
7988 InputMethodManager imm = InputMethodManager.peekInstance();
7989 return imm != null && imm.isActive(this);
7990 }
7991
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007992 // Selection context mode
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007993 private static final int ID_SELECT_ALL = android.R.id.selectAll;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007994 private static final int ID_CUT = android.R.id.cut;
7995 private static final int ID_COPY = android.R.id.copy;
7996 private static final int ID_PASTE = android.R.id.paste;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007997 // Context menu entries
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08007998 private static final int ID_COPY_URL = android.R.id.copyUrl;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07007999 private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008000
8001 private class MenuHandler implements MenuItem.OnMenuItemClickListener {
8002 public boolean onMenuItemClick(MenuItem item) {
8003 return onTextContextMenuItem(item.getItemId());
8004 }
8005 }
8006
8007 /**
8008 * Called when a context menu option for the text view is selected. Currently
Jeff Brownc1df9072010-12-21 16:38:50 -08008009 * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
8010 * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
8011 * or {@link android.R.id#copy}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008012 */
8013 public boolean onTextContextMenuItem(int id) {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008014 int min = 0;
8015 int max = mText.length();
Gilles Debunne64e54a62010-09-07 19:07:17 -07008016
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008017 if (isFocused()) {
Gilles Debunne64e54a62010-09-07 19:07:17 -07008018 final int selStart = getSelectionStart();
8019 final int selEnd = getSelectionEnd();
8020
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008021 min = Math.max(0, Math.min(selStart, selEnd));
8022 max = Math.max(0, Math.max(selStart, selEnd));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008023 }
8024
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008025 switch (id) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008026 case ID_COPY_URL:
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008027 URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008028 if (urls.length >= 1) {
Dianne Hackborn1040dc42010-08-26 22:11:06 -07008029 ClipData clip = null;
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008030 for (int i=0; i<urls.length; i++) {
8031 Uri uri = Uri.parse(urls[0].getURL());
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008032 if (clip == null) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -08008033 clip = ClipData.newRawUri(null, uri);
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008034 } else {
Dianne Hackborn1040dc42010-08-26 22:11:06 -07008035 clip.addItem(new ClipData.Item(uri));
Dianne Hackborn23fdaf62010-08-06 12:16:55 -07008036 }
8037 }
Dianne Hackborn1040dc42010-08-26 22:11:06 -07008038 if (clip != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008039 setPrimaryClip(clip);
Dianne Hackborn1040dc42010-08-26 22:11:06 -07008040 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008041 }
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008042 stopSelectionActionMode();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008043 return true;
8044
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008045 case ID_SELECTION_MODE:
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008046 if (mSelectionActionMode != null) {
8047 // Selection mode is already started, simply change selected part.
8048 updateSelectedRegion();
8049 } else {
8050 startSelectionActionMode();
8051 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008052 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008053
Jeff Brownc1df9072010-12-21 16:38:50 -08008054 case ID_SELECT_ALL:
Gilles Debunne299733e2011-02-07 17:11:41 -08008055 // This does not enter text selection mode. Text is highlighted, so that it can be
8056 // bulk edited, like selectAllOnFocus does.
Jeff Brownc1df9072010-12-21 16:38:50 -08008057 selectAll();
Jeff Brownc1df9072010-12-21 16:38:50 -08008058 return true;
8059
8060 case ID_PASTE:
8061 paste(min, max);
8062 return true;
8063
8064 case ID_CUT:
Dianne Hackborn327fbd22011-01-17 14:38:50 -08008065 setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08008066 ((Editable) mText).delete(min, max);
8067 stopSelectionActionMode();
8068 return true;
8069
8070 case ID_COPY:
Dianne Hackborn327fbd22011-01-17 14:38:50 -08008071 setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
Jeff Brownc1df9072010-12-21 16:38:50 -08008072 stopSelectionActionMode();
8073 return true;
8074 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008075 return false;
8076 }
8077
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008078 /**
8079 * Prepare text so that there are not zero or two spaces at beginning and end of region defined
8080 * by [min, max] when replacing this region by paste.
Gilles Debunnec0752ee2010-12-22 17:50:42 -08008081 * 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 -08008082 * make sure we do not add an extra one from the paste content.
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008083 */
8084 private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
Gilles Debunnec0752ee2010-12-22 17:50:42 -08008085 if (paste.length() > 0) {
8086 if (min > 0) {
8087 final char charBefore = mTransformed.charAt(min - 1);
8088 final char charAfter = paste.charAt(0);
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008089
Gilles Debunnec0752ee2010-12-22 17:50:42 -08008090 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8091 // Two spaces at beginning of paste: remove one
8092 final int originalLength = mText.length();
8093 ((Editable) mText).delete(min - 1, min);
8094 // Due to filters, there is no guarantee that exactly one character was
8095 // removed: count instead.
8096 final int delta = mText.length() - originalLength;
8097 min += delta;
8098 max += delta;
8099 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8100 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8101 // No space at beginning of paste: add one
8102 final int originalLength = mText.length();
8103 ((Editable) mText).replace(min, min, " ");
8104 // Taking possible filters into account as above.
8105 final int delta = mText.length() - originalLength;
8106 min += delta;
8107 max += delta;
8108 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008109 }
Gilles Debunnec0752ee2010-12-22 17:50:42 -08008110
8111 if (max < mText.length()) {
8112 final char charBefore = paste.charAt(paste.length() - 1);
8113 final char charAfter = mTransformed.charAt(max);
8114
8115 if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8116 // Two spaces at end of paste: remove one
8117 ((Editable) mText).delete(max, max + 1);
8118 } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8119 !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8120 // No space at end of paste: add one
8121 ((Editable) mText).replace(max, max, " ");
8122 }
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008123 }
8124 }
Gilles Debunne4ae0f292010-11-29 14:56:39 -08008125
Gilles Debunnecf1e9252010-10-07 20:46:03 -07008126 return packRangeInLong(min, max);
8127 }
8128
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008129 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
8130 TextView shadowView = (TextView) inflate(mContext,
Gilles Debunnef170a342010-11-11 11:08:59 -08008131 com.android.internal.R.layout.text_drag_thumbnail, null);
8132
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008133 if (shadowView == null) {
Gilles Debunnef170a342010-11-11 11:08:59 -08008134 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
8135 }
8136
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008137 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
8138 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
Gilles Debunnef170a342010-11-11 11:08:59 -08008139 }
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008140 shadowView.setText(text);
8141 shadowView.setTextColor(getTextColors());
Gilles Debunnef170a342010-11-11 11:08:59 -08008142
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008143 shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
8144 shadowView.setGravity(Gravity.CENTER);
Gilles Debunnef170a342010-11-11 11:08:59 -08008145
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008146 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
Gilles Debunnef170a342010-11-11 11:08:59 -08008147 ViewGroup.LayoutParams.WRAP_CONTENT));
8148
8149 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008150 shadowView.measure(size, size);
Gilles Debunnef170a342010-11-11 11:08:59 -08008151
Christopher Tate36d4c3f2011-01-07 13:34:24 -08008152 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
8153 shadowView.invalidate();
8154 return new DragShadowBuilder(shadowView);
Gilles Debunnef170a342010-11-11 11:08:59 -08008155 }
8156
Gilles Debunneaaa84792010-12-03 11:10:14 -08008157 private static class DragLocalState {
8158 public TextView sourceTextView;
8159 public int start, end;
8160
8161 public DragLocalState(TextView sourceTextView, int start, int end) {
8162 this.sourceTextView = sourceTextView;
8163 this.start = start;
8164 this.end = end;
8165 }
8166 }
8167
Gilles Debunnee15b3582010-06-16 15:17:21 -07008168 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008169 public boolean performLongClick() {
8170 if (super.performLongClick()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008171 mDiscardNextActionUp = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008172 return true;
8173 }
Gilles Debunnef170a342010-11-11 11:08:59 -08008174
Gilles Debunne299733e2011-02-07 17:11:41 -08008175 boolean handled = false;
8176
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08008177 // Long press in empty space moves cursor and shows the Paste affordance if available.
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008178 if (!isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
8179 mInsertionControllerEnabled) {
Gilles Debunne9948ad72010-11-24 14:00:46 -08008180 final int offset = getOffset(mLastDownPositionX, mLastDownPositionY);
Gilles Debunned1dc72a2010-11-30 10:16:35 -08008181 stopSelectionActionMode();
Gilles Debunne9948ad72010-11-24 14:00:46 -08008182 Selection.setSelection((Spannable)mText, offset);
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008183 getInsertionController().show(0);
Gilles Debunne299733e2011-02-07 17:11:41 -08008184 handled = true;
Gilles Debunne9948ad72010-11-24 14:00:46 -08008185 }
8186
Gilles Debunne299733e2011-02-07 17:11:41 -08008187 if (!handled && mSelectionActionMode != null) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008188 if (touchPositionIsInSelection()) {
8189 // Start a drag
8190 final int start = getSelectionStart();
8191 final int end = getSelectionEnd();
8192 CharSequence selectedText = mTransformed.subSequence(start, end);
Dianne Hackborn327fbd22011-01-17 14:38:50 -08008193 ClipData data = ClipData.newPlainText(null, selectedText);
Gilles Debunneaaa84792010-12-03 11:10:14 -08008194 DragLocalState localState = new DragLocalState(this, start, end);
Christopher Tate02d2b3b2011-01-10 20:43:53 -08008195 startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008196 stopSelectionActionMode();
8197 } else {
Gilles Debunne299733e2011-02-07 17:11:41 -08008198 // New selection at touch position
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008199 updateSelectedRegion();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008200 }
Gilles Debunne299733e2011-02-07 17:11:41 -08008201 handled = true;
Gilles Debunnef170a342010-11-11 11:08:59 -08008202 }
8203
Gilles Debunne33a8cfb2010-12-10 12:00:42 -08008204 // Start a new selection
Gilles Debunne299733e2011-02-07 17:11:41 -08008205 handled |= !handled && startSelectionActionMode();
8206
8207 if (handled) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008208 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
Gilles Debunne0eb704c2010-11-30 12:50:54 -08008209 mDiscardNextActionUp = true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008210 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008211
Gilles Debunne299733e2011-02-07 17:11:41 -08008212 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08008213 }
8214
Gilles Debunne4dfe0862010-12-17 15:46:28 -08008215 /**
8216 * When selection mode is already started, this method simply updates the selected part of text
8217 * to the text under the finger.
8218 */
8219 private void updateSelectedRegion() {
8220 // Start a new selection at current position, keep selectionAction mode on
8221 selectCurrentWord();
8222 // Updates handles' positions
8223 getSelectionController().show();
8224 }
8225
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008226 private boolean touchPositionIsInSelection() {
8227 int selectionStart = getSelectionStart();
8228 int selectionEnd = getSelectionEnd();
Gilles Debunne05336272010-07-09 20:13:45 -07008229
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008230 if (selectionStart == selectionEnd) {
8231 return false;
8232 }
Gilles Debunne05336272010-07-09 20:13:45 -07008233
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008234 if (selectionStart > selectionEnd) {
8235 int tmp = selectionStart;
8236 selectionStart = selectionEnd;
8237 selectionEnd = tmp;
Gilles Debunne05336272010-07-09 20:13:45 -07008238 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008239 }
Gilles Debunne05336272010-07-09 20:13:45 -07008240
Gilles Debunnee587d832010-11-23 20:20:11 -08008241 SelectionModifierCursorController selectionController = getSelectionController();
8242 int minOffset = selectionController.getMinTouchOffset();
8243 int maxOffset = selectionController.getMaxTouchOffset();
Gilles Debunne05336272010-07-09 20:13:45 -07008244
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008245 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
8246 }
8247
8248 /**
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008249 * If provided, this ActionMode.Callback will be used to create the ActionMode when text
8250 * selection is initiated in this View.
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008251 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008252 * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
8253 * Paste actions, depending on what this View supports.
8254 *
8255 * A custom implementation can add new entries in the default menu in its
8256 * {@link ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The default actions
8257 * can also be removed from the menu using {@link Menu#removeItem(int)} and passing
8258 * {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy} or
8259 * {@link android.R.id#paste} ids as parameters.
8260 *
Gilles Debunneddd6f392011-01-27 09:48:01 -08008261 * Returning false from {@link ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will
8262 * prevent the action mode from being started.
8263 *
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008264 * Action click events should be handled by the custom implementation of
8265 * {@link ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
8266 *
8267 * Note that text selection mode is not started when a TextView receives focus and the
8268 * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
8269 * that case, to allow for quick replacement.
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008270 */
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008271 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
8272 mCustomSelectionActionModeCallback = actionModeCallback;
8273 }
8274
8275 /**
8276 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
8277 *
8278 * @return The current custom selection callback.
8279 */
8280 public ActionMode.Callback getCustomSelectionActionModeCallback() {
8281 return mCustomSelectionActionModeCallback;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008282 }
8283
8284 /**
8285 *
8286 * @return true if the selection mode was actually started.
8287 */
8288 private boolean startSelectionActionMode() {
8289 if (mSelectionActionMode != null) {
8290 // Selection action mode is already started
8291 return false;
8292 }
8293
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008294 if (!canSelectText() || !requestFocus()) {
8295 Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
8296 return false;
8297 }
8298
Gilles Debunnec01f3fe2010-12-22 17:07:36 -08008299 if (!hasSelection()) {
8300 // If selection mode is started after a device rotation, there is already a selection.
8301 selectCurrentWord();
8302 }
8303
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008304 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
Gilles Debunne6da7e932010-12-07 14:28:14 -08008305 mSelectionActionMode = startActionMode(actionModeCallback);
Gilles Debunne17d31de2011-01-27 11:02:18 -08008306 final boolean selectionStarted = mSelectionActionMode != null;
8307
8308 if (selectionStarted && !mTextIsSelectable) {
8309 // Show the IME to be able to replace text, except when selecting non editable text.
8310 final InputMethodManager imm = InputMethodManager.peekInstance();
8311 if (imm != null) imm.showSoftInput(this, 0, null);
8312 }
8313
8314 return selectionStarted;
Gilles Debunne05336272010-07-09 20:13:45 -07008315 }
8316
Gilles Debunneed279f82010-08-18 21:24:35 -07008317 private void stopSelectionActionMode() {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008318 if (mSelectionActionMode != null) {
Gilles Debunned94f8c52011-01-10 11:29:15 -08008319 // This will hide the mSelectionModifierCursorController
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008320 mSelectionActionMode.finish();
8321 }
8322 }
8323
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008324 /**
8325 * Paste clipboard content between min and max positions.
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008326 */
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008327 private void paste(int min, int max) {
8328 ClipboardManager clipboard =
8329 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008330 ClipData clip = clipboard.getPrimaryClip();
8331 if (clip != null) {
8332 boolean didfirst = false;
8333 for (int i=0; i<clip.getItemCount(); i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -08008334 CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008335 if (paste != null) {
8336 if (!didfirst) {
8337 long minMax = prepareSpacesAroundPaste(min, max, paste);
8338 min = extractRangeStartFromLong(minMax);
8339 max = extractRangeEndFromLong(minMax);
8340 Selection.setSelection((Spannable) mText, max);
8341 ((Editable) mText).replace(min, max, paste);
8342 } else {
8343 ((Editable) mText).insert(getSelectionEnd(), "\n");
8344 ((Editable) mText).insert(getSelectionEnd(), paste);
8345 }
8346 }
8347 }
8348 stopSelectionActionMode();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008349 sLastCutOrCopyTime = 0;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008350 }
8351 }
8352
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008353 private void setPrimaryClip(ClipData clip) {
8354 ClipboardManager clipboard = (ClipboardManager) getContext().
8355 getSystemService(Context.CLIPBOARD_SERVICE);
8356 clipboard.setPrimaryClip(clip);
8357 sLastCutOrCopyTime = SystemClock.uptimeMillis();
8358 }
8359
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008360 /**
8361 * An ActionMode Callback class that is used to provide actions while in text selection mode.
8362 *
8363 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
8364 * on which of these this TextView supports.
8365 */
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008366 private class SelectionActionModeCallback implements ActionMode.Callback {
8367
8368 @Override
8369 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Gilles Debunne78996c92010-10-12 16:01:47 -07008370 TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
8371
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008372 mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle));
8373 mode.setSubtitle(null);
8374
Gilles Debunnecbcb3452010-12-17 15:31:02 -08008375 menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Adam Powelld8404b22010-10-13 14:26:41 -07008376 setAlphabeticShortcut('a').
8377 setShowAsAction(
8378 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008379
8380 if (canCut()) {
8381 menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008382 setIcon(styledAttributes.getResourceId(
8383 R.styleable.Theme_actionModeCutDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -07008384 setAlphabeticShortcut('x').
8385 setShowAsAction(
8386 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008387 }
8388
8389 if (canCopy()) {
8390 menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008391 setIcon(styledAttributes.getResourceId(
8392 R.styleable.Theme_actionModeCopyDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -07008393 setAlphabeticShortcut('c').
8394 setShowAsAction(
8395 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008396 }
8397
8398 if (canPaste()) {
8399 menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008400 setIcon(styledAttributes.getResourceId(
8401 R.styleable.Theme_actionModePasteDrawable, 0)).
Adam Powelld8404b22010-10-13 14:26:41 -07008402 setAlphabeticShortcut('v').
8403 setShowAsAction(
8404 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008405 }
8406
Gilles Debunne78996c92010-10-12 16:01:47 -07008407 styledAttributes.recycle();
8408
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008409 if (mCustomSelectionActionModeCallback != null) {
Gilles Debunneddd6f392011-01-27 09:48:01 -08008410 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
8411 // The custom mode can choose to cancel the action mode
8412 return false;
8413 }
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008414 }
8415
8416 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Gilles Debunnee587d832010-11-23 20:20:11 -08008417 getSelectionController().show();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008418 return true;
8419 } else {
8420 return false;
8421 }
Gilles Debunne05336272010-07-09 20:13:45 -07008422 }
8423
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008424 @Override
8425 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008426 if (mCustomSelectionActionModeCallback != null) {
8427 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
8428 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008429 return true;
8430 }
8431
8432 @Override
8433 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008434 if (mCustomSelectionActionModeCallback != null &&
8435 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
8436 return true;
8437 }
Jeff Brownc1df9072010-12-21 16:38:50 -08008438 return onTextContextMenuItem(item.getItemId());
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008439 }
8440
8441 @Override
8442 public void onDestroyActionMode(ActionMode mode) {
Gilles Debunnef4dceb12010-12-01 15:54:20 -08008443 if (mCustomSelectionActionModeCallback != null) {
8444 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
8445 }
Gilles Debunned94f8c52011-01-10 11:29:15 -08008446 Selection.setSelection((Spannable) mText, getSelectionEnd());
8447
8448 if (mSelectionModifierCursorController != null) {
8449 mSelectionModifierCursorController.hide();
8450 }
8451
Gilles Debunnef788a9f2010-07-22 10:17:23 -07008452 mSelectionActionMode = null;
8453 }
8454 }
Gilles Debunne05336272010-07-09 20:13:45 -07008455
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008456 /**
8457 * A CursorController instance can be used to control a cursor in the text.
Jeff Brown01ce2e92010-09-26 22:20:12 -07008458 * It is not used outside of {@link TextView}.
Adam Powell879fb6b2010-09-20 11:23:56 -07008459 * @hide
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008460 */
Adam Powell624380a2010-10-02 18:12:02 -07008461 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008462 /**
8463 * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
8464 * See also {@link #hide()}.
8465 */
8466 public void show();
8467
8468 /**
8469 * Hide the cursor controller from screen.
8470 * See also {@link #show()}.
8471 */
8472 public void hide();
8473
8474 /**
Adam Powellb08013c2010-09-16 16:28:11 -07008475 * @return true if the CursorController is currently visible
8476 */
8477 public boolean isShowing();
8478
8479 /**
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008480 * Update the controller's position.
8481 */
Jeff Brown01ce2e92010-09-26 22:20:12 -07008482 public void updatePosition(HandleView handle, int x, int y);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008483
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008484 public void updateOffset(HandleView handle, int offset);
8485
Adam Powell879fb6b2010-09-20 11:23:56 -07008486 public void updatePosition();
8487
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008488 public int getCurrentOffset(HandleView handle);
8489
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008490 /**
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008491 * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
8492 * a chance to become active and/or visible.
8493 * @param event The touch event
8494 */
Adam Powellb08013c2010-09-16 16:28:11 -07008495 public boolean onTouchEvent(MotionEvent event);
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08008496
8497 /**
8498 * Called when the view is detached from window. Perform house keeping task, such as
8499 * stopping Runnable thread that would otherwise keep a reference on the context, thus
8500 * preventing the activity to be recycled.
8501 */
8502 public void onDetached();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07008503 }
8504
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008505 private class PastePopupMenu implements OnClickListener {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008506 private final PopupWindow mContainer;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008507 private int mPositionX;
8508 private int mPositionY;
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008509 private final View[] mPasteViews = new View[4];
8510 private final int[] mPasteViewLayouts = new int[] {
8511 mTextEditPasteWindowLayout, mTextEditNoPasteWindowLayout,
8512 mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout };
8513
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008514 public PastePopupMenu() {
8515 mContainer = new PopupWindow(TextView.this.mContext, null,
8516 com.android.internal.R.attr.textSelectHandleWindowStyle);
8517 mContainer.setSplitTouchEnabled(true);
8518 mContainer.setClippingEnabled(false);
8519 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
8520
8521 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
8522 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
8523 }
8524
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008525 private int viewIndex(boolean onTop) {
8526 return (onTop ? 0 : 1<<1) + (canPaste() ? 0 : 1<<0);
8527 }
8528
8529 private void updateContent(boolean onTop) {
8530 final int viewIndex = viewIndex(onTop);
8531 View view = mPasteViews[viewIndex];
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008532
8533 if (view == null) {
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008534 final int layout = mPasteViewLayouts[viewIndex];
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008535 LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
8536 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
8537 if (inflater != null) {
8538 view = inflater.inflate(layout, null);
8539 }
8540
8541 if (view == null) {
8542 throw new IllegalArgumentException("Unable to inflate TextEdit paste window");
8543 }
8544
8545 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8546 view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
8547 ViewGroup.LayoutParams.WRAP_CONTENT));
8548 view.measure(size, size);
8549
8550 view.setOnClickListener(this);
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008551
8552 mPasteViews[viewIndex] = view;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008553 }
8554
8555 mContainer.setContentView(view);
8556 }
8557
8558 public void show() {
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008559 updateContent(true);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008560 positionAtCursor();
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008561 }
8562
8563 public void hide() {
8564 mContainer.dismiss();
8565 }
8566
8567 public boolean isShowing() {
8568 return mContainer.isShowing();
8569 }
8570
8571 @Override
8572 public void onClick(View v) {
Gilles Debunnee1c14e62010-11-03 19:24:29 -07008573 if (canPaste()) {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08008574 paste(getSelectionStart(), getSelectionEnd());
Gilles Debunnee1c14e62010-11-03 19:24:29 -07008575 }
8576 hide();
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008577 }
8578
8579 void positionAtCursor() {
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008580 View contentView = mContainer.getContentView();
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008581 int width = contentView.getMeasuredWidth();
8582 int height = contentView.getMeasuredHeight();
8583 final int offset = TextView.this.getSelectionStart();
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008584 final int line = mLayout.getLineForOffset(offset);
8585 final int lineTop = mLayout.getLineTop(line);
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008586 float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008587
8588 final Rect bounds = sCursorControllerTempRect;
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008589 bounds.left = (int) (primaryHorizontal - width / 2.0f);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008590 bounds.top = lineTop - height;
8591
8592 bounds.right = bounds.left + width;
8593 bounds.bottom = bounds.top + height;
8594
8595 convertFromViewportToContentCoordinates(bounds);
8596
8597 mPositionX = bounds.left;
8598 mPositionY = bounds.top;
Gilles Debunnee60e1e52011-01-20 12:19:44 -08008599
8600
8601 final int[] coords = mTempCoords;
8602 TextView.this.getLocationInWindow(coords);
8603 coords[0] += mPositionX;
8604 coords[1] += mPositionY;
8605
8606 final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
8607 if (coords[1] < 0) {
8608 updateContent(false);
8609 // Update dimensions from new view
8610 contentView = mContainer.getContentView();
8611 width = contentView.getMeasuredWidth();
8612 height = contentView.getMeasuredHeight();
8613
8614 // Vertical clipping, move under edited line and to the side of insertion cursor
8615 // TODO bottom clipping in case there is no system bar
8616 coords[1] += height;
8617 final int lineBottom = mLayout.getLineBottom(line);
8618 final int lineHeight = lineBottom - lineTop;
8619 coords[1] += lineHeight;
8620
8621 // Move to right hand side of insertion cursor by default. TODO RTL text.
8622 final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
8623 final int handleHalfWidth = handle.getIntrinsicWidth() / 2;
8624
8625 if (primaryHorizontal + handleHalfWidth + width < screenWidth) {
8626 coords[0] += handleHalfWidth + width / 2;
8627 } else {
8628 coords[0] -= handleHalfWidth + width / 2;
8629 }
8630 } else {
8631 // Horizontal clipping
8632 coords[0] = Math.max(0, coords[0]);
8633 coords[0] = Math.min(screenWidth - width, coords[0]);
8634 }
8635
8636 mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008637 }
8638 }
8639
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008640 private class HandleView extends View implements ViewTreeObserver.OnPreDrawListener {
Adam Powell879fb6b2010-09-20 11:23:56 -07008641 private Drawable mDrawable;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008642 private final PopupWindow mContainer;
8643 // Position with respect to the parent TextView
8644 private int mPositionX, mPositionY;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008645 private final CursorController mController;
Adam Powell879fb6b2010-09-20 11:23:56 -07008646 private boolean mIsDragging;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008647 // Offset from touch position to mPosition
8648 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
Adam Powellea32f942010-10-02 16:03:45 -07008649 private float mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008650 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
Adam Powellfbb3b472010-10-06 21:04:35 -07008651 private float mTouchOffsetY;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008652 // Where the touch position should be on the handle to ensure a maximum cursor visibility
8653 private float mIdealVerticalOffset;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008654 // Parent's (TextView) position in window
8655 private int mLastParentX, mLastParentY;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08008656 private float mDownPositionX, mDownPositionY;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008657 // PopupWindow container absolute position with respect to the enclosing window
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008658 private int mContainerPositionX, mContainerPositionY;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008659 // Visible or not (scrolled off screen), whether or not this handle should be visible
8660 private boolean mIsActive = false;
8661 // The insertion handle can have an associated PastePopupMenu
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08008662 private boolean mIsInsertionHandle = false;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008663 // Used to detect taps on the insertion handle, which will affect the PastePopupMenu
8664 private long mTouchTimer;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008665 private PastePopupMenu mPastePopupWindow;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -07008666
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008667 // Touch-up filter: number of previous positions remembered
8668 private static final int HISTORY_SIZE = 5;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008669 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
8670 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008671 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
8672 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
8673 private int mPreviousOffsetIndex = 0;
8674 private int mNumberPreviousOffsets = 0;
8675
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008676 private void startTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008677 mNumberPreviousOffsets = 0;
8678 addPositionToTouchUpFilter(offset);
8679 }
8680
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008681 private void addPositionToTouchUpFilter(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008682 if (mNumberPreviousOffsets > 0 &&
8683 mPreviousOffsets[mPreviousOffsetIndex] == offset) {
8684 // Make sure only actual changes of position are recorded.
8685 return;
8686 }
8687
8688 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
8689 mPreviousOffsets[mPreviousOffsetIndex] = offset;
8690 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
8691 mNumberPreviousOffsets++;
8692 }
8693
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008694 private void filterOnTouchUp() {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008695 final long now = SystemClock.uptimeMillis();
8696 int i = 0;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008697 int index = mPreviousOffsetIndex;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008698 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008699 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008700 i++;
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008701 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008702 }
8703
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008704 if (i > 0 && i < iMax &&
8705 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
8706 mController.updateOffset(this, mPreviousOffsets[index]);
8707 }
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008708 }
8709
Adam Powellfbb3b472010-10-06 21:04:35 -07008710 public static final int LEFT = 0;
8711 public static final int CENTER = 1;
8712 public static final int RIGHT = 2;
8713
8714 public HandleView(CursorController controller, int pos) {
Adam Powell879fb6b2010-09-20 11:23:56 -07008715 super(TextView.this.mContext);
8716 mController = controller;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008717 mContainer = new PopupWindow(TextView.this.mContext, null,
Adam Powell879fb6b2010-09-20 11:23:56 -07008718 com.android.internal.R.attr.textSelectHandleWindowStyle);
Jeff Brown01ce2e92010-09-26 22:20:12 -07008719 mContainer.setSplitTouchEnabled(true);
Adam Powellba0a2c32010-09-28 17:41:23 -07008720 mContainer.setClippingEnabled(false);
Adam Powell574b37e2010-10-07 11:15:19 -07008721 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008722 mContainer.setContentView(this);
Adam Powellabcbb1a2010-10-04 21:12:19 -07008723
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008724 setPosition(pos);
Adam Powellfbb3b472010-10-06 21:04:35 -07008725 }
8726
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008727 private void setPosition(int pos) {
Adam Powellfbb3b472010-10-06 21:04:35 -07008728 int handleWidth;
8729 switch (pos) {
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008730 case LEFT: {
8731 if (mSelectHandleLeft == null) {
8732 mSelectHandleLeft = mContext.getResources().getDrawable(
8733 mTextSelectHandleLeftRes);
8734 }
8735 mDrawable = mSelectHandleLeft;
8736 handleWidth = mDrawable.getIntrinsicWidth();
8737 mHotspotX = handleWidth * 3.0f / 4.0f;
8738 break;
Adam Powellfbb3b472010-10-06 21:04:35 -07008739 }
Adam Powellfbb3b472010-10-06 21:04:35 -07008740
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008741 case RIGHT: {
8742 if (mSelectHandleRight == null) {
8743 mSelectHandleRight = mContext.getResources().getDrawable(
8744 mTextSelectHandleRightRes);
8745 }
8746 mDrawable = mSelectHandleRight;
8747 handleWidth = mDrawable.getIntrinsicWidth();
8748 mHotspotX = handleWidth / 4.0f;
8749 break;
Adam Powellfbb3b472010-10-06 21:04:35 -07008750 }
Adam Powellfbb3b472010-10-06 21:04:35 -07008751
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008752 case CENTER:
8753 default: {
8754 if (mSelectHandleCenter == null) {
8755 mSelectHandleCenter = mContext.getResources().getDrawable(
8756 mTextSelectHandleRes);
8757 }
8758 mDrawable = mSelectHandleCenter;
8759 handleWidth = mDrawable.getIntrinsicWidth();
8760 mHotspotX = handleWidth / 2.0f;
8761 mIsInsertionHandle = true;
8762 break;
Adam Powellfbb3b472010-10-06 21:04:35 -07008763 }
Adam Powellfbb3b472010-10-06 21:04:35 -07008764 }
8765
Adam Powellabcbb1a2010-10-04 21:12:19 -07008766 final int handleHeight = mDrawable.getIntrinsicHeight();
Gilles Debunneb7012e842011-02-24 15:40:38 -08008767 mTouchOffsetY = -0.3f * handleHeight;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008768 mIdealVerticalOffset = 0.7f * handleHeight;
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008769
Adam Powellfbb3b472010-10-06 21:04:35 -07008770 invalidate();
Adam Powell879fb6b2010-09-20 11:23:56 -07008771 }
8772
8773 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008774 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008775 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
Adam Powell879fb6b2010-09-20 11:23:56 -07008776 }
8777
8778 public void show() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008779 updateContainerPosition();
8780 if (isShowing()) {
8781 mContainer.update(mContainerPositionX, mContainerPositionY,
8782 mRight - mLeft, mBottom - mTop);
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008783
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008784 hidePastePopupWindow();
8785 } else {
8786 mContainer.showAtLocation(TextView.this, 0,
8787 mContainerPositionX, mContainerPositionY);
8788
8789 mIsActive = true;
8790
8791 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
8792 vto.addOnPreDrawListener(this);
8793 }
8794 }
8795
8796 private void dismiss() {
8797 mIsDragging = false;
8798 mContainer.dismiss();
Gilles Debunne81f08082011-02-17 14:07:19 -08008799 hidePastePopupWindow();
Adam Powell879fb6b2010-09-20 11:23:56 -07008800 }
8801
8802 public void hide() {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008803 dismiss();
8804
8805 mIsActive = false;
8806
Gilles Debunneb7012e842011-02-24 15:40:38 -08008807 ViewTreeObserver vto = TextView.this.getViewTreeObserver();
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008808 vto.removeOnPreDrawListener(this);
Adam Powell879fb6b2010-09-20 11:23:56 -07008809 }
8810
8811 public boolean isShowing() {
8812 return mContainer.isShowing();
8813 }
8814
Adam Powellabcbb1a2010-10-04 21:12:19 -07008815 private boolean isPositionVisible() {
8816 // Always show a dragging handle.
8817 if (mIsDragging) {
8818 return true;
8819 }
8820
Adam Powell965b9692010-10-21 18:44:32 -07008821 if (isInBatchEditMode()) {
8822 return false;
8823 }
8824
Adam Powell879fb6b2010-09-20 11:23:56 -07008825 final int extendedPaddingTop = getExtendedPaddingTop();
8826 final int extendedPaddingBottom = getExtendedPaddingBottom();
8827 final int compoundPaddingLeft = getCompoundPaddingLeft();
8828 final int compoundPaddingRight = getCompoundPaddingRight();
8829
8830 final TextView hostView = TextView.this;
Adam Powell879fb6b2010-09-20 11:23:56 -07008831
Adam Powellabcbb1a2010-10-04 21:12:19 -07008832 if (mTempRect == null) {
8833 mTempRect = new Rect();
8834 }
8835 final Rect clip = mTempRect;
Gilles Debunne81f08082011-02-17 14:07:19 -08008836 clip.left = compoundPaddingLeft;
8837 clip.top = extendedPaddingTop;
8838 clip.right = hostView.getWidth() - compoundPaddingRight;
8839 clip.bottom = hostView.getHeight() - extendedPaddingBottom;
Adam Powell879fb6b2010-09-20 11:23:56 -07008840
Adam Powellabcbb1a2010-10-04 21:12:19 -07008841 final ViewParent parent = hostView.getParent();
8842 if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
8843 return false;
8844 }
8845
8846 final int[] coords = mTempCoords;
8847 hostView.getLocationInWindow(coords);
8848 final int posX = coords[0] + mPositionX + (int) mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008849 final int posY = coords[1] + mPositionY;
Adam Powellabcbb1a2010-10-04 21:12:19 -07008850
Gilles Debunne81f08082011-02-17 14:07:19 -08008851 // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
8852 return posX >= clip.left - 1 && posX <= clip.right + 1 &&
Adam Powellfbb3b472010-10-06 21:04:35 -07008853 posY >= clip.top && posY <= clip.bottom;
Adam Powell879fb6b2010-09-20 11:23:56 -07008854 }
8855
8856 private void moveTo(int x, int y) {
8857 mPositionX = x - TextView.this.mScrollX;
8858 mPositionY = y - TextView.this.mScrollY;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008859
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008860 if (mIsDragging) {
8861 TextView.this.getLocationInWindow(mTempCoords);
8862 if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) {
8863 mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX;
8864 mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY;
8865 mLastParentX = mTempCoords[0];
8866 mLastParentY = mTempCoords[1];
8867 }
8868 // Hide paste popup window as soon as the handle is dragged.
8869 hidePastePopupWindow();
8870 }
8871 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008872
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008873 /**
8874 * Updates the global container's position.
8875 * @return whether or not the position has actually changed
8876 */
8877 private boolean updateContainerPosition() {
8878 // TODO Prevent this using different HandleView subclasses
8879 mController.updateOffset(this, mController.getCurrentOffset(this));
8880 TextView.this.getLocationInWindow(mTempCoords);
8881 final int containerPositionX = mTempCoords[0] + mPositionX;
8882 final int containerPositionY = mTempCoords[1] + mPositionY;
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008883
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008884 if (containerPositionX != mContainerPositionX ||
8885 containerPositionY != mContainerPositionY) {
8886 mContainerPositionX = containerPositionX;
8887 mContainerPositionY = containerPositionY;
8888 return true;
8889 }
8890 return false;
8891 }
8892
8893 public boolean onPreDraw() {
8894 if (updateContainerPosition()) {
8895 if (isPositionVisible()) {
8896 mContainer.update(mContainerPositionX, mContainerPositionY,
8897 mRight - mLeft, mBottom - mTop);
8898
8899 if (mIsActive && !isShowing()) {
8900 show();
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008901 }
Adam Powell879fb6b2010-09-20 11:23:56 -07008902 } else {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008903 if (isShowing()) {
8904 dismiss();
8905 }
Adam Powell879fb6b2010-09-20 11:23:56 -07008906 }
Adam Powellabcbb1a2010-10-04 21:12:19 -07008907
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008908 // Hide paste popup as soon as the view is scrolled or moved
8909 hidePastePopupWindow();
Adam Powell879fb6b2010-09-20 11:23:56 -07008910 }
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008911 return true;
Adam Powell879fb6b2010-09-20 11:23:56 -07008912 }
8913
8914 @Override
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008915 protected void onDraw(Canvas c) {
Adam Powell879fb6b2010-09-20 11:23:56 -07008916 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008917 mDrawable.draw(c);
Adam Powell879fb6b2010-09-20 11:23:56 -07008918 }
8919
8920 @Override
8921 public boolean onTouchEvent(MotionEvent ev) {
8922 switch (ev.getActionMasked()) {
Gilles Debunne874d77c2011-01-25 15:29:13 -08008923 case MotionEvent.ACTION_DOWN: {
8924 startTouchUpFilter(mController.getCurrentOffset(this));
8925 mDownPositionX = ev.getRawX();
8926 mDownPositionY = ev.getRawY();
8927 mTouchToWindowOffsetX = mDownPositionX - mPositionX;
8928 mTouchToWindowOffsetY = mDownPositionY - mPositionY;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008929
Gilles Debunne874d77c2011-01-25 15:29:13 -08008930 final int[] coords = mTempCoords;
8931 TextView.this.getLocationInWindow(coords);
8932 mLastParentX = coords[0];
8933 mLastParentY = coords[1];
8934 mIsDragging = true;
Gilles Debunne3ce726e2011-01-31 15:00:08 -08008935 if (mIsInsertionHandle) {
8936 mTouchTimer = SystemClock.uptimeMillis();
8937 }
Gilles Debunne874d77c2011-01-25 15:29:13 -08008938 break;
8939 }
8940
8941 case MotionEvent.ACTION_MOVE: {
8942 final float rawX = ev.getRawX();
8943 final float rawY = ev.getRawY();
Gilles Debunneddf00b82011-02-23 17:25:13 -08008944
8945 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
8946 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
8947 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
8948 float newVerticalOffset;
8949 if (previousVerticalOffset < mIdealVerticalOffset) {
8950 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
8951 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
8952 } else {
8953 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
8954 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
8955 }
8956 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
8957
Gilles Debunne874d77c2011-01-25 15:29:13 -08008958 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
Gilles Debunneddf00b82011-02-23 17:25:13 -08008959 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunne874d77c2011-01-25 15:29:13 -08008960
8961 mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
8962 break;
8963 }
8964
8965 case MotionEvent.ACTION_UP:
8966 if (mIsInsertionHandle) {
8967 long delay = SystemClock.uptimeMillis() - mTouchTimer;
8968 if (delay < ViewConfiguration.getTapTimeout()) {
Gilles Debunnebc7a4c82011-02-09 10:45:51 -08008969 final float deltaX = mDownPositionX - ev.getRawX();
8970 final float deltaY = mDownPositionY - ev.getRawY();
8971 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
8972 if (distanceSquared < mSquaredTouchSlopDistance) {
8973 if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) {
8974 // Tapping on the handle dismisses the displayed paste view,
8975 mPastePopupWindow.hide();
8976 } else {
8977 ((InsertionPointCursorController) mController).show(0);
8978 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08008979 }
Gilles Debunne7b9652b2010-10-26 16:27:12 -07008980 }
Gilles Debunne1cafde02010-12-03 11:45:09 -08008981 }
Gilles Debunne874d77c2011-01-25 15:29:13 -08008982 filterOnTouchUp();
8983 mIsDragging = false;
8984 break;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08008985
Gilles Debunne874d77c2011-01-25 15:29:13 -08008986 case MotionEvent.ACTION_CANCEL:
8987 mIsDragging = false;
8988 break;
Adam Powell879fb6b2010-09-20 11:23:56 -07008989 }
8990 return true;
8991 }
8992
8993 public boolean isDragging() {
8994 return mIsDragging;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07008995 }
Gilles Debunne64e54a62010-09-07 19:07:17 -07008996
Gilles Debunnecfc22c52011-03-07 15:50:47 -08008997 void positionAtCursor(int offset) {
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08008998 addPositionToTouchUpFilter(offset);
Adam Powell879fb6b2010-09-20 11:23:56 -07008999 final int width = mDrawable.getIntrinsicWidth();
9000 final int height = mDrawable.getIntrinsicHeight();
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009001 final int line = mLayout.getLineForOffset(offset);
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009002 final int lineBottom = mLayout.getLineBottom(line);
9003
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009004 final Rect bounds = sCursorControllerTempRect;
Gilles Debunnef75c97e2011-02-10 16:09:53 -08009005 bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX) +
9006 TextView.this.mScrollX;
Gilles Debunneddf00b82011-02-23 17:25:13 -08009007 bounds.top = lineBottom + TextView.this.mScrollY;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009008
Adam Powell879fb6b2010-09-20 11:23:56 -07009009 bounds.right = bounds.left + width;
9010 bounds.bottom = bounds.top + height;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009011
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009012 convertFromViewportToContentCoordinates(bounds);
Adam Powell879fb6b2010-09-20 11:23:56 -07009013 moveTo(bounds.left, bounds.top);
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009014 }
Gilles Debunne9948ad72010-11-24 14:00:46 -08009015
9016 void showPastePopupWindow() {
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08009017 if (mIsInsertionHandle) {
Gilles Debunne28802942010-11-24 11:23:53 -08009018 if (mPastePopupWindow == null) {
9019 // Lazy initialisation: create when actually shown only.
9020 mPastePopupWindow = new PastePopupMenu();
9021 }
Gilles Debunne9948ad72010-11-24 14:00:46 -08009022 mPastePopupWindow.show();
9023 }
9024 }
Gilles Debunne81f08082011-02-17 14:07:19 -08009025
9026 void hidePastePopupWindow() {
9027 if (mPastePopupWindow != null) {
9028 mPastePopupWindow.hide();
9029 }
9030 }
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009031 }
9032
Jeff Brown01ce2e92010-09-26 22:20:12 -07009033 private class InsertionPointCursorController implements CursorController {
Gilles Debunne81f08082011-02-17 14:07:19 -08009034 private static final int DELAY_BEFORE_FADE_OUT = 4000;
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009035 private static final int DELAY_BEFORE_PASTE = 2000;
9036 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000;
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009037
Gilles Debunnec4440f02010-11-24 14:40:48 -08009038 // The cursor controller image. Lazily created.
9039 private HandleView mHandle;
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009040 private Runnable mHider;
9041 private Runnable mPastePopupShower;
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009042
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009043 public void show() {
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009044 show(DELAY_BEFORE_PASTE);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009045 }
9046
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009047 public void show(int delayBeforePaste) {
Gilles Debunnecfc22c52011-03-07 15:50:47 -08009048 getHandle().show();
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009049 hideDelayed();
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009050 removePastePopupCallback();
Gilles Debunnee60e1e52011-01-20 12:19:44 -08009051 final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
9052 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
9053 delayBeforePaste = 0;
9054 }
9055 if (delayBeforePaste == 0 || canPaste()) {
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009056 if (mPastePopupShower == null) {
9057 mPastePopupShower = new Runnable() {
9058 public void run() {
9059 getHandle().showPastePopupWindow();
9060 }
9061 };
9062 }
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009063 postDelayed(mPastePopupShower, delayBeforePaste);
9064 }
Gilles Debunne9948ad72010-11-24 14:00:46 -08009065 }
9066
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009067 private void removePastePopupCallback() {
9068 if (mPastePopupShower != null) {
9069 removeCallbacks(mPastePopupShower);
9070 }
9071 }
9072
9073 private void removeHiderCallback() {
9074 if (mHider != null) {
9075 removeCallbacks(mHider);
9076 }
9077 }
9078
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009079 public void hide() {
Gilles Debunnec4440f02010-11-24 14:40:48 -08009080 if (mHandle != null) {
9081 mHandle.hide();
9082 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009083 removeHiderCallback();
9084 removePastePopupCallback();
Adam Powell879fb6b2010-09-20 11:23:56 -07009085 }
9086
Gilles Debunne2a7f3462010-11-24 16:31:26 -08009087 private void hideDelayed() {
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009088 removeHiderCallback();
9089 if (mHider == null) {
9090 mHider = new Runnable() {
9091 public void run() {
9092 hide();
9093 }
9094 };
9095 }
Gilles Debunne2a7f3462010-11-24 16:31:26 -08009096 postDelayed(mHider, DELAY_BEFORE_FADE_OUT);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009097 }
9098
Adam Powellb08013c2010-09-16 16:28:11 -07009099 public boolean isShowing() {
Gilles Debunnec4440f02010-11-24 14:40:48 -08009100 return mHandle != null && mHandle.isShowing();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009101 }
9102
Jeff Brown01ce2e92010-09-26 22:20:12 -07009103 public void updatePosition(HandleView handle, int x, int y) {
Gilles Debunne3e05a0b2010-08-23 14:55:06 -07009104 final int previousOffset = getSelectionStart();
Gilles Debunneddf00b82011-02-23 17:25:13 -08009105 final int newOffset = getOffset(x, y);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -07009106
Gilles Debunneddf00b82011-02-23 17:25:13 -08009107 if (newOffset != previousOffset) {
9108 updateOffset(handle, newOffset);
Gilles Debunne3ce726e2011-01-31 15:00:08 -08009109 removePastePopupCallback();
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009110 }
Gilles Debunne2a7f3462010-11-24 16:31:26 -08009111 hideDelayed();
Gilles Debunne05336272010-07-09 20:13:45 -07009112 }
9113
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08009114 public void updateOffset(HandleView handle, int offset) {
9115 Selection.setSelection((Spannable) mText, offset);
9116 updatePosition();
9117 }
9118
Adam Powell879fb6b2010-09-20 11:23:56 -07009119 public void updatePosition() {
Gilles Debunne05336272010-07-09 20:13:45 -07009120 final int offset = getSelectionStart();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009121
9122 if (offset < 0) {
9123 // Should never happen, safety check.
Gilles Debunne05336272010-07-09 20:13:45 -07009124 Log.w(LOG_TAG, "Update cursor controller position called with no cursor");
Adam Powell879fb6b2010-09-20 11:23:56 -07009125 hide();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009126 return;
9127 }
9128
Gilles Debunneddf00b82011-02-23 17:25:13 -08009129 getHandle().positionAtCursor(offset);
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009130 }
9131
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08009132 public int getCurrentOffset(HandleView handle) {
9133 return getSelectionStart();
9134 }
9135
Adam Powell879fb6b2010-09-20 11:23:56 -07009136 public boolean onTouchEvent(MotionEvent ev) {
9137 return false;
9138 }
Adam Powell624380a2010-10-02 18:12:02 -07009139
9140 public void onTouchModeChanged(boolean isInTouchMode) {
9141 if (!isInTouchMode) {
9142 hide();
9143 }
9144 }
Gilles Debunnec4440f02010-11-24 14:40:48 -08009145
9146 private HandleView getHandle() {
9147 if (mHandle == null) {
9148 mHandle = new HandleView(this, HandleView.CENTER);
9149 }
9150 return mHandle;
9151 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009152
9153 @Override
9154 public void onDetached() {
9155 removeHiderCallback();
9156 removePastePopupCallback();
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009157 }
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009158 }
9159
Jeff Brown01ce2e92010-09-26 22:20:12 -07009160 private class SelectionModifierCursorController implements CursorController {
Gilles Debunnec4440f02010-11-24 14:40:48 -08009161 // The cursor controller images, lazily created when shown.
Adam Powell879fb6b2010-09-20 11:23:56 -07009162 private HandleView mStartHandle, mEndHandle;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009163 // The offsets of that last touch down event. Remembered to start selection there.
9164 private int mMinTouchOffset, mMaxTouchOffset;
Adam Powell879fb6b2010-09-20 11:23:56 -07009165 // Whether selection anchors are active
9166 private boolean mIsShowing;
Gilles Debunne05336272010-07-09 20:13:45 -07009167
Gilles Debunne5347c582010-10-27 14:22:35 -07009168 // Double tap detection
9169 private long mPreviousTapUpTime = 0;
9170 private int mPreviousTapPositionX;
9171 private int mPreviousTapPositionY;
9172
Gilles Debunne05336272010-07-09 20:13:45 -07009173 SelectionModifierCursorController() {
Gilles Debunne380b6042010-10-08 16:12:11 -07009174 resetTouchOffsets();
Gilles Debunne05336272010-07-09 20:13:45 -07009175 }
9176
9177 public void show() {
Adam Powell965b9692010-10-21 18:44:32 -07009178 if (isInBatchEditMode()) {
9179 return;
9180 }
9181
Gilles Debunnec4440f02010-11-24 14:40:48 -08009182 // Lazy object creation has to be done before updatePosition() is called.
9183 if (mStartHandle == null) mStartHandle = new HandleView(this, HandleView.LEFT);
9184 if (mEndHandle == null) mEndHandle = new HandleView(this, HandleView.RIGHT);
9185
Adam Powell879fb6b2010-09-20 11:23:56 -07009186 mIsShowing = true;
Gilles Debunnec4440f02010-11-24 14:40:48 -08009187
Adam Powell879fb6b2010-09-20 11:23:56 -07009188 mStartHandle.show();
9189 mEndHandle.show();
Gilles Debunnec4440f02010-11-24 14:40:48 -08009190
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009191 hideInsertionPointCursorController();
Gilles Debunne05336272010-07-09 20:13:45 -07009192 }
9193
9194 public void hide() {
Gilles Debunnec4440f02010-11-24 14:40:48 -08009195 if (mStartHandle != null) mStartHandle.hide();
9196 if (mEndHandle != null) mEndHandle.hide();
Adam Powell879fb6b2010-09-20 11:23:56 -07009197 mIsShowing = false;
Gilles Debunne05336272010-07-09 20:13:45 -07009198 }
9199
Adam Powellb08013c2010-09-16 16:28:11 -07009200 public boolean isShowing() {
Adam Powell879fb6b2010-09-20 11:23:56 -07009201 return mIsShowing;
Adam Powellb08013c2010-09-16 16:28:11 -07009202 }
9203
Jeff Brown01ce2e92010-09-26 22:20:12 -07009204 public void updatePosition(HandleView handle, int x, int y) {
Gilles Debunne05336272010-07-09 20:13:45 -07009205 int selectionStart = getSelectionStart();
9206 int selectionEnd = getSelectionEnd();
9207
Gilles Debunneddf00b82011-02-23 17:25:13 -08009208 int offset = getOffset(x, y);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -07009209
Gilles Debunne05336272010-07-09 20:13:45 -07009210 // Handle the case where start and end are swapped, making sure start <= end
Jeff Brown01ce2e92010-09-26 22:20:12 -07009211 if (handle == mStartHandle) {
Adam Powellfbb3b472010-10-06 21:04:35 -07009212 if (selectionStart == offset || offset > selectionEnd) {
9213 return; // no change, no need to redraw;
Gilles Debunne05336272010-07-09 20:13:45 -07009214 }
Adam Powellfbb3b472010-10-06 21:04:35 -07009215 // If the user "closes" the selection entirely they were probably trying to
9216 // select a single character. Help them out.
9217 if (offset == selectionEnd) {
9218 offset = selectionEnd - 1;
9219 }
9220 selectionStart = offset;
Gilles Debunne05336272010-07-09 20:13:45 -07009221 } else {
Adam Powellfbb3b472010-10-06 21:04:35 -07009222 if (selectionEnd == offset || offset < selectionStart) {
9223 return; // no change, no need to redraw;
Gilles Debunne05336272010-07-09 20:13:45 -07009224 }
Adam Powellfbb3b472010-10-06 21:04:35 -07009225 // If the user "closes" the selection entirely they were probably trying to
9226 // select a single character. Help them out.
9227 if (offset == selectionStart) {
9228 offset = selectionStart + 1;
9229 }
9230 selectionEnd = offset;
Gilles Debunne05336272010-07-09 20:13:45 -07009231 }
9232
9233 Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
Adam Powell879fb6b2010-09-20 11:23:56 -07009234 updatePosition();
Gilles Debunne05336272010-07-09 20:13:45 -07009235 }
9236
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08009237 public void updateOffset(HandleView handle, int offset) {
9238 int start = getSelectionStart();
9239 int end = getSelectionEnd();
9240
9241 if (mStartHandle == handle) {
9242 start = offset;
9243 } else {
9244 end = offset;
9245 }
9246
9247 Selection.setSelection((Spannable) mText, start, end);
9248 updatePosition();
9249 }
9250
Adam Powell879fb6b2010-09-20 11:23:56 -07009251 public void updatePosition() {
Adam Powell965b9692010-10-21 18:44:32 -07009252 if (!isShowing()) {
9253 return;
9254 }
9255
Gilles Debunne05336272010-07-09 20:13:45 -07009256 final int selectionStart = getSelectionStart();
9257 final int selectionEnd = getSelectionEnd();
9258
9259 if ((selectionStart < 0) || (selectionEnd < 0)) {
9260 // Should never happen, safety check.
9261 Log.w(LOG_TAG, "Update selection controller position called with no cursor");
Adam Powell879fb6b2010-09-20 11:23:56 -07009262 hide();
Gilles Debunne05336272010-07-09 20:13:45 -07009263 return;
9264 }
9265
Gilles Debunnec4440f02010-11-24 14:40:48 -08009266 // The handles have been created since the controller isShowing().
Gilles Debunneddf00b82011-02-23 17:25:13 -08009267 mStartHandle.positionAtCursor(selectionStart);
9268 mEndHandle.positionAtCursor(selectionEnd);
Gilles Debunne05336272010-07-09 20:13:45 -07009269 }
9270
Gilles Debunneaa8d73b2011-01-17 09:16:40 -08009271 public int getCurrentOffset(HandleView handle) {
9272 return mStartHandle == handle ? getSelectionStart() : getSelectionEnd();
9273 }
9274
Adam Powellb08013c2010-09-16 16:28:11 -07009275 public boolean onTouchEvent(MotionEvent event) {
Gilles Debunne528c64882010-10-08 11:56:13 -07009276 // This is done even when the View does not have focus, so that long presses can start
9277 // selection and tap can move cursor from this tap position.
Gilles Debunnef076eeb2010-11-29 11:32:53 -08009278 if (isTextEditable() || mTextIsSelectable) {
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009279 switch (event.getActionMasked()) {
9280 case MotionEvent.ACTION_DOWN:
9281 final int x = (int) event.getX();
9282 final int y = (int) event.getY();
Gilles Debunne05336272010-07-09 20:13:45 -07009283
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009284 // Remember finger down position, to be able to start selection from there
Gilles Debunne528c64882010-10-08 11:56:13 -07009285 mMinTouchOffset = mMaxTouchOffset = getOffset(x, y);
Gilles Debunne05336272010-07-09 20:13:45 -07009286
Gilles Debunne5347c582010-10-27 14:22:35 -07009287 // Double tap detection
9288 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
Gilles Debunne28253662010-12-01 13:40:43 -08009289 if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
9290 isPositionOnText(x, y)) {
Gilles Debunne5347c582010-10-27 14:22:35 -07009291 final int deltaX = x - mPreviousTapPositionX;
9292 final int deltaY = y - mPreviousTapPositionY;
9293 final int distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunneaa85a4c2010-12-06 18:27:17 -08009294 if (distanceSquared < mSquaredTouchSlopDistance) {
Gilles Debunne5347c582010-10-27 14:22:35 -07009295 startSelectionActionMode();
Gilles Debunne0eb704c2010-11-30 12:50:54 -08009296 mDiscardNextActionUp = true;
Gilles Debunne5347c582010-10-27 14:22:35 -07009297 }
9298 }
9299
9300 mPreviousTapPositionX = x;
9301 mPreviousTapPositionY = y;
9302
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009303 break;
9304
9305 case MotionEvent.ACTION_POINTER_DOWN:
9306 case MotionEvent.ACTION_POINTER_UP:
9307 // Handle multi-point gestures. Keep min and max offset positions.
9308 // Only activated for devices that correctly handle multi-touch.
9309 if (mContext.getPackageManager().hasSystemFeature(
9310 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
9311 updateMinAndMaxOffsets(event);
9312 }
9313 break;
Gilles Debunne5347c582010-10-27 14:22:35 -07009314
9315 case MotionEvent.ACTION_UP:
9316 mPreviousTapUpTime = SystemClock.uptimeMillis();
9317 break;
Gilles Debunne05336272010-07-09 20:13:45 -07009318 }
9319 }
Adam Powellb08013c2010-09-16 16:28:11 -07009320 return false;
Gilles Debunne05336272010-07-09 20:13:45 -07009321 }
9322
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009323 /**
9324 * @param event
9325 */
9326 private void updateMinAndMaxOffsets(MotionEvent event) {
9327 int pointerCount = event.getPointerCount();
9328 for (int index = 0; index < pointerCount; index++) {
9329 final int x = (int) event.getX(index);
9330 final int y = (int) event.getY(index);
9331 int offset = getOffset(x, y);
9332 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
9333 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
9334 }
9335 }
9336
9337 public int getMinTouchOffset() {
9338 return mMinTouchOffset;
9339 }
9340
9341 public int getMaxTouchOffset() {
9342 return mMaxTouchOffset;
Gilles Debunne05336272010-07-09 20:13:45 -07009343 }
9344
Gilles Debunne380b6042010-10-08 16:12:11 -07009345 public void resetTouchOffsets() {
9346 mMinTouchOffset = mMaxTouchOffset = -1;
9347 }
9348
Gilles Debunne05336272010-07-09 20:13:45 -07009349 /**
9350 * @return true iff this controller is currently used to move the selection start.
9351 */
9352 public boolean isSelectionStartDragged() {
Gilles Debunnec4440f02010-11-24 14:40:48 -08009353 return mStartHandle != null && mStartHandle.isDragging();
Gilles Debunne05336272010-07-09 20:13:45 -07009354 }
Adam Powell624380a2010-10-02 18:12:02 -07009355
9356 public void onTouchModeChanged(boolean isInTouchMode) {
9357 if (!isInTouchMode) {
9358 hide();
9359 }
9360 }
Gilles Debunne7eeba5f2010-12-10 16:55:55 -08009361
9362 @Override
9363 public void onDetached() {}
Gilles Debunne05336272010-07-09 20:13:45 -07009364 }
9365
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009366 private void hideInsertionPointCursorController() {
Gilles Debunnee587d832010-11-23 20:20:11 -08009367 // No need to create the controller to hide it.
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009368 if (mInsertionPointCursorController != null) {
9369 mInsertionPointCursorController.hide();
9370 }
9371 }
9372
Gilles Debunned94f8c52011-01-10 11:29:15 -08009373 /**
9374 * Hides the insertion controller and stops text selection mode, hiding the selection controller
9375 */
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009376 private void hideControllers() {
9377 hideInsertionPointCursorController();
Gilles Debunned94f8c52011-01-10 11:29:15 -08009378 stopSelectionActionMode();
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009379 }
9380
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009381 /**
9382 * Get the offset character closest to the specified absolute position.
9383 *
9384 * @param x The horizontal absolute position of a point on screen
9385 * @param y The vertical absolute position of a point on screen
9386 * @return the character offset for the character whose position is closest to the specified
9387 * position. Returns -1 if there is no layout.
9388 *
9389 * @hide
9390 */
9391 public int getOffset(int x, int y) {
Gilles Debunne3e05a0b2010-08-23 14:55:06 -07009392 if (getLayout() == null) return -1;
Gilles Debunne9948ad72010-11-24 14:00:46 -08009393 final int line = getLineAtCoordinate(y);
9394 final int offset = getOffsetAtCoordinate(line, x);
Gilles Debunne3e05a0b2010-08-23 14:55:06 -07009395 return offset;
9396 }
9397
Gilles Debunne9948ad72010-11-24 14:00:46 -08009398 private int convertToLocalHorizontalCoordinate(int x) {
9399 x -= getTotalPaddingLeft();
9400 // Clamp the position to inside of the view.
9401 x = Math.max(0, x);
9402 x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
9403 x += getScrollX();
9404 return x;
9405 }
9406
9407 private int getLineAtCoordinate(int y) {
9408 y -= getTotalPaddingTop();
9409 // Clamp the position to inside of the view.
9410 y = Math.max(0, y);
9411 y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
9412 y += getScrollY();
9413 return getLayout().getLineForVertical(y);
9414 }
9415
9416 private int getOffsetAtCoordinate(int line, int x) {
9417 x = convertToLocalHorizontalCoordinate(x);
9418 return getLayout().getOffsetForHorizontal(line, x);
9419 }
9420
9421 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
9422 * in the view. Returns false when the position is in the empty space of left/right of text.
9423 */
9424 private boolean isPositionOnText(int x, int y) {
9425 if (getLayout() == null) return false;
9426
9427 final int line = getLineAtCoordinate(y);
9428 x = convertToLocalHorizontalCoordinate(x);
9429
9430 if (x < getLayout().getLineLeft(line)) return false;
9431 if (x > getLayout().getLineRight(line)) return false;
9432 return true;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009433 }
9434
Gilles Debunnef170a342010-11-11 11:08:59 -08009435 @Override
9436 public boolean onDragEvent(DragEvent event) {
9437 switch (event.getAction()) {
9438 case DragEvent.ACTION_DRAG_STARTED:
Gilles Debunnee587d832010-11-23 20:20:11 -08009439 return hasInsertionController();
Gilles Debunnef170a342010-11-11 11:08:59 -08009440
9441 case DragEvent.ACTION_DRAG_ENTERED:
9442 TextView.this.requestFocus();
9443 return true;
9444
Gilles Debunne2226a192010-11-30 18:50:51 -08009445 case DragEvent.ACTION_DRAG_LOCATION:
Gilles Debunne9948ad72010-11-24 14:00:46 -08009446 final int offset = getOffset((int) event.getX(), (int) event.getY());
Gilles Debunnef170a342010-11-11 11:08:59 -08009447 Selection.setSelection((Spannable)mText, offset);
9448 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -08009449
Gilles Debunne2226a192010-11-30 18:50:51 -08009450 case DragEvent.ACTION_DROP:
9451 onDrop(event);
Gilles Debunnef170a342010-11-11 11:08:59 -08009452 return true;
Gilles Debunnef170a342010-11-11 11:08:59 -08009453
Gilles Debunnef170a342010-11-11 11:08:59 -08009454 case DragEvent.ACTION_DRAG_ENDED:
Gilles Debunne4ae0f292010-11-29 14:56:39 -08009455 case DragEvent.ACTION_DRAG_EXITED:
Gilles Debunnef170a342010-11-11 11:08:59 -08009456 default:
9457 return true;
9458 }
9459 }
9460
Gilles Debunne2226a192010-11-30 18:50:51 -08009461 private void onDrop(DragEvent event) {
9462 StringBuilder content = new StringBuilder("");
9463 ClipData clipData = event.getClipData();
9464 final int itemCount = clipData.getItemCount();
9465 for (int i=0; i < itemCount; i++) {
Dianne Hackborn327fbd22011-01-17 14:38:50 -08009466 Item item = clipData.getItemAt(i);
Gilles Debunne2226a192010-11-30 18:50:51 -08009467 content.append(item.coerceToText(TextView.this.mContext));
9468 }
9469
9470 final int offset = getOffset((int) event.getX(), (int) event.getY());
9471
Gilles Debunneaaa84792010-12-03 11:10:14 -08009472 Object localState = event.getLocalState();
9473 DragLocalState dragLocalState = null;
9474 if (localState instanceof DragLocalState) {
9475 dragLocalState = (DragLocalState) localState;
9476 }
9477 boolean dragDropIntoItself = dragLocalState != null &&
9478 dragLocalState.sourceTextView == this;
9479
9480 if (dragDropIntoItself) {
9481 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
Gilles Debunne2226a192010-11-30 18:50:51 -08009482 // A drop inside the original selection discards the drop.
9483 return;
9484 }
9485 }
9486
9487 final int originalLength = mText.length();
9488 long minMax = prepareSpacesAroundPaste(offset, offset, content);
9489 int min = extractRangeStartFromLong(minMax);
9490 int max = extractRangeEndFromLong(minMax);
9491
9492 Selection.setSelection((Spannable) mText, max);
9493 ((Editable) mText).replace(min, max, content);
9494
Gilles Debunneaaa84792010-12-03 11:10:14 -08009495 if (dragDropIntoItself) {
9496 int dragSourceStart = dragLocalState.start;
9497 int dragSourceEnd = dragLocalState.end;
Gilles Debunne2226a192010-11-30 18:50:51 -08009498 if (max <= dragSourceStart) {
9499 // Inserting text before selection has shifted positions
9500 final int shift = mText.length() - originalLength;
9501 dragSourceStart += shift;
9502 dragSourceEnd += shift;
9503 }
9504
9505 // Delete original selection
9506 ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
9507
9508 // Make sure we do not leave two adjacent spaces.
9509 if ((dragSourceStart == 0 ||
9510 Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
9511 (dragSourceStart == mText.length() ||
9512 Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
9513 final int pos = dragSourceStart == mText.length() ?
9514 dragSourceStart - 1 : dragSourceStart;
9515 ((Editable) mText).delete(pos, pos + 1);
9516 }
9517 }
9518 }
9519
Adam Powell965b9692010-10-21 18:44:32 -07009520 /**
9521 * @return True if this view supports insertion handles.
9522 */
9523 boolean hasInsertionController() {
9524 return mInsertionControllerEnabled;
9525 }
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009526
Adam Powell965b9692010-10-21 18:44:32 -07009527 /**
9528 * @return True if this view supports selection handles.
9529 */
9530 boolean hasSelectionController() {
9531 return mSelectionControllerEnabled;
9532 }
9533
Gilles Debunne9948ad72010-11-24 14:00:46 -08009534 InsertionPointCursorController getInsertionController() {
Adam Powell965b9692010-10-21 18:44:32 -07009535 if (!mInsertionControllerEnabled) {
9536 return null;
9537 }
9538
9539 if (mInsertionPointCursorController == null) {
9540 mInsertionPointCursorController = new InsertionPointCursorController();
9541
9542 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08009543 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
Adam Powell965b9692010-10-21 18:44:32 -07009544 }
9545
9546 return mInsertionPointCursorController;
9547 }
9548
Gilles Debunnee587d832010-11-23 20:20:11 -08009549 SelectionModifierCursorController getSelectionController() {
Adam Powell965b9692010-10-21 18:44:32 -07009550 if (!mSelectionControllerEnabled) {
9551 return null;
9552 }
9553
9554 if (mSelectionModifierCursorController == null) {
9555 mSelectionModifierCursorController = new SelectionModifierCursorController();
9556
9557 final ViewTreeObserver observer = getViewTreeObserver();
Gilles Debunne81f08082011-02-17 14:07:19 -08009558 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
Adam Powell965b9692010-10-21 18:44:32 -07009559 }
9560
9561 return mSelectionModifierCursorController;
9562 }
9563
9564 boolean isInBatchEditMode() {
9565 final InputMethodState ims = mInputMethodState;
9566 if (ims != null) {
9567 return ims.mBatchEditNesting > 0;
9568 }
9569 return mInBatchEditControllers;
9570 }
9571
Romain Guy6d956622010-11-29 11:38:05 -08009572 @ViewDebug.ExportedProperty(category = "text")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009573 private CharSequence mText;
9574 private CharSequence mTransformed;
9575 private BufferType mBufferType = BufferType.NORMAL;
9576
9577 private int mInputType = EditorInfo.TYPE_NULL;
9578 private CharSequence mHint;
9579 private Layout mHintLayout;
9580
9581 private KeyListener mInput;
9582
9583 private MovementMethod mMovement;
9584 private TransformationMethod mTransformation;
9585 private ChangeWatcher mChangeWatcher;
9586
9587 private ArrayList<TextWatcher> mListeners = null;
9588
9589 // display attributes
Gilles Debunneb6ca7232010-06-24 11:45:21 -07009590 private final TextPaint mTextPaint;
Romain Guy939151f2009-04-08 14:22:40 -07009591 private boolean mUserSetTextScaleX;
Gilles Debunneb6ca7232010-06-24 11:45:21 -07009592 private final Paint mHighlightPaint;
Gilles Debunnea6d7ee12010-08-13 14:43:10 -07009593 private int mHighlightColor = 0xCC475925;
Leon Scroggins1ca56262010-11-18 14:03:03 -05009594 /**
9595 * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
9596 * this field being protected. Will be restored as private when lineHeight
9597 * feature request 3215097 is implemented
9598 * @hide
9599 */
9600 protected Layout mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009601
9602 private long mShowCursor;
9603 private Blink mBlink;
9604 private boolean mCursorVisible = true;
9605
Adam Powell965b9692010-10-21 18:44:32 -07009606 // Cursor Controllers.
Gilles Debunne9948ad72010-11-24 14:00:46 -08009607 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunnee587d832010-11-23 20:20:11 -08009608 private SelectionModifierCursorController mSelectionModifierCursorController;
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009609 private ActionMode mSelectionActionMode;
Adam Powell965b9692010-10-21 18:44:32 -07009610 private boolean mInsertionControllerEnabled;
9611 private boolean mSelectionControllerEnabled;
9612 private boolean mInBatchEditControllers;
9613
Gilles Debunnecf1e9252010-10-07 20:46:03 -07009614 // These are needed to desambiguate a long click. If the long click comes from ones of these, we
9615 // select from the current cursor position. Otherwise, select from long pressed position.
9616 private boolean mDPadCenterIsDown = false;
9617 private boolean mEnterKeyIsDown = false;
Gilles Debunne528c64882010-10-08 11:56:13 -07009618 private boolean mContextMenuTriggeredByKey = false;
Gilles Debunne05336272010-07-09 20:13:45 -07009619 // Created once and shared by different CursorController helper methods.
Gilles Debunnef788a9f2010-07-22 10:17:23 -07009620 // Only one cursor controller is active at any time which prevent race conditions.
9621 private static Rect sCursorControllerTempRect = new Rect();
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -07009622
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009623 private boolean mSelectAllOnFocus = false;
9624
9625 private int mGravity = Gravity.TOP | Gravity.LEFT;
9626 private boolean mHorizontallyScrolling;
9627
9628 private int mAutoLinkMask;
9629 private boolean mLinksClickable = true;
9630
9631 private float mSpacingMult = 1;
9632 private float mSpacingAdd = 0;
Gilles Debunne86b9c782010-11-11 10:43:48 -08009633 private boolean mTextIsSelectable = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009634
9635 private static final int LINES = 1;
9636 private static final int EMS = LINES;
9637 private static final int PIXELS = 2;
9638
9639 private int mMaximum = Integer.MAX_VALUE;
9640 private int mMaxMode = LINES;
9641 private int mMinimum = 0;
9642 private int mMinMode = LINES;
9643
9644 private int mMaxWidth = Integer.MAX_VALUE;
9645 private int mMaxWidthMode = PIXELS;
9646 private int mMinWidth = 0;
9647 private int mMinWidthMode = PIXELS;
9648
9649 private boolean mSingleLine;
9650 private int mDesiredHeightAtMeasure = -1;
9651 private boolean mIncludePad = true;
9652
9653 // tmp primitives, so we don't alloc them on each draw
9654 private Path mHighlightPath;
9655 private boolean mHighlightPathBogus = true;
9656 private static final RectF sTempRect = new RectF();
9657
9658 // XXX should be much larger
9659 private static final int VERY_WIDE = 16384;
9660
9661 private static final int BLINK = 500;
9662
9663 private static final int ANIMATED_SCROLL_GAP = 250;
9664 private long mLastScroll;
9665 private Scroller mScroller = null;
9666
9667 private BoringLayout.Metrics mBoring;
9668 private BoringLayout.Metrics mHintBoring;
9669
9670 private BoringLayout mSavedLayout, mSavedHintLayout;
9671
9672 private static final InputFilter[] NO_FILTERS = new InputFilter[0];
9673 private InputFilter[] mFilters = NO_FILTERS;
9674 private static final Spanned EMPTY_SPANNED = new SpannedString("");
Christopher Tate36d4c3f2011-01-07 13:34:24 -08009675 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Gilles Debunne0a2aa402010-11-24 17:57:46 -08009676 // System wide time for last cut or copy action.
9677 private static long sLastCutOrCopyTime;
Gilles Debunne12d91ce2010-12-10 11:36:29 -08009678 // Used to highlight a word when it is corrected by the IME
9679 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunneda0a3f02010-12-22 10:07:15 -08009680 // New state used to change background based on whether this TextView is multiline.
9681 private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08009682}