private static void autofit( TextView view, TextPaint paint, float minTextSize, float maxTextSize, int maxLines, float precision) { if (maxLines <= 0 || maxLines == Integer.MAX_VALUE) { return; } int targetWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(); if (targetWidth <= 0) { return; } CharSequence text = view.getText(); TransformationMethod method = view.getTransformationMethod(); if (method != null) { text = method.getTransformation(text, view); } Context context = view.getContext(); Resources r = Resources.getSystem(); DisplayMetrics displayMetrics; float size = maxTextSize; float high = size; float low = 0; if (context != null) { r = context.getResources(); } displayMetrics = r.getDisplayMetrics(); paint.set(view.getPaint()); paint.setTextSize(size); if ((maxLines == 1 && paint.measureText(text, 0, text.length()) > targetWidth) || getLineCount(text, paint, size, targetWidth, displayMetrics) > maxLines) { size = getAutofitTextSize( text, paint, targetWidth, maxLines, low, high, precision, displayMetrics); } if (size < minTextSize) { size = minTextSize; } view.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); }
/** * Returns the ascent of the text at start. This is used for scaling emoji. * * @param pos the line-relative position * @return the ascent of the text at start */ float ascent(int pos) { if (mSpanned == null) { return mPaint.ascent(); } pos += mStart; MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class); if (spans.length == 0) { return mPaint.ascent(); } TextPaint wp = mWorkPaint; wp.set(mPaint); for (MetricAffectingSpan span : spans) { span.updateMeasureState(wp); } return wp.ascent(); }
float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, FontMetricsInt fm) { int i; float wid; TextPaint workPaint = this.mWorkPaint; workPaint.set(paint); workPaint.baselineShift = 0; ReplacementSpan replacement = null; for (MetricAffectingSpan span : spans) { if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan) span; } else { span.updateMeasureState(workPaint); } } if (replacement == null) { wid = addStyleRun(workPaint, len, fm); } else { wid = (float) replacement.getSize( workPaint, this.mText, this.mTextStart + this.mPos, (this.mTextStart + this.mPos) + len, fm); float[] w = this.mWidths; w[this.mPos] = wid; int e = this.mPos + len; for (i = this.mPos + 1; i < e; i++) { w[i] = 0.0f; } this.mPos += len; } if (fm != null) { if (workPaint.baselineShift < 0) { fm.ascent += workPaint.baselineShift; fm.top += workPaint.baselineShift; } else { fm.descent += workPaint.baselineShift; fm.bottom += workPaint.baselineShift; } } return wid; }
public void initPaint(@NonNull TextPaint base) { paint.set(base); paint.setColor(color); paint.setAlpha(intAlpha()); final Typeface typeface = base.getTypeface(); if (typeface != null && typeface.getStyle() != Typeface.NORMAL) { paint.setTypeface(Typeface.create(typeface, Typeface.NORMAL)); } // pre-calculate fixed height paint.setTextSize(Math.max(base.getTextSize() * DEF_SCALE, minTextSize)); paint.getTextBounds("|", 0, 1, bounds); fixedTextHeight = bounds.height(); // set real text size value paint.setTextSize(Math.max(base.getTextSize() * scale, minTextSize)); initPaintShadow(); invalidate(true); }
/** * Returns the advance widths for a uniform left-to-right run of text with no style changes in the * middle of the run. If any style is replacement text, the first character will isCancelled the * width of the replacement and the remaining characters will isCancelled a width of 0. * * @param paint the paint, will not be modified * @param workPaint a paint to modify; on return will reflect the original paint plus the effect * of all spans on the run * @param text the text * @param start the start of the run * @param end the limit of the run * @param widths array to receive the advance widths of the characters. Must be at least a large * as (end - start). * @param fmi FontMetrics information; can be null * @return the actual number of widths returned */ public static int getTextWidths( TextPaint paint, TextPaint workPaint, Spanned text, int start, int end, float[] widths, Paint.FontMetricsInt fmi) { MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class); ReplacementSpan replacement = null; workPaint.set(paint); for (MetricAffectingSpan span : spans) { if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan) span; } else { span.updateMeasureState(workPaint); } } if (replacement == null) { workPaint.getFontMetricsInt(fmi); workPaint.getTextWidths(text, start, end, widths); } else { int wid = replacement.getSize(workPaint, text, start, end, fmi); if (end > start) { widths[0] = wid; for (int i = start + 1; i < end; i++) { widths[i - start] = 0; } } } return end - start; }
/** * Utility function for handling a unidirectional run. The run must not contain tabs or emoji but * can contain styles. * * @param start the line-relative start of the run * @param measureLimit the offset to measure to, between start and limit inclusive * @param limit the limit of the run * @param runIsRtl true if the run is right-to-left * @param c the canvas, can be null * @param x the end of the run closest to the leading margin * @param top the top of the line * @param y the baseline * @param bottom the bottom of the line * @param fmi receives metrics information, can be null * @param needWidth true if the width is required * @return the signed width of the run based on the run direction; only valid if needWidth is true */ private float handleRun( int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth) { // Case of an empty line, make sure we update fmi according to mPaint if (start == measureLimit) { TextPaint wp = mWorkPaint; wp.set(mPaint); if (fmi != null) { /// M: new FontMetrics method for complex text support. expandMetricsFromPaint(fmi, wp, mText); } return 0f; } if (mSpanned == null) { TextPaint wp = mWorkPaint; wp.set(mPaint); final int mlimit = measureLimit; return handleText( wp, start, mlimit, start, limit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); } mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); // Shaping needs to take into account context up to metric boundaries, // but rendering needs to take into account character style boundaries. // So we iterate through metric runs to get metric bounds, // then within each metric run iterate through character style runs // for the run bounds. final float originalX = x; for (int i = start, inext; i < measureLimit; i = inext) { TextPaint wp = mWorkPaint; wp.set(mPaint); inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - mStart; int mlimit = Math.min(inext, measureLimit); ReplacementSpan replacement = null; for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT // empty by construction. This special case in getSpans() explains the >= & <= tests if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan) span; } else { // We might have a replacement that uses the draw // state, otherwise measure state would suffice. span.updateDrawState(wp); } } if (replacement != null) { x += handleReplacement( replacement, wp, i, mlimit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); continue; } for (int j = i, jnext; j < mlimit; j = jnext) { jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) - mStart; wp.set(mPaint); for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { // Intentionally using >= and <= as explained above if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) || (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; CharacterStyle span = mCharacterStyleSpanSet.spans[k]; span.updateDrawState(wp); } x += handleText( wp, j, jnext, i, inext, runIsRtl, c, x, top, y, bottom, fmi, needWidth || jnext < measureLimit); } } return x - originalX; }
/** * Returns the next valid offset within this directional run, skipping conjuncts and zero-width * characters. This should not be called to walk off the end of the line, since the returned * values might not be valid on neighboring lines. If the returned offset is less than zero or * greater than the line length, the offset should be recomputed on the preceding or following * line, respectively. * * @param runIndex the run index * @param runStart the start of the run * @param runLimit the limit of the run * @param runIsRtl true if the run is right-to-left * @param offset the offset * @param after true if the new offset should logically follow the provided offset * @return the new offset */ private int getOffsetBeforeAfter( int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after) { if (runIndex < 0 || offset == (after ? mLen : 0)) { // Walking off end of line. Since we don't know // what cursor positions are available on other lines, we can't // return accurate values. These are a guess. if (after) { return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; } return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; } TextPaint wp = mWorkPaint; wp.set(mPaint); int spanStart = runStart; int spanLimit; if (mSpanned == null) { spanLimit = runLimit; } else { int target = after ? offset + 1 : offset; int limit = mStart + runLimit; while (true) { spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, MetricAffectingSpan.class) - mStart; if (spanLimit >= target) { break; } spanStart = spanLimit; } MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, mStart + spanLimit, MetricAffectingSpan.class); spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); if (spans.length > 0) { ReplacementSpan replacement = null; for (int j = 0; j < spans.length; j++) { MetricAffectingSpan span = spans[j]; if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan) span; } else { span.updateMeasureState(wp); } } if (replacement != null) { // If we have a replacement span, we're moving either to // the start or end of this span. return after ? spanLimit : spanStart; } } } int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR; int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; if (mCharsValid) { return wp.getTextRunCursor( mChars, spanStart, spanLimit - spanStart, flags, offset, cursorOpt); } else { return wp.getTextRunCursor( mText, mStart + spanStart, mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart; } }
private void icUpdateGecko(boolean force) { // Skip if receiving a repeated request, or // if suppressing compositions during text selection. if ((!force && mIcUpdateSeqno == mLastIcUpdateSeqno) || mSuppressCompositions) { if (DEBUG) { Log.d(LOGTAG, "icUpdateGecko() skipped"); } return; } mLastIcUpdateSeqno = mIcUpdateSeqno; mActionQueue.syncWithGecko(); if (DEBUG) { Log.d(LOGTAG, "icUpdateGecko()"); } final int selStart = mText.getSpanStart(Selection.SELECTION_START); final int selEnd = mText.getSpanEnd(Selection.SELECTION_END); int composingStart = mText.length(); int composingEnd = 0; Object[] spans = mText.getSpans(0, composingStart, Object.class); for (Object span : spans) { if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { composingStart = Math.min(composingStart, mText.getSpanStart(span)); composingEnd = Math.max(composingEnd, mText.getSpanEnd(span)); } } if (DEBUG) { Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd); Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd); } if (composingStart >= composingEnd) { if (selStart >= 0 && selEnd >= 0) { onImeSetSelection(selStart, selEnd); } else { onImeRemoveComposition(); } return; } if (selEnd >= composingStart && selEnd <= composingEnd) { onImeAddCompositionRange( selEnd - composingStart, selEnd - composingStart, IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0); } int rangeStart = composingStart; TextPaint tp = new TextPaint(); TextPaint emptyTp = new TextPaint(); // set initial foreground color to 0, because we check for tp.getColor() == 0 // below to decide whether to pass a foreground color to Gecko emptyTp.setColor(0); do { int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE; boolean rangeBoldLine = false; int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0; int rangeEnd = mText.nextSpanTransition(rangeStart, composingEnd, Object.class); if (selStart > rangeStart && selStart < rangeEnd) { rangeEnd = selStart; } else if (selEnd > rangeStart && selEnd < rangeEnd) { rangeEnd = selEnd; } CharacterStyle[] styleSpans = mText.getSpans(rangeStart, rangeEnd, CharacterStyle.class); if (DEBUG) { Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + rangeStart + "-" + rangeEnd); } if (styleSpans.length == 0) { rangeType = (selStart == rangeStart && selEnd == rangeEnd) ? IME_RANGE_SELECTEDRAWTEXT : IME_RANGE_RAWINPUT; } else { rangeType = (selStart == rangeStart && selEnd == rangeEnd) ? IME_RANGE_SELECTEDCONVERTEDTEXT : IME_RANGE_CONVERTEDTEXT; tp.set(emptyTp); for (CharacterStyle span : styleSpans) { span.updateDrawState(tp); } int tpUnderlineColor = 0; float tpUnderlineThickness = 0.0f; // These TextPaint fields only exist on Android ICS+ and are not in the SDK. if (Versions.feature14Plus) { tpUnderlineColor = (Integer) getField(tp, "underlineColor", 0); tpUnderlineThickness = (Float) getField(tp, "underlineThickness", 0.0f); } if (tpUnderlineColor != 0) { rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR; rangeLineColor = tpUnderlineColor; // Approximately translate underline thickness to what Gecko understands if (tpUnderlineThickness <= 0.5f) { rangeLineStyle = IME_RANGE_LINE_DOTTED; } else { rangeLineStyle = IME_RANGE_LINE_SOLID; if (tpUnderlineThickness >= 2.0f) { rangeBoldLine = true; } } } else if (tp.isUnderlineText()) { rangeStyles |= IME_RANGE_UNDERLINE; rangeLineStyle = IME_RANGE_LINE_SOLID; } if (tp.getColor() != 0) { rangeStyles |= IME_RANGE_FORECOLOR; rangeForeColor = tp.getColor(); } if (tp.bgColor != 0) { rangeStyles |= IME_RANGE_BACKCOLOR; rangeBackColor = tp.bgColor; } } onImeAddCompositionRange( rangeStart - composingStart, rangeEnd - composingStart, rangeType, rangeStyles, rangeLineStyle, rangeBoldLine, rangeForeColor, rangeBackColor, rangeLineColor); rangeStart = rangeEnd; if (DEBUG) { Log.d( LOGTAG, " added " + rangeType + " : " + Integer.toHexString(rangeStyles) + " : " + Integer.toHexString(rangeForeColor) + " : " + Integer.toHexString(rangeBackColor)); } } while (rangeStart < composingEnd); onImeUpdateComposition(composingStart, composingEnd); }
private void init(@Nullable AttributeSet attrs) { final TextView view = new TextView(getContext(), attrs); baseTextPaint.set(view.getPaint()); textView.init(this, attrs, baseTextPaint); }
/** * Draws and/or measures a uniform run of text on a single line. No span of interest should start * or end in the middle of this run (if not drawing, character spans that don't affect metrics can * be ignored). Neither should the run direction change in the middle of the run. * * <p> * * <p>The x position is the leading edge of the text. In a right-to-left paragraph, this will be * to the right of the text to be drawn. Paint should not have an Align value other than LEFT or * positioning will isCancelled confused. * * <p> * * <p>On return, workPaint will reflect the original paint plus any modifications made by * character styles on the run. * * <p> * * <p>The returned width is signed and will be < 0 if the paragraph direction is right-to-left. */ private static float drawUniformRun( Canvas canvas, Spanned text, int start, int end, int dir, boolean runIsRtl, float x, int top, int y, int bottom, Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, boolean needWidth) { boolean haveWidth = false; float ret = 0; CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); ReplacementSpan replacement = null; // XXX: This shouldn't be modifying paint, only workPaint. // However, the members belonging to TextPaint should have default // values anyway. Better to ensure this in the Layout constructor. paint.bgColor = 0; paint.baselineShift = 0; workPaint.set(paint); if (spans.length > 0) { for (CharacterStyle span : spans) { if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan) span; } else { span.updateDrawState(workPaint); } } } if (replacement == null) { CharSequence tmp; int tmpstart, tmpend; if (runIsRtl) { tmp = TextUtils.getReverse(text, start, end); tmpstart = 0; // XXX: assumes getReverse doesn't change the length of the text tmpend = end - start; } else { tmp = text; tmpstart = start; tmpend = end; } if (fmi != null) { workPaint.getFontMetricsInt(fmi); } if (canvas != null) { if (workPaint.bgColor != 0) { int c = workPaint.getColor(); Paint.Style s = workPaint.getStyle(); workPaint.setColor(workPaint.bgColor); workPaint.setStyle(Paint.Style.FILL); if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } if (dir == Layout.DIR_RIGHT_TO_LEFT) { canvas.drawRect(x - ret, top, x, bottom, workPaint); } else { canvas.drawRect(x, top, x + ret, bottom, workPaint); } workPaint.setStyle(s); workPaint.setColor(c); } if (dir == Layout.DIR_RIGHT_TO_LEFT) { if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } canvas.drawText(tmp, tmpstart, tmpend, x - ret, y + workPaint.baselineShift, workPaint); } else { if (needWidth) { if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } } canvas.drawText(tmp, tmpstart, tmpend, x, y + workPaint.baselineShift, workPaint); } } else { if (needWidth && !haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } } } else { ret = replacement.getSize(workPaint, text, start, end, fmi); if (canvas != null) { if (dir == Layout.DIR_RIGHT_TO_LEFT) { replacement.draw(canvas, text, start, end, x - ret, top, y, bottom, workPaint); } else { replacement.draw(canvas, text, start, end, x, top, y, bottom, workPaint); } } } if (dir == Layout.DIR_RIGHT_TO_LEFT) { return -ret; } else { return ret; } }