package com.glview.widget; import java.lang.ref.WeakReference; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.text.GetChars; import android.util.AttributeSet; import android.util.TypedValue; import com.glview.content.GLContext; import com.glview.graphics.Rect; import com.glview.graphics.Typeface; import com.glview.graphics.drawable.Drawable; import com.glview.hwui.GLCanvas; import com.glview.hwui.GLPaint; import com.glview.text.BoringLayout; import com.glview.text.Layout; import com.glview.text.StaticLayout; import com.glview.text.TextDirectionHeuristic; import com.glview.text.TextDirectionHeuristics; import com.glview.text.TextUtils; import com.glview.text.TextUtils.TruncateAt; import com.glview.thread.Handler; import com.glview.thread.Looper; import com.glview.util.FastMath; import com.glview.view.Gravity; import com.glview.view.View; import com.glview.view.ViewGroup.LayoutParams; import com.glview.view.ViewTreeObserver; import com.glview.view.animation.AnimationUtils; public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { private final static String TAG = "TextView"; /** * Draw marquee text with fading edges as usual */ private static final int MARQUEE_FADE_NORMAL = 0; /** * Draw marquee text as ellipsize end while inactive instead of with the fade. * (Useful for devices where the fade can be expensive if overdone) */ private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1; /** * Draw marquee text with fading edges because it is currently active/animating. */ private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2; private static final int LINES = 1; private static final int EMS = LINES; private static final int PIXELS = 2; // XXX should be much larger private static final int VERY_WIDE = 1024*1024; private static final int ANIMATED_SCROLL_GAP = 250; private float mShadowRadius, mShadowDx, mShadowDy; private int mShadowColor; private boolean mPreDrawRegistered; private boolean mPreDrawListenerDetached; // A flag to prevent repeated movements from escaping the enclosing text view. The idea here is // that if a user is holding down a movement key to traverse text, we shouldn't also traverse // the view hierarchy. On the other hand, if the user is using the movement key to traverse views // (i.e. the first movement was to traverse out of this view, or this view was traversed into by // the user holding the movement key down) then we shouldn't prevent the focus from changing. private boolean mPreventDefaultMovement; private TextUtils.TruncateAt mEllipsize; static class Drawables { final static int DRAWABLE_NONE = -1; final static int DRAWABLE_RIGHT = 0; final static int DRAWABLE_LEFT = 1; final Rect mCompoundRect = new Rect(); Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight, mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp; Drawable mDrawableLeftInitial, mDrawableRightInitial; boolean mOverride; int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight, mDrawableSizeStart, mDrawableSizeEnd, mDrawableSizeError, mDrawableSizeTemp; int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight, mDrawableHeightStart, mDrawableHeightEnd, mDrawableHeightError, mDrawableHeightTemp; int mDrawablePadding; int mDrawableSaved = DRAWABLE_NONE; public Drawables(Context context) { final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; mOverride = false; } public void resolveWithLayoutDirection(int layoutDirection) { // First reset "left" and "right" drawables to their initial values mDrawableLeft = mDrawableLeftInitial; mDrawableRight = mDrawableRightInitial; // JB-MR1+ normal case: "start" / "end" drawables are overriding "left" / "right" // drawable if and only if they have been defined switch(layoutDirection) { case LAYOUT_DIRECTION_RTL: if (mOverride) { mDrawableRight = mDrawableStart; mDrawableSizeRight = mDrawableSizeStart; mDrawableHeightRight = mDrawableHeightStart; mDrawableLeft = mDrawableEnd; mDrawableSizeLeft = mDrawableSizeEnd; mDrawableHeightLeft = mDrawableHeightEnd; } break; case LAYOUT_DIRECTION_LTR: default: if (mOverride) { mDrawableLeft = mDrawableStart; mDrawableSizeLeft = mDrawableSizeStart; mDrawableHeightLeft = mDrawableHeightStart; mDrawableRight = mDrawableEnd; mDrawableSizeRight = mDrawableSizeEnd; mDrawableHeightRight = mDrawableHeightEnd; } break; } applyErrorDrawableIfNeeded(layoutDirection); updateDrawablesLayoutDirection(layoutDirection); } private void updateDrawablesLayoutDirection(int layoutDirection) { if (mDrawableLeft != null) { mDrawableLeft.setLayoutDirection(layoutDirection); } if (mDrawableRight != null) { mDrawableRight.setLayoutDirection(layoutDirection); } if (mDrawableTop != null) { mDrawableTop.setLayoutDirection(layoutDirection); } if (mDrawableBottom != null) { mDrawableBottom.setLayoutDirection(layoutDirection); } } public void setErrorDrawable(Drawable dr, TextView tv) { if (mDrawableError != dr && mDrawableError != null) { mDrawableError.setCallback(null); } mDrawableError = dr; final Rect compoundRect = mCompoundRect; int[] state = tv.getDrawableState(); if (mDrawableError != null) { mDrawableError.setState(state); mDrawableError.copyBounds(compoundRect); mDrawableError.setCallback(tv); mDrawableSizeError = compoundRect.width(); mDrawableHeightError = compoundRect.height(); } else { mDrawableSizeError = mDrawableHeightError = 0; } } private void applyErrorDrawableIfNeeded(int layoutDirection) { // first restore the initial state if needed switch (mDrawableSaved) { case DRAWABLE_LEFT: mDrawableLeft = mDrawableTemp; mDrawableSizeLeft = mDrawableSizeTemp; mDrawableHeightLeft = mDrawableHeightTemp; break; case DRAWABLE_RIGHT: mDrawableRight = mDrawableTemp; mDrawableSizeRight = mDrawableSizeTemp; mDrawableHeightRight = mDrawableHeightTemp; break; case DRAWABLE_NONE: default: } // then, if needed, assign the Error drawable to the correct location if (mDrawableError != null) { switch(layoutDirection) { case LAYOUT_DIRECTION_RTL: mDrawableSaved = DRAWABLE_LEFT; mDrawableTemp = mDrawableLeft; mDrawableSizeTemp = mDrawableSizeLeft; mDrawableHeightTemp = mDrawableHeightLeft; mDrawableLeft = mDrawableError; mDrawableSizeLeft = mDrawableSizeError; mDrawableHeightLeft = mDrawableHeightError; break; case LAYOUT_DIRECTION_LTR: default: mDrawableSaved = DRAWABLE_RIGHT; mDrawableTemp = mDrawableRight; mDrawableSizeTemp = mDrawableSizeRight; mDrawableHeightTemp = mDrawableHeightRight; mDrawableRight = mDrawableError; mDrawableSizeRight = mDrawableSizeError; mDrawableHeightRight = mDrawableHeightError; break; } } } } Drawables mDrawables; private CharWrapper mCharWrapper; private Marquee mMarquee; private boolean mRestartMarquee; private int mMarqueeRepeatLimit = 3; private int mLastLayoutDirection = -1; /** * On some devices the fading edges add a performance penalty if used * extensively in the same layout. This mode indicates how the marquee * is currently being shown, if applicable. (mEllipsize will == MARQUEE) */ private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL; /** * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores * the layout that should be used when the mode switches. */ private Layout mSavedMarqueeModeLayout; private CharSequence mText; private boolean mDrawDefer = true;; private final GLPaint mTextPaint; private boolean mUserSetTextScaleX; private Layout mLayout; private int mGravity = Gravity.TOP | Gravity.START; private boolean mHorizontallyScrolling; private float mSpacingMult = 1.0f; private float mSpacingAdd = 0.0f; private int mMaximum = Integer.MAX_VALUE; private int mMaxMode = LINES; private int mMinimum = 0; private int mMinMode = LINES; private int mOldMaximum = mMaximum; private int mOldMaxMode = mMaxMode; private int mMaxWidth = Integer.MAX_VALUE; private int mMaxWidthMode = PIXELS; private int mMinWidth = 0; private int mMinWidthMode = PIXELS; private boolean mSingleLine; private int mDesiredHeightAtMeasure = -1; private boolean mIncludePad = true; private BoringLayout.Metrics mBoring; private BoringLayout mSavedLayout; private TextDirectionHeuristic mTextDir; public TextView(Context context) { this(context, null); } public TextView(Context context, AttributeSet attrs) { this(context, attrs, com.glview.R.attr.textViewStyle); } public TextView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mText = ""; final Resources res = context.getResources(); mTextPaint = new GLPaint(); int textColor = Color.WHITE; int textSize = 15; boolean allCaps = false; int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean elegant = false; float letterSpacing = 0; Drawable drawableLeft = null, drawableTop = null, drawableRight = null, drawableBottom = null, drawableStart = null, drawableEnd = null; int drawablePadding = 0; int ellipsize = -1; boolean singleLine = false; int maxlength = -1; CharSequence text = ""; final Resources.Theme theme = context.getTheme(); final TypedArray a = theme.obtainStyledAttributes(attrs, com.glview.R.styleable.TextView, defStyleAttr, defStyleRes); final int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); if (attr == com.glview.R.styleable.TextView_autoLink) { // mAutoLinkMask = a.getInt(attr, 0); } else if (attr == com.glview.R.styleable.TextView_linksClickable) { // mLinksClickable = a.getBoolean(attr, true); } else if (attr == com.glview.R.styleable.TextView_maxLines) { setMaxLines(a.getInt(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_maxHeight) { setMaxHeight(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_lines) { setLines(a.getInt(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_height) { setHeight(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_minLines) { setMinLines(a.getInt(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_minHeight) { setMinHeight(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_maxWidth) { setMaxWidth(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_width) { setWidth(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_minWidth) { setMinWidth(a.getDimensionPixelSize(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_gravity) { setGravity(a.getInt(attr, -1)); } else if (attr == com.glview.R.styleable.TextView_hint) { // hint = a.getText(attr); } else if (attr == com.glview.R.styleable.TextView_text) { mText = a.getText(attr); } else if (attr == com.glview.R.styleable.TextView_scrollHorizontally) { // if (a.getBoolean(attr, false)) { // setHorizontallyScrolling(true); // } } else if (attr == com.glview.R.styleable.TextView_singleLine) { singleLine = a.getBoolean(attr, singleLine); } else if (attr == com.glview.R.styleable.TextView_ellipsize) { ellipsize = a.getInt(attr, ellipsize); } else if (attr == com.glview.R.styleable.TextView_marqueeRepeatLimit) { setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit)); } else if (attr == com.glview.R.styleable.TextView_includeFontPadding) { // if (!a.getBoolean(attr, true)) { // setIncludeFontPadding(false); // } } else if (attr == com.glview.R.styleable.TextView_maxLength) { maxlength = a.getInt(attr, -1); } else if (attr == com.glview.R.styleable.TextView_textScaleX) { // setTextScaleX(a.getFloat(attr, 1.0f)); } else if (attr == com.glview.R.styleable.TextView_shadowColor) { shadowcolor = a.getInt(attr, 0); } else if (attr == com.glview.R.styleable.TextView_shadowDx) { dx = a.getFloat(attr, 0); } else if (attr == com.glview.R.styleable.TextView_shadowDy) { dy = a.getFloat(attr, 0); } else if (attr == com.glview.R.styleable.TextView_shadowRadius) { r = a.getFloat(attr, 0); } else if (attr == com.glview.R.styleable.TextView_enabled) { setEnabled(a.getBoolean(attr, isEnabled())); } else if (attr == com.glview.R.styleable.TextView_textColorHighlight) { // textColorHighlight = a.getColor(attr, textColorHighlight); } else if (attr == com.glview.R.styleable.TextView_textColor) { textColor = a.getColor(attr, textColor); } else if (attr == com.glview.R.styleable.TextView_textColorHint) { // textColorHint = a.getColorStateList(attr); } else if (attr == com.glview.R.styleable.TextView_textColorLink) { // textColorLink = a.getColorStateList(attr); } else if (attr == com.glview.R.styleable.TextView_textSize) { textSize = a.getDimensionPixelSize(attr, textSize); } else if (attr == com.glview.R.styleable.TextView_typeface) { // typefaceIndex = a.getInt(attr, typefaceIndex); } else if (attr == com.glview.R.styleable.TextView_textStyle) { // styleIndex = a.getInt(attr, styleIndex); } else if (attr == com.glview.R.styleable.TextView_fontFamily) { // fontFamily = a.getString(attr); } } a.recycle(); // This call will save the initial left/right drawables setCompoundDrawablesWithIntrinsicBounds( drawableLeft, drawableTop, drawableRight, drawableBottom); setRelativeDrawablesIfNeeded(drawableStart, drawableEnd); setCompoundDrawablePadding(drawablePadding); applySingleLine(singleLine, singleLine); if (singleLine && ellipsize < 0) { ellipsize = 3; // END } switch (ellipsize) { case 1: setEllipsize(TextUtils.TruncateAt.START); break; case 2: setEllipsize(TextUtils.TruncateAt.MIDDLE); break; case 3: setEllipsize(TextUtils.TruncateAt.END); break; case 4: mMarqueeFadeMode = MARQUEE_FADE_NORMAL; setEllipsize(TextUtils.TruncateAt.MARQUEE); break; } setRawTextSize(textSize); setTextColor(textColor); if(shadowcolor != 0){ setShadowLayer(r, dx, dy, shadowcolor); } } /** * Returns the top padding of the view, plus space for the top * Drawable if any. */ public int getCompoundPaddingTop() { final Drawables dr = mDrawables; if (dr == null || dr.mDrawableTop == null) { return mPaddingTop; } else { return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop; } } /** * Returns the bottom padding of the view, plus space for the bottom * Drawable if any. */ public int getCompoundPaddingBottom() { final Drawables dr = mDrawables; if (dr == null || dr.mDrawableBottom == null) { return mPaddingBottom; } else { return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom; } } /** * Returns the left padding of the view, plus space for the left * Drawable if any. */ public int getCompoundPaddingLeft() { final Drawables dr = mDrawables; if (dr == null || dr.mDrawableLeft == null) { return mPaddingLeft; } else { return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft; } } /** * Returns the right padding of the view, plus space for the right * Drawable if any. */ public int getCompoundPaddingRight() { final Drawables dr = mDrawables; if (dr == null || dr.mDrawableRight == null) { return mPaddingRight; } else { return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight; } } /** * Returns the start padding of the view, plus space for the start * Drawable if any. */ public int getCompoundPaddingStart() { resolveDrawables(); switch(getLayoutDirection()) { default: case LAYOUT_DIRECTION_LTR: return getCompoundPaddingLeft(); case LAYOUT_DIRECTION_RTL: return getCompoundPaddingRight(); } } /** * Returns the end padding of the view, plus space for the end * Drawable if any. */ public int getCompoundPaddingEnd() { resolveDrawables(); switch(getLayoutDirection()) { default: case LAYOUT_DIRECTION_LTR: return getCompoundPaddingRight(); case LAYOUT_DIRECTION_RTL: return getCompoundPaddingLeft(); } } /** * Returns the extended top padding of the view, including both the * top Drawable if any and any extra space to keep more than maxLines * of text from showing. It is only valid to call this after measuring. */ public int getExtendedPaddingTop() { if (mMaxMode != LINES) { return getCompoundPaddingTop(); } if (mLayout == null) { assumeLayout(); } if (mLayout.getLineCount() <= mMaximum) { return getCompoundPaddingTop(); } int top = getCompoundPaddingTop(); int bottom = getCompoundPaddingBottom(); int viewht = getHeight() - top - bottom; int layoutht = mLayout.getLineTop(mMaximum); if (layoutht >= viewht) { return top; } final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; if (gravity == Gravity.TOP) { return top; } else if (gravity == Gravity.BOTTOM) { return top + viewht - layoutht; } else { // (gravity == Gravity.CENTER_VERTICAL) return top + (viewht - layoutht) / 2; } } /** * Returns the extended bottom padding of the view, including both the * bottom Drawable if any and any extra space to keep more than maxLines * of text from showing. It is only valid to call this after measuring. */ public int getExtendedPaddingBottom() { if (mMaxMode != LINES) { return getCompoundPaddingBottom(); } if (mLayout == null) { assumeLayout(); } if (mLayout.getLineCount() <= mMaximum) { return getCompoundPaddingBottom(); } int top = getCompoundPaddingTop(); int bottom = getCompoundPaddingBottom(); int viewht = getHeight() - top - bottom; int layoutht = mLayout.getLineTop(mMaximum); if (layoutht >= viewht) { return bottom; } final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; if (gravity == Gravity.TOP) { return bottom + viewht - layoutht; } else if (gravity == Gravity.BOTTOM) { return bottom; } else { // (gravity == Gravity.CENTER_VERTICAL) return bottom + (viewht - layoutht) / 2; } } /** * Returns the total left padding of the view, including the left * Drawable if any. */ public int getTotalPaddingLeft() { return getCompoundPaddingLeft(); } /** * Returns the total right padding of the view, including the right * Drawable if any. */ public int getTotalPaddingRight() { return getCompoundPaddingRight(); } /** * Returns the total start padding of the view, including the start * Drawable if any. */ public int getTotalPaddingStart() { return getCompoundPaddingStart(); } /** * Returns the total end padding of the view, including the end * Drawable if any. */ public int getTotalPaddingEnd() { return getCompoundPaddingEnd(); } /** * Returns the total top padding of the view, including the top * Drawable if any, the extra space to keep more than maxLines * from showing, and the vertical offset for gravity, if any. */ public int getTotalPaddingTop() { return getExtendedPaddingTop() + getVerticalOffset(true); } /** * Returns the total bottom padding of the view, including the bottom * Drawable if any, the extra space to keep more than maxLines * from showing, and the vertical offset for gravity, if any. */ public int getTotalPaddingBottom() { return getExtendedPaddingBottom() + getBottomVerticalOffset(true); } private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) { boolean hasRelativeDrawables = (start != null) || (end != null); if (hasRelativeDrawables) { Drawables dr = mDrawables; if (dr == null) { mDrawables = dr = new Drawables(getContext()); } mDrawables.mOverride = true; final Rect compoundRect = dr.mCompoundRect; int[] state = getDrawableState(); if (start != null) { start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); start.setState(state); start.copyBounds(compoundRect); start.setCallback(this); dr.mDrawableStart = start; dr.mDrawableSizeStart = compoundRect.width(); dr.mDrawableHeightStart = compoundRect.height(); } else { dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; } if (end != null) { end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); end.setState(state); end.copyBounds(compoundRect); end.setCallback(this); dr.mDrawableEnd = end; dr.mDrawableSizeEnd = compoundRect.width(); dr.mDrawableHeightEnd = compoundRect.height(); } else { dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; } resetResolvedDrawables(); resolveDrawables(); } } /** * Sets the Drawables (if any) to appear to the left of, above, to the * right of, and below the text. Use {@code null} if you do not want a * Drawable there. The Drawables must already have had * {@link Drawable#setBounds} called. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawablesRelative} or related methods. * * @attr ref android.R.styleable#TextView_drawableLeft * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawables( Drawable left, Drawable top, Drawable right, Drawable bottom) { Drawables dr = mDrawables; // We're switching to absolute, discard relative. if (dr != null) { if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null); dr.mDrawableStart = null; if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null); dr.mDrawableEnd = null; dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; } final boolean drawables = left != null || top != null || right != null || bottom != null; if (!drawables) { // Clearing drawables... can we free the data structure? if (dr != null) { if (dr.mDrawablePadding == 0) { mDrawables = null; } else { // We need to retain the last set padding, so just clear // out all of the fields in the existing structure. if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); dr.mDrawableLeft = null; if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); dr.mDrawableRight = null; if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); dr.mDrawableBottom = null; dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } } else { if (dr == null) { mDrawables = dr = new Drawables(getContext()); } mDrawables.mOverride = false; if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { dr.mDrawableLeft.setCallback(null); } dr.mDrawableLeft = left; if (dr.mDrawableTop != top && dr.mDrawableTop != null) { dr.mDrawableTop.setCallback(null); } dr.mDrawableTop = top; if (dr.mDrawableRight != right && dr.mDrawableRight != null) { dr.mDrawableRight.setCallback(null); } dr.mDrawableRight = right; if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { dr.mDrawableBottom.setCallback(null); } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; state = getDrawableState(); if (left != null) { left.setState(state); left.copyBounds(compoundRect); left.setCallback(this); dr.mDrawableSizeLeft = compoundRect.width(); dr.mDrawableHeightLeft = compoundRect.height(); } else { dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; } if (right != null) { right.setState(state); right.copyBounds(compoundRect); right.setCallback(this); dr.mDrawableSizeRight = compoundRect.width(); dr.mDrawableHeightRight = compoundRect.height(); } else { dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; } if (top != null) { top.setState(state); top.copyBounds(compoundRect); top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; } if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); bottom.setCallback(this); dr.mDrawableSizeBottom = compoundRect.height(); dr.mDrawableWidthBottom = compoundRect.width(); } else { dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } // Save initial left/right drawables if (dr != null) { dr.mDrawableLeftInitial = left; dr.mDrawableRightInitial = right; } resetResolvedDrawables(); resolveDrawables(); invalidate(); requestLayout(); } /** * Sets the Drawables (if any) to appear to the left of, above, to the * right of, and below the text. Use 0 if you do not want a Drawable there. * The Drawables' bounds will be set to their intrinsic bounds. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawablesRelative} or related methods. * * @param left Resource identifier of the left Drawable. * @param top Resource identifier of the top Drawable. * @param right Resource identifier of the right Drawable. * @param bottom Resource identifier of the bottom Drawable. * * @attr ref android.R.styleable#TextView_drawableLeft * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { final Context context = getContext(); setCompoundDrawablesWithIntrinsicBounds(left != 0 ? GLContext.get().getResources().getDrawable(left) : null, top != 0 ? GLContext.get().getResources().getDrawable(top) : null, right != 0 ? GLContext.get().getResources().getDrawable(right) : null, bottom != 0 ? GLContext.get().getResources().getDrawable(bottom) : null); } /** * Sets the Drawables (if any) to appear to the left of, above, to the * right of, and below the text. Use {@code null} if you do not want a * Drawable there. The Drawables' bounds will be set to their intrinsic * bounds. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawablesRelative} or related methods. * * @attr ref android.R.styleable#TextView_drawableLeft * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom) { if (left != null) { left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight()); } if (right != null) { right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight()); } if (top != null) { top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); } if (bottom != null) { bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); } setCompoundDrawables(left, top, right, bottom); } /** * Sets the Drawables (if any) to appear to the start of, above, to the end * of, and below the text. Use {@code null} if you do not want a Drawable * there. The Drawables must already have had {@link Drawable#setBounds} * called. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawables} or related methods. * * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, Drawable bottom) { Drawables dr = mDrawables; // We're switching to relative, discard absolute. if (dr != null) { if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); dr.mDrawableLeft = dr.mDrawableLeftInitial = null; if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); dr.mDrawableRight = dr.mDrawableRightInitial = null; dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; } final boolean drawables = start != null || top != null || end != null || bottom != null; if (!drawables) { // Clearing drawables... can we free the data structure? if (dr != null) { if (dr.mDrawablePadding == 0) { mDrawables = null; } else { // We need to retain the last set padding, so just clear // out all of the fields in the existing structure. if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null); dr.mDrawableStart = null; if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null); dr.mDrawableEnd = null; if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); dr.mDrawableBottom = null; dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } } else { if (dr == null) { mDrawables = dr = new Drawables(getContext()); } mDrawables.mOverride = true; if (dr.mDrawableStart != start && dr.mDrawableStart != null) { dr.mDrawableStart.setCallback(null); } dr.mDrawableStart = start; if (dr.mDrawableTop != top && dr.mDrawableTop != null) { dr.mDrawableTop.setCallback(null); } dr.mDrawableTop = top; if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) { dr.mDrawableEnd.setCallback(null); } dr.mDrawableEnd = end; if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { dr.mDrawableBottom.setCallback(null); } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; state = getDrawableState(); if (start != null) { start.setState(state); start.copyBounds(compoundRect); start.setCallback(this); dr.mDrawableSizeStart = compoundRect.width(); dr.mDrawableHeightStart = compoundRect.height(); } else { dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; } if (end != null) { end.setState(state); end.copyBounds(compoundRect); end.setCallback(this); dr.mDrawableSizeEnd = compoundRect.width(); dr.mDrawableHeightEnd = compoundRect.height(); } else { dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; } if (top != null) { top.setState(state); top.copyBounds(compoundRect); top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; } if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); bottom.setCallback(this); dr.mDrawableSizeBottom = compoundRect.height(); dr.mDrawableWidthBottom = compoundRect.width(); } else { dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } resetResolvedDrawables(); resolveDrawables(); invalidate(); requestLayout(); } /** * Sets the Drawables (if any) to appear to the start of, above, to the end * of, and below the text. Use 0 if you do not want a Drawable there. The * Drawables' bounds will be set to their intrinsic bounds. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawables} or related methods. * * @param start Resource identifier of the start Drawable. * @param top Resource identifier of the top Drawable. * @param end Resource identifier of the end Drawable. * @param bottom Resource identifier of the bottom Drawable. * * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, int bottom) { final Context context = getContext(); setCompoundDrawablesRelativeWithIntrinsicBounds( start != 0 ? GLContext.get().getResources().getDrawable(start) : null, top != 0 ? GLContext.get().getResources().getDrawable(top) : null, end != 0 ? GLContext.get().getResources().getDrawable(end) : null, bottom != 0 ? GLContext.get().getResources().getDrawable(bottom) : null); } /** * Sets the Drawables (if any) to appear to the start of, above, to the end * of, and below the text. Use {@code null} if you do not want a Drawable * there. The Drawables' bounds will be set to their intrinsic bounds. * <p> * Calling this method will overwrite any Drawables previously set using * {@link #setCompoundDrawables} or related methods. * * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom */ public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top, Drawable end, Drawable bottom) { if (start != null) { start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); } if (end != null) { end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); } if (top != null) { top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); } if (bottom != null) { bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); } setCompoundDrawablesRelative(start, top, end, bottom); } /** * Returns drawables for the left, top, right, and bottom borders. * * @attr ref android.R.styleable#TextView_drawableLeft * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableBottom */ public Drawable[] getCompoundDrawables() { final Drawables dr = mDrawables; if (dr != null) { return new Drawable[] { dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom }; } else { return new Drawable[] { null, null, null, null }; } } /** * Returns drawables for the start, top, end, and bottom borders. * * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom */ public Drawable[] getCompoundDrawablesRelative() { final Drawables dr = mDrawables; if (dr != null) { return new Drawable[] { dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, dr.mDrawableBottom }; } else { return new Drawable[] { null, null, null, null }; } } /** * Sets the size of the padding between the compound drawables and * the text. * * @attr ref android.R.styleable#TextView_drawablePadding */ public void setCompoundDrawablePadding(int pad) { Drawables dr = mDrawables; if (pad == 0) { if (dr != null) { dr.mDrawablePadding = pad; } } else { if (dr == null) { mDrawables = dr = new Drawables(getContext()); } dr.mDrawablePadding = pad; } invalidate(); requestLayout(); } /** * Returns the padding between the compound drawables and the text. * * @attr ref android.R.styleable#TextView_drawablePadding */ public int getCompoundDrawablePadding() { final Drawables dr = mDrawables; return dr != null ? dr.mDrawablePadding : 0; } @Override public void setPadding(int left, int top, int right, int bottom) { if (left != mPaddingLeft || right != mPaddingRight || top != mPaddingTop || bottom != mPaddingBottom) { nullLayouts(); } // the super call will requestLayout() super.setPadding(left, top, right, bottom); invalidate(); } @Override public void setPaddingRelative(int start, int top, int end, int bottom) { if (start != getPaddingStart() || end != getPaddingEnd() || top != mPaddingTop || bottom != mPaddingBottom) { nullLayouts(); } // the super call will requestLayout() super.setPaddingRelative(start, top, end, bottom); invalidate(); } /** * Sets whether the text should be allowed to be wider than the * View is. If false, it will be wrapped to the width of the View. * * @attr ref android.R.styleable#TextView_scrollHorizontally */ public void setHorizontallyScrolling(boolean whether) { if (mHorizontallyScrolling != whether) { mHorizontallyScrolling = whether; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } /** * Returns whether the text is allowed to be wider than the View is. * If false, the text will be wrapped to the width of the View. * * @attr ref android.R.styleable#TextView_scrollHorizontally * @hide */ public boolean getHorizontallyScrolling() { return mHorizontallyScrolling; } /** * Makes the TextView at least this many lines tall. * * Setting this value overrides any other (minimum) height setting. A single line TextView will * set this value to 1. * * @see #getMinLines() * * @attr ref android.R.styleable#TextView_minLines */ public void setMinLines(int minlines) { mMinimum = minlines; mMinMode = LINES; requestLayout(); invalidate(); } /** * @return the minimum number of lines displayed in this TextView, or -1 if the minimum * height was set in pixels instead using {@link #setMinHeight(int) or #setHeight(int)}. * * @see #setMinLines(int) * * @attr ref android.R.styleable#TextView_minLines */ public int getMinLines() { return mMinMode == LINES ? mMinimum : -1; } /** * Makes the TextView at least this many pixels tall. * * Setting this value overrides any other (minimum) number of lines setting. * * @attr ref android.R.styleable#TextView_minHeight */ public void setMinHeight(int minHeight) { mMinimum = minHeight; mMinMode = PIXELS; requestLayout(); invalidate(); } /** * @return the minimum height of this TextView expressed in pixels, or -1 if the minimum * height was set in number of lines instead using {@link #setMinLines(int) or #setLines(int)}. * * @see #setMinHeight(int) * * @attr ref android.R.styleable#TextView_minHeight */ public int getMinHeight() { return mMinMode == PIXELS ? mMinimum : -1; } /** * Makes the TextView at most this many lines tall. * * Setting this value overrides any other (maximum) height setting. * * @attr ref android.R.styleable#TextView_maxLines */ public void setMaxLines(int maxlines) { mMaximum = maxlines; mMaxMode = LINES; requestLayout(); invalidate(); } /** * @return the maximum number of lines displayed in this TextView, or -1 if the maximum * height was set in pixels instead using {@link #setMaxHeight(int) or #setHeight(int)}. * * @see #setMaxLines(int) * * @attr ref android.R.styleable#TextView_maxLines */ public int getMaxLines() { return mMaxMode == LINES ? mMaximum : -1; } /** * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the * {@link #setMaxLines(int)} method. * * Setting this value overrides any other (maximum) number of lines setting. * * @attr ref android.R.styleable#TextView_maxHeight */ public void setMaxHeight(int maxHeight) { mMaximum = maxHeight; mMaxMode = PIXELS; requestLayout(); invalidate(); } /** * @return the maximum height of this TextView expressed in pixels, or -1 if the maximum * height was set in number of lines instead using {@link #setMaxLines(int) or #setLines(int)}. * * @see #setMaxHeight(int) * * @attr ref android.R.styleable#TextView_maxHeight */ public int getMaxHeight() { return mMaxMode == PIXELS ? mMaximum : -1; } /** * Makes the TextView exactly this many lines tall. * * Note that setting this value overrides any other (minimum / maximum) number of lines or * height setting. A single line TextView will set this value to 1. * * @attr ref android.R.styleable#TextView_lines */ public void setLines(int lines) { mMaximum = mMinimum = lines; mMaxMode = mMinMode = LINES; requestLayout(); invalidate(); } /** * Makes the TextView exactly this many pixels tall. * You could do the same thing by specifying this number in the * LayoutParams. * * Note that setting this value overrides any other (minimum / maximum) number of lines or * height setting. * * @attr ref android.R.styleable#TextView_height */ public void setHeight(int pixels) { mMaximum = mMinimum = pixels; mMaxMode = mMinMode = PIXELS; requestLayout(); invalidate(); } /** * Makes the TextView at least this many ems wide * * @attr ref android.R.styleable#TextView_minEms */ public void setMinEms(int minems) { mMinWidth = minems; mMinWidthMode = EMS; requestLayout(); invalidate(); } /** * @return the minimum width of the TextView, expressed in ems or -1 if the minimum width * was set in pixels instead (using {@link #setMinWidth(int)} or {@link #setWidth(int)}). * * @see #setMinEms(int) * @see #setEms(int) * * @attr ref android.R.styleable#TextView_minEms */ public int getMinEms() { return mMinWidthMode == EMS ? mMinWidth : -1; } /** * Makes the TextView at least this many pixels wide * * @attr ref android.R.styleable#TextView_minWidth */ public void setMinWidth(int minpixels) { mMinWidth = minpixels; mMinWidthMode = PIXELS; requestLayout(); invalidate(); } /** * @return the minimum width of the TextView, in pixels or -1 if the minimum width * was set in ems instead (using {@link #setMinEms(int)} or {@link #setEms(int)}). * * @see #setMinWidth(int) * @see #setWidth(int) * * @attr ref android.R.styleable#TextView_minWidth */ public int getMinWidth() { return mMinWidthMode == PIXELS ? mMinWidth : -1; } /** * Makes the TextView at most this many ems wide * * @attr ref android.R.styleable#TextView_maxEms */ public void setMaxEms(int maxems) { mMaxWidth = maxems; mMaxWidthMode = EMS; requestLayout(); invalidate(); } /** * @return the maximum width of the TextView, expressed in ems or -1 if the maximum width * was set in pixels instead (using {@link #setMaxWidth(int)} or {@link #setWidth(int)}). * * @see #setMaxEms(int) * @see #setEms(int) * * @attr ref android.R.styleable#TextView_maxEms */ public int getMaxEms() { return mMaxWidthMode == EMS ? mMaxWidth : -1; } /** * Makes the TextView at most this many pixels wide * * @attr ref android.R.styleable#TextView_maxWidth */ public void setMaxWidth(int maxpixels) { mMaxWidth = maxpixels; mMaxWidthMode = PIXELS; requestLayout(); invalidate(); } /** * @return the maximum width of the TextView, in pixels or -1 if the maximum width * was set in ems instead (using {@link #setMaxEms(int)} or {@link #setEms(int)}). * * @see #setMaxWidth(int) * @see #setWidth(int) * * @attr ref android.R.styleable#TextView_maxWidth */ public int getMaxWidth() { return mMaxWidthMode == PIXELS ? mMaxWidth : -1; } /** * Makes the TextView exactly this many ems wide * * @see #setMaxEms(int) * @see #setMinEms(int) * @see #getMinEms() * @see #getMaxEms() * * @attr ref android.R.styleable#TextView_ems */ public void setEms(int ems) { mMaxWidth = mMinWidth = ems; mMaxWidthMode = mMinWidthMode = EMS; requestLayout(); invalidate(); } /** * Makes the TextView exactly this many pixels wide. * You could do the same thing by specifying this number in the * LayoutParams. * * @see #setMaxWidth(int) * @see #setMinWidth(int) * @see #getMinWidth() * @see #getMaxWidth() * * @attr ref android.R.styleable#TextView_width */ public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; requestLayout(); invalidate(); } /** * Sets line spacing for this TextView. Each line will have its height * multiplied by <code>mult</code> and have <code>add</code> added to it. * * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier */ public void setLineSpacing(float add, float mult) { if (mSpacingAdd != add || mSpacingMult != mult) { mSpacingAdd = add; mSpacingMult = mult; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } /** * Gets the line spacing multiplier * * @return the value by which each line's height is multiplied to get its actual height. * * @see #setLineSpacing(float, float) * @see #getLineSpacingExtra() * * @attr ref android.R.styleable#TextView_lineSpacingMultiplier */ public float getLineSpacingMultiplier() { return mSpacingMult; } /** * Gets the line spacing extra space * * @return the extra space that is added to the height of each lines of this TextView. * * @see #setLineSpacing(float, float) * @see #getLineSpacingMultiplier() * * @attr ref android.R.styleable#TextView_lineSpacingExtra */ public float getLineSpacingExtra() { return mSpacingAdd; } private void nullLayouts() { if (mLayout instanceof BoringLayout && mSavedLayout == null) { mSavedLayout = (BoringLayout) mLayout; } mSavedMarqueeModeLayout = mLayout = null; mBoring = null; } /** * Make a new Layout based on the already-measured size of the view, * on the assumption that it was measured correctly at some point. */ private void assumeLayout() { int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); if (width < 1) { width = 0; } int physicalWidth = width; if (mHorizontallyScrolling) { width = VERY_WIDE; } makeNewLayout(width, UNKNOWN_BORING, physicalWidth, false); } private int getBoxHeight(Layout l) { int padding = getExtendedPaddingTop() + getExtendedPaddingBottom(); return getMeasuredHeight() - padding; } int getVerticalOffset(boolean forceNormal) { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; Layout l = mLayout; if (gravity != Gravity.TOP) { int boxht = getBoxHeight(l); int textht = l.getHeight(); if (textht < boxht) { if (gravity == Gravity.BOTTOM) voffset = boxht - textht; else // (gravity == Gravity.CENTER_VERTICAL) voffset = (boxht - textht) >> 1; } } return voffset; } private int getBottomVerticalOffset(boolean forceNormal) { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; Layout l = mLayout; if (gravity != Gravity.BOTTOM) { int boxht = getBoxHeight(l); int textht = l.getHeight(); if (textht < boxht) { if (gravity == Gravity.TOP) voffset = boxht - textht; else // (gravity == Gravity.CENTER_VERTICAL) voffset = (boxht - textht) >> 1; } } return voffset; } private void registerForPreDraw() { if (!mPreDrawRegistered) { getViewTreeObserver().addOnPreDrawListener(this); mPreDrawRegistered = true; } } private void unregisterForPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); mPreDrawRegistered = false; mPreDrawListenerDetached = false; } /** * {@inheritDoc} */ public boolean onPreDraw() { if (mLayout == null) { assumeLayout(); } bringTextIntoView(); unregisterForPreDraw(); return true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mPreDrawListenerDetached) { getViewTreeObserver().addOnPreDrawListener(this); mPreDrawListenerDetached = false; } } /** @hide */ @Override protected void onDetachedFromWindowInternal() { if (mPreDrawRegistered) { getViewTreeObserver().removeOnPreDrawListener(this); mPreDrawListenerDetached = true; } resetResolvedDrawables(); super.onDetachedFromWindowInternal(); } @Override protected boolean isPaddingOffsetRequired() { return mShadowRadius != 0 || mDrawables != null; } @Override protected int getLeftPaddingOffset() { return getCompoundPaddingLeft() - mPaddingLeft + (int) Math.min(0, mShadowDx - mShadowRadius); } @Override protected int getTopPaddingOffset() { return (int) Math.min(0, mShadowDy - mShadowRadius); } @Override protected int getBottomPaddingOffset() { return (int) Math.max(0, mShadowDy + mShadowRadius); } @Override protected int getRightPaddingOffset() { return -(getCompoundPaddingRight() - mPaddingRight) + (int) Math.max(0, mShadowDx + mShadowRadius); } @Override protected boolean verifyDrawable(Drawable who) { final boolean verified = super.verifyDrawable(who); if (!verified && mDrawables != null) { return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop || who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom || who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd; } return verified; } @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mDrawables != null) { if (mDrawables.mDrawableLeft != null) { mDrawables.mDrawableLeft.jumpToCurrentState(); } if (mDrawables.mDrawableTop != null) { mDrawables.mDrawableTop.jumpToCurrentState(); } if (mDrawables.mDrawableRight != null) { mDrawables.mDrawableRight.jumpToCurrentState(); } if (mDrawables.mDrawableBottom != null) { mDrawables.mDrawableBottom.jumpToCurrentState(); } if (mDrawables.mDrawableStart != null) { mDrawables.mDrawableStart.jumpToCurrentState(); } if (mDrawables.mDrawableEnd != null) { mDrawables.mDrawableEnd.jumpToCurrentState(); } } } @Override public void invalidateDrawable(Drawable drawable) { boolean handled = false; if (verifyDrawable(drawable)) { final Rect dirty = drawable.getBounds(); int scrollX = mScrollX; int scrollY = mScrollY; // IMPORTANT: The coordinates below are based on the coordinates computed // for each compound drawable in onDraw(). Make sure to update each section // accordingly. final TextView.Drawables drawables = mDrawables; if (drawables != null) { if (drawable == drawables.mDrawableLeft) { final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; scrollX += mPaddingLeft; scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2; handled = true; } else if (drawable == drawables.mDrawableRight) { final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight); scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2; handled = true; } else if (drawable == drawables.mDrawableTop) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2; scrollY += mPaddingTop; handled = true; } else if (drawable == drawables.mDrawableBottom) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2; scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom); handled = true; } } if (handled) { invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); } } if (!handled) { super.invalidateDrawable(drawable); } } /** * Returns true if anything changed. */ private boolean bringTextIntoView() { Layout layout = mLayout; int line = 0; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { line = layout.getLineCount() - 1; } Layout.Alignment a = layout.getParagraphAlignment(line); int dir = layout.getParagraphDirection(line); int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); int ht = layout.getHeight(); int scrollx, scrolly; // Convert to left, center, or right alignment. if (a == Layout.Alignment.ALIGN_NORMAL) { a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT; } else if (a == Layout.Alignment.ALIGN_OPPOSITE){ a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT; } if (a == Layout.Alignment.ALIGN_CENTER) { /* * Keep centered if possible, or, if it is too wide to fit, * keep leading edge in view. */ int left = (int) Math.floor(layout.getLineLeft(line)); int right = (int) Math.ceil(layout.getLineRight(line)); if (right - left < hspace) { scrollx = (right + left) / 2 - hspace / 2; } else { if (dir < 0) { scrollx = right - hspace; } else { scrollx = left; } } } else if (a == Layout.Alignment.ALIGN_RIGHT) { int right = (int) Math.ceil(layout.getLineRight(line)); scrollx = right - hspace; } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default) scrollx = (int) Math.floor(layout.getLineLeft(line)); } if (ht < vspace) { scrolly = 0; } else { if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { scrolly = ht - vspace; } else { scrolly = 0; } } if (scrollx != mScrollX || scrolly != mScrollY) { scrollTo(scrollx, scrolly); return true; } else { return false; } } //interface @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { startStopMarquee(focused); super.onFocusChanged(focused, direction, previouslyFocusedRect); } public void setEllipsize(TextUtils.TruncateAt where) { // TruncateAt is an enum. != comparison is ok between these singleton objects. if (mEllipsize != where) { mEllipsize = where; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } /** * Sets how many times to repeat the marquee animation. Only applied if the * TextView has marquee enabled. Set to -1 to repeat indefinitely. * * @see #getMarqueeRepeatLimit() * * @attr ref android.R.styleable#TextView_marqueeRepeatLimit */ public void setMarqueeRepeatLimit(int marqueeLimit) { mMarqueeRepeatLimit = marqueeLimit; } /** * Gets the number of times the marquee animation is repeated. Only meaningful if the * TextView has marquee enabled. * * @return the number of times the marquee animation is repeated. -1 if the animation * repeats indefinitely * * @see #setMarqueeRepeatLimit(int) * * @attr ref android.R.styleable#TextView_marqueeRepeatLimit */ public int getMarqueeRepeatLimit() { return mMarqueeRepeatLimit; } /** * Sets the string value of the TextView. TextView <em>does not</em> accept * HTML-like formatting, which you can do with text strings in XML resource files. * To style your strings, attach android.text.style.* objects to a * {@link android.text.SpannableString SpannableString}, or see the * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources"> * Available Resource Types</a> documentation for an example of setting * formatted text in the XML resource file. * * @attr ref android.R.styleable#TextView_text */ public final void setText(CharSequence text) { setText(text, 0); if (mCharWrapper != null) { mCharWrapper.mChars = null; } } private void setText(CharSequence text, int oldlen) { if (text == null) { text = ""; } // if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f); if (mText != null) { oldlen = mText.length(); } mText = text; final int textLength = text.length(); if (mLayout != null) { checkForRelayout(); } onTextChanged(text, 0, oldlen, textLength); } /** * Sets the TextView to display the specified slice of the specified * char array. You must promise that you will not change the contents * of the array except for right before another call to setText(), * since the TextView has no way to know that the text * has changed and that it needs to invalidate and re-layout. */ public final void setText(char[] text, int start, int len) { int oldlen = 0; if (start < 0 || len < 0 || start + len > text.length) { throw new IndexOutOfBoundsException(start + ", " + len); } /* * We must do the before-notification here ourselves because if * the old text is a CharWrapper we destroy it before calling * into the normal path. */ if (mText != null) { oldlen = mText.length(); } else { } if (mCharWrapper == null) { mCharWrapper = new CharWrapper(text, start, len); } else { mCharWrapper.set(text, start, len); } setText(mCharWrapper, oldlen); } public final void setText(int resid) { setText(getContext().getResources().getText(resid)); } @Override protected boolean setFrame(int l, int t, int r, int b) { boolean result = super.setFrame(l, t, r, b); restartMarqueeIfNeeded(); return result; } private void restartMarqueeIfNeeded() { if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) { mRestartMarquee = false; startMarquee(); } } public void setTextColor(int textColor){ mTextPaint.setColor(textColor); requestLayout(); } /** * Set the default text size to the given value, interpreted as "scaled * pixel" units. This size is adjusted based on the current density and * user font size preference. * * @param size The scaled pixel size. * * @attr ref android.R.styleable#TextView_textSize */ public void setTextSize(float size) { setTextSize(TypedValue.COMPLEX_UNIT_SP, size); } /** * Set the default text size to a given unit and value. See {@link * TypedValue} for the possible dimension units. * * @param unit The desired dimension unit. * @param size The desired size in the given units. * * @attr ref android.R.styleable#TextView_textSize */ public void setTextSize(int unit, float size) { Context c = getContext(); Resources r; if (c == null) r = Resources.getSystem(); else r = c.getResources(); setRawTextSize(TypedValue.applyDimension( unit, size, r.getDisplayMetrics())); } private void setRawTextSize(float size) { if (size != mTextPaint.getTextSize()) { mTextPaint.setTextSize(size); if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } public float getTextSize(){ return mTextPaint.getTextSize(); } public int getTextColor(){ return mTextPaint.getColor(); } public CharSequence getText(){ return mText; } private Layout.Alignment getLayoutAlignment() { Layout.Alignment alignment; switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.START: alignment = Layout.Alignment.ALIGN_NORMAL; break; case Gravity.END: alignment = Layout.Alignment.ALIGN_OPPOSITE; break; case Gravity.LEFT: alignment = Layout.Alignment.ALIGN_LEFT; break; case Gravity.RIGHT: alignment = Layout.Alignment.ALIGN_RIGHT; break; case Gravity.CENTER_HORIZONTAL: alignment = Layout.Alignment.ALIGN_CENTER; break; default: alignment = Layout.Alignment.ALIGN_NORMAL; break; } return alignment; } /** * The width passed in is now the desired layout width, * not the full view width with padding. * {@hide} */ protected void makeNewLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, boolean bringIntoView) { stopMarquee(); // Update "old" cached values mOldMaximum = mMaximum; mOldMaxMode = mMaxMode; if (wantWidth < 0) { wantWidth = 0; } Layout.Alignment alignment = getLayoutAlignment(); final boolean testDirChange = mSingleLine && mLayout != null && (alignment == Layout.Alignment.ALIGN_NORMAL || alignment == Layout.Alignment.ALIGN_OPPOSITE); int oldDir = 0; if (testDirChange) oldDir = mLayout.getParagraphDirection(0); boolean shouldEllipsize = mEllipsize != null; final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE && mMarqueeFadeMode != MARQUEE_FADE_NORMAL; TruncateAt effectiveEllipsize = mEllipsize; if (mEllipsize == TruncateAt.MARQUEE && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) { effectiveEllipsize = TruncateAt.END_SMALL; } if (mTextDir == null) { mTextDir = getTextDirectionHeuristic(); } mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, effectiveEllipsize, effectiveEllipsize == mEllipsize); if (switchEllipsize) { TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ? TruncateAt.END : TruncateAt.MARQUEE; mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize); } if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) { registerForPreDraw(); } if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { final int height = mLayoutParams.height; // If the size of the view does not depend on the size of the text, try to // start the marquee immediately if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) { startMarquee(); } else { // Defer the start of the marquee until we know our width (see setFrame()) mRestartMarquee = true; } } } private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize, boolean useSaved) { Layout result = null; if (boring == UNKNOWN_BORING) { boring = BoringLayout.isBoring(getText(), mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } } if (boring != null) { if (boring.width <= wantWidth && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) { if (useSaved && mSavedLayout != null) { result = mSavedLayout.replaceOrMake(getText(), mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad); } else { result = BoringLayout.make(getText(), mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, mDrawDefer); } if (useSaved) { mSavedLayout = (BoringLayout) result; } } else if (shouldEllipsize && boring.width <= wantWidth) { if (useSaved && mSavedLayout != null) { result = mSavedLayout.replaceOrMake(getText(), mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth); } else { result = BoringLayout.make(getText(), mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth, mDrawDefer); } } else if (shouldEllipsize) { result = new StaticLayout(getText(), 0, getText().length(), mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, effectiveEllipsize, ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE, mDrawDefer); } else { result = new StaticLayout(getText(), mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mDrawDefer); } } else if (shouldEllipsize) { result = new StaticLayout(getText(), 0, getText().length(), mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, effectiveEllipsize, ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE, mDrawDefer); } else { result = new StaticLayout(getText(), mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mDrawDefer); } return result; } private static int desired(Layout layout) { int n = layout.getLineCount(); CharSequence text = layout.getText(); float max = 0; // if any line was wrapped, we can't use it. // but it's ok for the last line not to have a newline for (int i = 0; i < n - 1; i++) { if (text.charAt(layout.getLineEnd(i) - 1) != '\n') return -1; } for (int i = 0; i < n; i++) { max = Math.max(max, layout.getLineWidth(i)); } return (int) Math.ceil(max); } @Override public void setSelected(boolean selected) { boolean wasSelected = isSelected(); super.setSelected(selected); if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (selected) { startMarquee(); } else { stopMarquee(); } } } private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; BoringLayout.Metrics boring = UNKNOWN_BORING; if (mTextDir == null) { mTextDir = getTextDirectionHeuristic(); } int des = -1; boolean fromexisting = false; if (widthMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. width = widthSize; } else { if (mLayout != null && mEllipsize == null) { des = desired(mLayout); } if (des < 0) { boring = BoringLayout.isBoring(mText, mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } } else { fromexisting = true; } if (boring == null || boring == UNKNOWN_BORING) { if (des < 0) { des = (int) Math.ceil(Layout.getDesiredWidth(mText, mTextPaint)); } width = des; } else { width = boring.width; } final Drawables dr = mDrawables; if (dr != null) { width = Math.max(width, dr.mDrawableWidthTop); width = Math.max(width, dr.mDrawableWidthBottom); } width += getCompoundPaddingLeft() + getCompoundPaddingRight(); if (mMaxWidthMode == EMS) { width = Math.min(width, mMaxWidth * getLineHeight()); } else { width = Math.min(width, mMaxWidth); } if (mMinWidthMode == EMS) { width = Math.max(width, mMinWidth * getLineHeight()); } else { width = Math.max(width, mMinWidth); } // Check against our minimum width width = Math.max(width, getSuggestedMinimumWidth()); if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(widthSize, width); } } int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); int unpaddedWidth = want; if (mHorizontallyScrolling) want = VERY_WIDE; if (mLayout == null) { makeNewLayout(want, boring, width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } else { final boolean layoutChanged = (mLayout.getWidth() != want) || (mLayout.getEllipsizedWidth() != width - getCompoundPaddingLeft() - getCompoundPaddingRight()); final boolean widthChanged = (mEllipsize == null) && (want > mLayout.getWidth()) && (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want)); final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum); if (layoutChanged || maximumChanged) { if (!maximumChanged && widthChanged) { mLayout.increaseWidthTo(want); } else { makeNewLayout(want, boring, width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } } else { // Nothing has changed } } if (heightMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. height = heightSize; mDesiredHeightAtMeasure = -1; } else { int desired = getDesiredHeight(); height = desired; mDesiredHeightAtMeasure = desired; if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); } } int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); } /* * We didn't let makeNewLayout() register to bring the cursor into view, * so do it here if there is any possibility that it is needed. */ if (mLayout.getWidth() > unpaddedWidth || mLayout.getHeight() > unpaddedHeight) { registerForPreDraw(); } else { scrollTo(0, 0); } setMeasuredDimension(width, height); } private int getDesiredHeight() { return getDesiredHeight(mLayout, true); } private int getDesiredHeight(Layout layout, boolean cap) { if (layout == null) { return 0; } int linecount = layout.getLineCount(); int pad = getCompoundPaddingTop() + getCompoundPaddingBottom(); int desired = layout.getLineTop(linecount); final Drawables dr = mDrawables; if (dr != null) { desired = Math.max(desired, dr.mDrawableHeightLeft); desired = Math.max(desired, dr.mDrawableHeightRight); } desired += pad; if (mMaxMode == LINES) { /* * Don't cap the hint to a certain number of lines. * (Do cap it, though, if we have a maximum pixel height.) */ if (cap) { if (linecount > mMaximum) { desired = layout.getLineTop(mMaximum); if (dr != null) { desired = Math.max(desired, dr.mDrawableHeightLeft); desired = Math.max(desired, dr.mDrawableHeightRight); } desired += pad; linecount = mMaximum; } } } else { desired = Math.min(desired, mMaximum); } if (mMinMode == LINES) { if (linecount < mMinimum) { desired += getLineHeight() * (mMinimum - linecount); } } else { desired = Math.max(desired, mMinimum); } // Check against our minimum height desired = Math.max(desired, getSuggestedMinimumHeight()); return desired; } /** * Check whether a change to the existing text layout requires a * new view layout. */ private void checkForResize() { boolean sizeChanged = false; if (mLayout != null) { // Check if our width changed if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) { sizeChanged = true; invalidate(); } // Check if our height changed if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) { int desiredHeight = getDesiredHeight(); if (desiredHeight != this.getHeight()) { sizeChanged = true; } } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) { if (mDesiredHeightAtMeasure >= 0) { int desiredHeight = getDesiredHeight(); if (desiredHeight != mDesiredHeightAtMeasure) { sizeChanged = true; } } } } if (sizeChanged) { requestLayout(); // caller will have already invalidated } } /** * Check whether entirely new text requires a new view layout * or merely a new text layout. */ private void checkForRelayout() { // If we have a fixed width, we can just swap in a new text layout // if the text height stays the same or if the view height is fixed. if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) { // Static width, so try making a new text layout. int oldht = mLayout.getHeight(); int want = mLayout.getWidth(); /* * No need to bring the text into view, since the size is not * changing (unless we do the requestLayout(), in which case it * will happen at measure). */ makeNewLayout(want, UNKNOWN_BORING, mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); if (mEllipsize != TextUtils.TruncateAt.MARQUEE) { // In a fixed-height view, so use our new text layout. if (mLayoutParams.height != LayoutParams.WRAP_CONTENT && mLayoutParams.height != LayoutParams.MATCH_PARENT) { invalidate(); return; } // Dynamic height, but height has stayed the same, // so use our new text layout. if (mLayout.getHeight() == oldht) { invalidate(); return; } } // We lose: the height has changed and we have a dynamic height. // Request a new view layout using our new text layout. requestLayout(); invalidate(); } else { // Dynamic width, so we have no choice but to request a new // view layout with a new text layout. nullLayouts(); requestLayout(); invalidate(); } } private boolean canMarquee() { int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()); return width > 0 && (mLayout.getLineWidth(0) > width || (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null && mSavedMarqueeModeLayout.getLineWidth(0) > width)); } private void startMarquee() { if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && getLineCount() == 1 && canMarquee()) { if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) { mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE; final Layout tmp = mLayout; mLayout = mSavedMarqueeModeLayout; mSavedMarqueeModeLayout = tmp; requestLayout(); invalidate(); } if (mMarquee == null) mMarquee = new Marquee(this); mMarquee.start(mMarqueeRepeatLimit); } } private void stopMarquee() { if (mMarquee != null && !mMarquee.isStopped()) { mMarquee.stop(); } if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) { mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS; final Layout tmp = mSavedMarqueeModeLayout; mSavedMarqueeModeLayout = mLayout; mLayout = tmp; requestLayout(); invalidate(); } } private void startStopMarquee(boolean start) { if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (start) { startMarquee(); } else { stopMarquee(); } } } /** * This method is called when the text is changed, in case any subclasses * would like to know. * * Within <code>text</code>, the <code>lengthAfter</code> characters * beginning at <code>start</code> have just replaced old text that had * length <code>lengthBefore</code>. It is an error to attempt to make * changes to <code>text</code> from this callback. * * @param text The text the TextView is displaying * @param start The offset of the start of the range of the text that was * modified * @param lengthBefore The length of the former text that has been replaced * @param lengthAfter The length of the replacement modified text */ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { // intentionally empty, template pattern method can be overridden by subclasses } public int getLineHeight(){ return FastMath.round(mTextPaint.getFontMetricsInt(null)); } /** * Sets the typeface and style in which the text should be displayed. * Note that not all Typeface families actually have bold and italic * variants, so you may need to use * {@link #setTypeface(Typeface, int)} to get the appearance * that you actually want. * * @see #getTypeface() * */ public void setTypeface(Typeface tf) { if (mTextPaint.getTypeface() != tf) { mTextPaint.setTypeface(tf); } } /** * @return the current typeface and style in which the text is being * displayed. * * @see #setTypeface(Typeface) * */ public Typeface getTypeface(){ return mTextPaint.getTypeface(); } /** * Gives the text a shadow of the specified radius and color, the specified * distance from its normal position. * * @attr ref android.R.styleable#TextView_shadowColor * @attr ref android.R.styleable#TextView_shadowDx * @attr ref android.R.styleable#TextView_shadowDy * @attr ref android.R.styleable#TextView_shadowRadius */ public void setShadowLayer(float radius, float dx, float dy, int color) { mTextPaint.setShadowLayer(radius, dx, dy, color); mShadowRadius = radius; mShadowDx = dx; mShadowDy = dy; // Will change text clip region } int getVerticalOffset() { int voffset = 0; final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; if (gravity != Gravity.TOP) { int boxht = getHeight() - getPaddingTop() - getPaddingBottom(); int textht = getLineHeight(); if (textht < boxht) { if (gravity == Gravity.BOTTOM) voffset = boxht - textht; else // (gravity == Gravity.CENTER_VERTICAL) voffset = (boxht - textht) >> 1; } } return voffset; } /** * @hide */ public int getHorizontalOffsetForDrawables() { return 0; } @Override protected void onDraw(GLCanvas canvas) { restartMarqueeIfNeeded(); // Draw the background for this view super.onDraw(canvas); final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingRight = getCompoundPaddingRight(); final int compoundPaddingBottom = getCompoundPaddingBottom(); final int scrollX = mScrollX; final int scrollY = mScrollY; final int right = mRight; final int left = mLeft; final int bottom = mBottom; final int top = mTop; final boolean isLayoutRtl = isLayoutRtl(); final int offset = getHorizontalOffsetForDrawables(); final int leftOffset = isLayoutRtl ? 0 : offset; final int rightOffset = isLayoutRtl ? offset : 0 ; final Drawables dr = mDrawables; if (dr != null) { /* * Compound, not extended, because the icon is not clipped * if the text height is smaller. */ int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableLeft != null) { canvas.save(); canvas.translate(scrollX + mPaddingLeft + leftOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2); dr.mDrawableLeft.draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableRight != null) { canvas.save(); canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight - rightOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); dr.mDrawableRight.draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableTop != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); dr.mDrawableTop.draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableBottom != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthBottom) / 2, scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); dr.mDrawableBottom.draw(canvas); canvas.restore(); } } // int color = mCurTextColor; if (mLayout == null) { assumeLayout(); } Layout layout = mLayout; // mTextPaint.setColor(color); // mTextPaint.drawableState = getDrawableState(); boolean marquee = mEllipsize == TextUtils.TruncateAt.MARQUEE && mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS; if (marquee) { canvas.save(GLCanvas.SAVE_FLAG_ALL); } else { canvas.save(); } /* Would be faster if we didn't have to do this. Can we chop the (displayable) text so that we don't need to do this ever? */ int extendedPaddingTop = getExtendedPaddingTop(); int extendedPaddingBottom = getExtendedPaddingBottom(); if (marquee) { final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; final int maxScrollY = mLayout.getHeight() - vspace; float clipLeft = compoundPaddingLeft + scrollX; float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY; float clipRight = right - left - compoundPaddingRight + scrollX; float clipBottom = bottom - top + scrollY - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom); if (mShadowRadius != 0) { clipLeft += Math.min(0, mShadowDx - mShadowRadius); clipRight += Math.max(0, mShadowDx + mShadowRadius); clipTop += Math.min(0, mShadowDy - mShadowRadius); clipBottom += Math.max(0, mShadowDy + mShadowRadius); } canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); } int voffsetText = 0; int voffsetCursor = 0; // translate in by our padding /* shortcircuit calling getVerticaOffset() */ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffsetText = getVerticalOffset(false); voffsetCursor = getVerticalOffset(true); } canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); if (mEllipsize == TextUtils.TruncateAt.MARQUEE && mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) { if (!mSingleLine && getLineCount() == 1 && canMarquee() && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { final int width = mRight - mLeft; final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight(); final float dx = mLayout.getLineRight(0) - (width - padding); canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); } if (mMarquee != null && mMarquee.isRunning()) { final float dx = -mMarquee.getScroll(); canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); } } layout.draw(canvas); if (mMarquee != null && mMarquee.shouldDrawGhost()) { final float dx = mMarquee.getGhostOffset(); canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); // layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); layout.draw(canvas); } canvas.restore(); } /** * Return the number of lines of text, or 0 if the internal Layout has not * been built. */ public int getLineCount() { return mLayout != null ? mLayout.getLineCount() : 0; } /** * Return the baseline for the specified line (0...getLineCount() - 1) * If bounds is not null, return the top, left, right, bottom extents * of the specified line in it. If the internal Layout has not been built, * return 0 and set bounds to (0, 0, 0, 0) * @param line which line to examine (0..getLineCount() - 1) * @param bounds Optional. If not null, it returns the extent of the line * @return the Y-coordinate of the baseline */ public int getLineBounds(int line, Rect bounds) { if (mLayout == null) { if (bounds != null) { bounds.set(0, 0, 0, 0); } return 0; } else { int baseline = mLayout.getLineBounds(line, bounds); int voffset = getExtendedPaddingTop(); if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { voffset += getVerticalOffset(true); } if (bounds != null) { bounds.offset(getCompoundPaddingLeft(), voffset); } return baseline + voffset; } } /** * Sets the horizontal alignment of the text and the * vertical gravity that will be used when there is extra space * in the TextView beyond what is required for the text itself. * * @see android.view.Gravity */ public void setGravity(int gravity) { if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { gravity |= Gravity.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { gravity |= Gravity.TOP; } boolean newLayout = false; if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) { newLayout = true; } if (gravity != mGravity) { mGravity = gravity; } if (mLayout != null && newLayout) { // XXX this is heavy-handed because no actual content changes. int want = mLayout.getWidth(); makeNewLayout(want, UNKNOWN_BORING, mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), true); } } /** * Sets the properties of this field (lines, horizontally scrolling, * transformation method) to be for a single-line input. * * @attr ref android.R.styleable#TextView_singleLine */ public void setSingleLine() { setSingleLine(true); } /** * If true, sets the properties of this field (number of lines, horizontally scrolling, * transformation method) to be for a single-line input; if false, restores these to the default * conditions. * * Note that the default conditions are not necessarily those that were in effect prior this * method, and you may want to reset these properties to your custom values. * * @attr ref android.R.styleable#TextView_singleLine */ public void setSingleLine(boolean singleLine) { // Could be used, but may break backward compatibility. // if (mSingleLine == singleLine) return; applySingleLine(singleLine, true); } private void applySingleLine(boolean singleLine, boolean changeMaxLines) { mSingleLine = singleLine; if (singleLine) { setLines(1); setHorizontallyScrolling(true); } else { if (changeMaxLines) { setMaxLines(Integer.MAX_VALUE); } setHorizontallyScrolling(false); } } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); mTextDir = getTextDirectionHeuristic(); if (mLayout != null) { checkForRelayout(); } } TextDirectionHeuristic getTextDirectionHeuristic() { // Always need to resolve layout direction first final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL); return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL : TextDirectionHeuristics.FIRSTSTRONG_LTR); // Now, we can select the heuristic // switch (getTextDirection()) { // default: // case TEXT_DIRECTION_FIRST_STRONG: // return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL : // TextDirectionHeuristics.FIRSTSTRONG_LTR); // case TEXT_DIRECTION_ANY_RTL: // return TextDirectionHeuristics.ANYRTL_LTR; // case TEXT_DIRECTION_LTR: // return TextDirectionHeuristics.LTR; // case TEXT_DIRECTION_RTL: // return TextDirectionHeuristics.RTL; // } } /** * @hide */ @Override public void onResolveDrawables(int layoutDirection) { // No need to resolve twice if (mLastLayoutDirection == layoutDirection) { return; } mLastLayoutDirection = layoutDirection; // Resolve drawables if (mDrawables != null) { mDrawables.resolveWithLayoutDirection(layoutDirection); } } private static class CharWrapper implements CharSequence, GetChars { private char[] mChars; private int mStart, mLength; public CharWrapper(char[] chars, int start, int len) { mChars = chars; mStart = start; mLength = len; } /* package */ void set(char[] chars, int start, int len) { mChars = chars; mStart = start; mLength = len; } public int length() { return mLength; } public char charAt(int off) { return mChars[off + mStart]; } @Override public String toString() { return new String(mChars, mStart, mLength); } public CharSequence subSequence(int start, int end) { if (start < 0 || end < 0 || start > mLength || end > mLength) { throw new IndexOutOfBoundsException(start + ", " + end); } return new String(mChars, start + mStart, end - start); } public void getChars(int start, int end, char[] buf, int off) { if (start < 0 || end < 0 || start > mLength || end > mLength) { throw new IndexOutOfBoundsException(start + ", " + end); } System.arraycopy(mChars, start + mStart, buf, off, end - start); } } private static final class Marquee { // TODO: Add an option to configure this private static final float MARQUEE_DELTA_MAX = 0.07f; private static final int MARQUEE_DELAY = 1200; private static final int MARQUEE_RESTART_DELAY = 1200; private static final int MARQUEE_DP_PER_SECOND = 30; private static final byte MARQUEE_STOPPED = 0x0; private static final byte MARQUEE_STARTING = 0x1; private static final byte MARQUEE_RUNNING = 0x2; private final WeakReference<TextView> mView; private final Handler mHandler; private byte mStatus = MARQUEE_STOPPED; private final float mPixelsPerSecond; private float mMaxScroll; private float mMaxFadeScroll; private float mGhostStart; private float mGhostOffset; private float mFadeStop; private int mRepeatLimit; private float mScroll; private long mLastAnimationMs; Marquee(TextView v) { final float density = v.getContext().getResources().getDisplayMetrics().density; mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density; mView = new WeakReference<TextView>(v); mHandler = new Handler(Looper.getMainLooper()); } private Runnable mTickCallback = new Runnable() { @Override public void run() { tick(); } }; private Runnable mStartCallback = new Runnable() { @Override public void run() { mStatus = MARQUEE_RUNNING; mLastAnimationMs = AnimationUtils.currentAnimationTimeMillis(); tick(); } }; private Runnable mRestartCallback = new Runnable() { @Override public void run() { if (mStatus == MARQUEE_RUNNING) { if (mRepeatLimit >= 0) { mRepeatLimit--; } start(mRepeatLimit); } } }; void tick() { if (mStatus != MARQUEE_RUNNING) { return; } mHandler.removeCallbacks(mTickCallback); final TextView textView = mView.get(); if (textView != null && (textView.isFocused() || textView.isSelected())) { long currentMs = AnimationUtils.currentAnimationTimeMillis(); long deltaMs = currentMs - mLastAnimationMs; mLastAnimationMs = currentMs; float deltaPx = deltaMs / 1000f * mPixelsPerSecond; mScroll += deltaPx; if (mScroll > mMaxScroll) { mScroll = mMaxScroll; mHandler.postDelayed(mRestartCallback, MARQUEE_DELAY); } else { mHandler.post(mTickCallback); } textView.invalidate(); } } void stop() { mStatus = MARQUEE_STOPPED; mHandler.removeCallbacks(mStartCallback); mHandler.removeCallbacks(mRestartCallback); mHandler.removeCallbacks(mTickCallback); resetScroll(); } private void resetScroll() { mScroll = 0.0f; final TextView textView = mView.get(); if (textView != null) textView.invalidate(); } void start(int repeatLimit) { if (repeatLimit == 0) { stop(); return; } mRepeatLimit = repeatLimit; final TextView textView = mView.get(); if (textView != null && textView.mLayout != null) { mStatus = MARQUEE_STARTING; mScroll = 0.0f; final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight(); final float lineWidth = textView.mLayout.getLineWidth(0); final float gap = textWidth / 3.0f; mGhostStart = lineWidth - textWidth + gap; mMaxScroll = mGhostStart + textWidth; mGhostOffset = lineWidth + gap; mFadeStop = lineWidth + textWidth / 6.0f; mMaxFadeScroll = mGhostStart + lineWidth + lineWidth; textView.invalidate(); mHandler.post(mStartCallback); } } float getGhostOffset() { return mGhostOffset; } float getScroll() { return mScroll; } float getMaxFadeScroll() { return mMaxFadeScroll; } boolean shouldDrawLeftFade() { return mScroll <= mFadeStop; } boolean shouldDrawGhost() { return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart; } boolean isRunning() { return mStatus == MARQUEE_RUNNING; } boolean isStopped() { return mStatus == MARQUEE_STOPPED; } } }