/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.taobao.weex.dom; import android.graphics.Canvas; import android.graphics.Typeface; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.UnderlineSpan; import com.taobao.weex.WXEnvironment; import com.taobao.weex.common.Constants; import com.taobao.weex.dom.flex.CSSConstants; import com.taobao.weex.dom.flex.CSSNode; import com.taobao.weex.dom.flex.FloatUtil; import com.taobao.weex.dom.flex.MeasureOutput; import com.taobao.weex.ui.component.WXText; import com.taobao.weex.ui.component.WXTextDecoration; import com.taobao.weex.utils.WXDomUtils; import com.taobao.weex.utils.WXLogUtils; import com.taobao.weex.utils.WXResourceUtils; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static com.taobao.weex.dom.WXStyle.UNSET; /** * Class for calculating a given text's height and width. The calculating of width and height of * text is done by {@link Layout}. */ public class WXTextDomObject extends WXDomObject { /** * Command object for setSpan */ private static class SetSpanOperation { protected final int start, end, flag; protected final Object what; SetSpanOperation(int start, int end, Object what) { this(start, end, what, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } SetSpanOperation(int start, int end, Object what, int flag) { this.start = start; this.end = end; this.what = what; this.flag = flag; } public void execute(Spannable sb) { sb.setSpan(what, start, end, flag); } } /** * Object for calculating text's width and height. This class is an anonymous class of * implementing {@link com.taobao.weex.dom.flex.CSSNode.MeasureFunction} */ /** package **/ static final CSSNode.MeasureFunction TEXT_MEASURE_FUNCTION = new CSSNode.MeasureFunction() { @Override public void measure(CSSNode node, float width, @NonNull MeasureOutput measureOutput) { WXTextDomObject textDomObject = (WXTextDomObject) node; if (CSSConstants.isUndefined(width)) { width = node.cssstyle.maxWidth; } if(textDomObject.getTextWidth(textDomObject.mTextPaint,width,false)>0) { textDomObject.layout = textDomObject.createLayout(width, false, null); textDomObject.hasBeenMeasured = true; textDomObject.previousWidth = textDomObject.layout.getWidth(); measureOutput.height = textDomObject.layout.getHeight(); measureOutput.width = textDomObject.previousWidth; }else{ measureOutput.height = 0; measureOutput.width = 0; } } }; private static final Canvas DUMMY_CANVAS = new Canvas(); private static final String ELLIPSIS = "\u2026"; private boolean mIsColorSet = false; private boolean hasBeenMeasured = false; private int mColor; /** * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. */ private int mFontStyle = UNSET; /** * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}. */ private int mFontWeight = UNSET; private int mNumberOfLines = UNSET; private int mFontSize = UNSET; private int mLineHeight = UNSET; private float previousWidth = Float.NaN; private String mFontFamily = null; private String mText = null; private TextUtils.TruncateAt textOverflow; private Layout.Alignment mAlignment; private WXTextDecoration mTextDecoration = WXTextDecoration.NONE; private TextPaint mTextPaint = new TextPaint(); private @Nullable Spanned spanned; private @Nullable Layout layout; private AtomicReference<Layout> atomicReference = new AtomicReference<>(); /** * Create an instance of current class, and set {@link #TEXT_MEASURE_FUNCTION} as the * measureFunction * @see CSSNode#setMeasureFunction(MeasureFunction) */ public WXTextDomObject() { super(); mTextPaint.setFlags(TextPaint.ANTI_ALIAS_FLAG); setMeasureFunction(TEXT_MEASURE_FUNCTION); } public TextPaint getTextPaint() { return mTextPaint; } /** * Prepare the text {@link Spanned} for calculating text's size. This is done by setting * various text span to the text. * @see android.text.style.CharacterStyle */ @Override public void layoutBefore() { hasBeenMeasured = false; updateStyleAndText(); spanned = createSpanned(mText); super.dirty(); super.layoutBefore(); } @Override public void layoutAfter() { if (hasBeenMeasured) { if (layout != null && !FloatUtil.floatsEqual(WXDomUtils.getContentWidth(this), previousWidth)) { recalculateLayout(); } } else { updateStyleAndText(); recalculateLayout(); } hasBeenMeasured = false; if (layout != null && !layout.equals(atomicReference.get()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //TODO Warm up, a profile should be used to see the improvement. warmUpTextLayoutCache(layout); } swap(); super.layoutAfter(); } @Override public Layout getExtra() { return atomicReference.get(); } @Override public void updateAttr(Map<String, Object> attrs) { swap(); super.updateAttr(attrs); if (attrs.containsKey(Constants.Name.VALUE)) { mText = WXAttr.getValue(attrs); } } @Override public void updateStyle(Map<String, Object> styles) { swap(); super.updateStyle(styles); updateStyleImp(styles); } @Override public WXTextDomObject clone() { WXTextDomObject dom = null; try { dom = new WXTextDomObject(); copyFields(dom); dom.hasBeenMeasured = hasBeenMeasured; dom.atomicReference = atomicReference; } catch (Exception e) { if (WXEnvironment.isApkDebugable()) { WXLogUtils.e("WXTextDomObject clone error: ", e); } } if (dom != null) { dom.spanned = spanned; } return dom; } /** * RecalculateLayout. */ private void recalculateLayout() { float contentWidth = WXDomUtils.getContentWidth(this); if (contentWidth > 0) { spanned = createSpanned(mText); layout = createLayout(contentWidth, true, layout); previousWidth = layout.getWidth(); } } /** * Update style and text. */ private void updateStyleAndText() { updateStyleImp(getStyles()); mText = WXAttr.getValue(getAttrs()); } /** * Record the property according to the given style * @param style the give style. */ private void updateStyleImp(Map<String, Object> style) { if (style != null) { if (style.containsKey(Constants.Name.LINES)) { int lines = WXStyle.getLines(style); if (lines > 0) { mNumberOfLines = lines; } } if (style.containsKey(Constants.Name.FONT_SIZE)) { mFontSize = WXStyle.getFontSize(style,getViewPortWidth()); } if (style.containsKey(Constants.Name.FONT_WEIGHT)) { mFontWeight = WXStyle.getFontWeight(style); } if (style.containsKey(Constants.Name.FONT_STYLE)) { mFontStyle = WXStyle.getFontStyle(style); } if (style.containsKey(Constants.Name.COLOR)) { mColor = WXResourceUtils.getColor(WXStyle.getTextColor(style)); mIsColorSet = mColor != Integer.MIN_VALUE; } if (style.containsKey(Constants.Name.TEXT_DECORATION)) { mTextDecoration = WXStyle.getTextDecoration(style); } if (style.containsKey(Constants.Name.FONT_FAMILY)) { mFontFamily = WXStyle.getFontFamily(style); } mAlignment = WXStyle.getTextAlignment(style); textOverflow = WXStyle.getTextOverflow(style); int lineHeight = WXStyle.getLineHeight(style,getViewPortWidth()); if (lineHeight != UNSET) { mLineHeight = lineHeight; } } } /** * Update layout according to {@link #mText} and span * @param width the specified width. * @param forceWidth If true, force the text width to the specified width, otherwise, text width * may equals to or be smaller than the specified width. * @param previousLayout the result of previous layout, could be null. */ private @NonNull Layout createLayout(float width, boolean forceWidth, @Nullable Layout previousLayout) { float textWidth; textWidth = getTextWidth(mTextPaint, width, forceWidth); Layout layout; if (!FloatUtil.floatsEqual(previousWidth, textWidth) || previousLayout == null) { layout = new StaticLayout(spanned, mTextPaint, (int) Math.ceil(textWidth), Layout.Alignment.ALIGN_NORMAL, 1, 0, false); } else { layout = previousLayout; } if (mNumberOfLines != UNSET && mNumberOfLines > 0 && mNumberOfLines < layout.getLineCount()) { int lastLineStart, lastLineEnd; lastLineStart = layout.getLineStart(mNumberOfLines - 1); lastLineEnd = layout.getLineEnd(mNumberOfLines - 1); if (lastLineStart < lastLineEnd) { String text = mText.subSequence(0, lastLineStart).toString() + truncate(mText.substring(lastLineStart, lastLineEnd), mTextPaint, layout.getWidth(), textOverflow); spanned = createSpanned(text); return new StaticLayout(spanned, mTextPaint, (int) Math.ceil(textWidth), Layout.Alignment.ALIGN_NORMAL, 1, 0, false); } } return layout; } public @NonNull String truncate(@Nullable String source, @NonNull TextPaint paint, int desired, @Nullable TextUtils.TruncateAt truncateAt){ if(!TextUtils.isEmpty(source)){ StringBuilder builder; Spanned spanned; StaticLayout layout; for(int i=source.length();i>0;i--){ builder=new StringBuilder(i+1); builder.append(source, 0, i); if(truncateAt!=null){ builder.append(ELLIPSIS); } spanned = createSpanned(builder.toString()); layout = new StaticLayout(spanned, paint, desired, Layout.Alignment.ALIGN_NORMAL, 1, 0, true); if(layout.getLineCount()<=1){ return spanned.toString(); } } } return ""; } /** * Get text width according to constrain of outerWidth with and forceToDesired * @param textPaint paint used to measure text * @param outerWidth the width that css-layout desired. * @param forceToDesired if set true, the return value will be outerWidth, no matter what the width * of text is. * @return if forceToDesired is false, it will be the minimum value of the width of text and * outerWidth in case of outerWidth is defined, in other case, it will be outer width. */ /** package **/ float getTextWidth(TextPaint textPaint,float outerWidth, boolean forceToDesired) { float textWidth; if (forceToDesired) { textWidth = outerWidth; } else { float desiredWidth = Layout.getDesiredWidth(spanned, textPaint); if (CSSConstants.isUndefined(outerWidth) || desiredWidth < outerWidth) { textWidth = desiredWidth; } else { textWidth = outerWidth; } } return textWidth; } /** * Update {@link #spanned} according to the give charSequence and styles * @param text the give raw text. * @return an Spanned contains text and spans */ protected @NonNull Spanned createSpanned(String text) { if (!TextUtils.isEmpty(text)) { SpannableString spannable = new SpannableString(text); updateSpannable(spannable, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return spannable; } return new SpannableString(""); } protected void updateSpannable(Spannable spannable, int spanFlag) { List<SetSpanOperation> ops = createSetSpanOperation(spannable.length(), spanFlag); if (mFontSize == UNSET) { ops.add(new SetSpanOperation(0, spannable.length(), new AbsoluteSizeSpan(WXText.sDEFAULT_SIZE), spanFlag)); } Collections.reverse(ops); for (SetSpanOperation op : ops) { op.execute(spannable); } } /** * Create a task list which contains {@link SetSpanOperation}. The task list will be executed * in other method. * @param end the end character of the text. * @return a task list which contains {@link SetSpanOperation}. */ private List<SetSpanOperation> createSetSpanOperation(int end, int spanFlag) { List<SetSpanOperation> ops = new LinkedList<>(); int start = 0; if (end >= start) { if (mTextDecoration == WXTextDecoration.UNDERLINE) { ops.add(new SetSpanOperation(start, end, new UnderlineSpan(), spanFlag)); } if (mTextDecoration == WXTextDecoration.LINETHROUGH) { ops.add(new SetSpanOperation(start, end, new StrikethroughSpan(), spanFlag)); } if (mIsColorSet) { ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(mColor), spanFlag)); } if (mFontSize != UNSET) { ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(mFontSize), spanFlag)); } if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) { ops.add(new SetSpanOperation(start, end, new WXCustomStyleSpan(mFontStyle, mFontWeight, mFontFamily), spanFlag)); } ops.add(new SetSpanOperation(start, end, new AlignmentSpan.Standard(mAlignment), spanFlag)); if (mLineHeight != UNSET) { ops.add(new SetSpanOperation(start, end, new WXLineHeightSpan(mLineHeight), spanFlag)); } } return ops; } /** * Move the reference of current layout to the {@link AtomicReference} for further use, * then clear current layout. */ private void swap() { if (layout != null) { atomicReference.set(layout); layout = null; mTextPaint = new TextPaint(mTextPaint); } } /** * As warming up TextLayoutCache done in the DOM thread may manipulate UI operation, there may be some exception, in which case the exception is ignored. After all, this is just a warm up operation. * @return false for warm up failure, otherwise returns true. */ private boolean warmUpTextLayoutCache(Layout layout) { boolean result; try { layout.draw(DUMMY_CANVAS); result = true; } catch (Exception e) { WXLogUtils.eTag(TAG, e); result = false; } return result; } }