| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.text; |
| |
| import android.graphics.Paint; |
| import android.text.style.MetricAffectingSpan; |
| import android.text.style.ReplacementSpan; |
| import android.util.Log; |
| |
| import com.android.internal.util.ArrayUtils; |
| |
| /** |
| * @hide |
| */ |
| class MeasuredText { |
| private static final boolean localLOGV = false; |
| CharSequence mText; |
| int mTextStart; |
| float[] mWidths; |
| char[] mChars; |
| byte[] mLevels; |
| int mDir; |
| boolean mEasy; |
| int mLen; |
| |
| private int mPos; |
| private TextPaint mWorkPaint; |
| private StaticLayout.Builder mBuilder; |
| |
| private MeasuredText() { |
| mWorkPaint = new TextPaint(); |
| } |
| |
| private static final Object[] sLock = new Object[0]; |
| private static final MeasuredText[] sCached = new MeasuredText[3]; |
| |
| static MeasuredText obtain() { |
| MeasuredText mt; |
| synchronized (sLock) { |
| for (int i = sCached.length; --i >= 0;) { |
| if (sCached[i] != null) { |
| mt = sCached[i]; |
| sCached[i] = null; |
| return mt; |
| } |
| } |
| } |
| mt = new MeasuredText(); |
| if (localLOGV) { |
| Log.v("MEAS", "new: " + mt); |
| } |
| return mt; |
| } |
| |
| static MeasuredText recycle(MeasuredText mt) { |
| mt.finish(); |
| synchronized(sLock) { |
| for (int i = 0; i < sCached.length; ++i) { |
| if (sCached[i] == null) { |
| sCached[i] = mt; |
| mt.mText = null; |
| break; |
| } |
| } |
| } |
| return null; |
| } |
| |
| void finish() { |
| mText = null; |
| mBuilder = null; |
| if (mLen > 1000) { |
| mWidths = null; |
| mChars = null; |
| mLevels = null; |
| } |
| } |
| |
| void setPos(int pos) { |
| mPos = pos - mTextStart; |
| } |
| |
| /** |
| * Analyzes text for bidirectional runs. Allocates working buffers. |
| */ |
| void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir, |
| StaticLayout.Builder builder) { |
| mBuilder = builder; |
| mText = text; |
| mTextStart = start; |
| |
| int len = end - start; |
| mLen = len; |
| mPos = 0; |
| |
| if (mWidths == null || mWidths.length < len) { |
| mWidths = ArrayUtils.newUnpaddedFloatArray(len); |
| } |
| if (mChars == null || mChars.length < len) { |
| mChars = ArrayUtils.newUnpaddedCharArray(len); |
| } |
| TextUtils.getChars(text, start, end, mChars, 0); |
| |
| if (text instanceof Spanned) { |
| Spanned spanned = (Spanned) text; |
| ReplacementSpan[] spans = spanned.getSpans(start, end, |
| ReplacementSpan.class); |
| |
| for (int i = 0; i < spans.length; i++) { |
| int startInPara = spanned.getSpanStart(spans[i]) - start; |
| int endInPara = spanned.getSpanEnd(spans[i]) - start; |
| // The span interval may be larger and must be restricted to [start, end[ |
| if (startInPara < 0) startInPara = 0; |
| if (endInPara > len) endInPara = len; |
| for (int j = startInPara; j < endInPara; j++) { |
| mChars[j] = '\uFFFC'; // object replacement character |
| } |
| } |
| } |
| |
| if ((textDir == TextDirectionHeuristics.LTR || |
| textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR || |
| textDir == TextDirectionHeuristics.ANYRTL_LTR) && |
| TextUtils.doesNotNeedBidi(mChars, 0, len)) { |
| mDir = Layout.DIR_LEFT_TO_RIGHT; |
| mEasy = true; |
| } else { |
| if (mLevels == null || mLevels.length < len) { |
| mLevels = ArrayUtils.newUnpaddedByteArray(len); |
| } |
| int bidiRequest; |
| if (textDir == TextDirectionHeuristics.LTR) { |
| bidiRequest = Layout.DIR_REQUEST_LTR; |
| } else if (textDir == TextDirectionHeuristics.RTL) { |
| bidiRequest = Layout.DIR_REQUEST_RTL; |
| } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) { |
| bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR; |
| } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { |
| bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; |
| } else { |
| boolean isRtl = textDir.isRtl(mChars, 0, len); |
| bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; |
| } |
| mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); |
| mEasy = false; |
| } |
| } |
| |
| float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { |
| if (fm != null) { |
| paint.getFontMetricsInt(fm); |
| } |
| |
| int p = mPos; |
| mPos = p + len; |
| |
| // try to do widths measurement in native code, but use Java if paint has been subclassed |
| // FIXME: may want to eliminate special case for subclass |
| float[] widths = null; |
| if (mBuilder == null || paint.getClass() != TextPaint.class) { |
| widths = mWidths; |
| } |
| if (mEasy) { |
| boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; |
| float width = 0; |
| if (widths != null) { |
| width = paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, widths, p); |
| if (mBuilder != null) { |
| mBuilder.addMeasuredRun(p, p + len, widths); |
| } |
| } else { |
| width = mBuilder.addStyleRun(paint, p, p + len, isRtl); |
| } |
| return width; |
| } |
| |
| float totalAdvance = 0; |
| int level = mLevels[p]; |
| for (int q = p, i = p + 1, e = p + len;; ++i) { |
| if (i == e || mLevels[i] != level) { |
| boolean isRtl = (level & 0x1) != 0; |
| if (widths != null) { |
| totalAdvance += |
| paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, widths, q); |
| if (mBuilder != null) { |
| mBuilder.addMeasuredRun(q, i, widths); |
| } |
| } else { |
| totalAdvance += mBuilder.addStyleRun(paint, q, i, isRtl); |
| } |
| if (i == e) { |
| break; |
| } |
| q = i; |
| level = mLevels[i]; |
| } |
| } |
| return totalAdvance; |
| } |
| |
| float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, |
| Paint.FontMetricsInt fm) { |
| |
| TextPaint workPaint = mWorkPaint; |
| workPaint.set(paint); |
| // XXX paint should not have a baseline shift, but... |
| workPaint.baselineShift = 0; |
| |
| ReplacementSpan replacement = null; |
| for (int i = 0; i < spans.length; i++) { |
| MetricAffectingSpan span = spans[i]; |
| if (span instanceof ReplacementSpan) { |
| replacement = (ReplacementSpan)span; |
| } else { |
| span.updateMeasureState(workPaint); |
| } |
| } |
| |
| float wid; |
| if (replacement == null) { |
| wid = addStyleRun(workPaint, len, fm); |
| } else { |
| // Use original text. Shouldn't matter. |
| wid = replacement.getSize(workPaint, mText, mTextStart + mPos, |
| mTextStart + mPos + len, fm); |
| if (mBuilder == null) { |
| float[] w = mWidths; |
| w[mPos] = wid; |
| for (int i = mPos + 1, e = mPos + len; i < e; i++) |
| w[i] = 0; |
| } else { |
| mBuilder.addReplacementRun(mPos, mPos + len, wid); |
| } |
| 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; |
| } |
| |
| int breakText(int limit, boolean forwards, float width) { |
| float[] w = mWidths; |
| if (forwards) { |
| int i = 0; |
| while (i < limit) { |
| width -= w[i]; |
| if (width < 0.0f) break; |
| i++; |
| } |
| while (i > 0 && mChars[i - 1] == ' ') i--; |
| return i; |
| } else { |
| int i = limit - 1; |
| while (i >= 0) { |
| width -= w[i]; |
| if (width < 0.0f) break; |
| i--; |
| } |
| while (i < limit - 1 && mChars[i + 1] == ' ') i++; |
| return limit - i - 1; |
| } |
| } |
| |
| float measure(int start, int limit) { |
| float width = 0; |
| float[] w = mWidths; |
| for (int i = start; i < limit; ++i) { |
| width += w[i]; |
| } |
| return width; |
| } |
| } |