/** * 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; }
/** * 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; } }