blob: e9a5c798822d018ddf07d89427f4522ad81e4881 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.taobao.weex.dom;
import static com.taobao.weex.dom.WXStyle.UNSET;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
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.util.Log;
import com.taobao.weex.WXEnvironment;
import com.taobao.weex.common.Constants;
import com.taobao.weex.common.WXThread;
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;
/**
* 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;
}
boolean forceWidth = false;
if(width > 0){
if(node.getParent() != null && textDomObject.mAlignment == Layout.Alignment.ALIGN_CENTER){
forceWidth = FloatUtil.floatsEqual(width, node.getParent().getLayoutWidth());
}
}
textDomObject.hasBeenMeasured = true;
width = textDomObject.getTextWidth(textDomObject.mTextPaint,width, forceWidth);
if(width > 0 && textDomObject.mText != null) {
textDomObject.layout = textDomObject.createLayout(width, true, null);
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);
if(hasNewLayout()){
if(WXEnvironment.isApkDebugable()) {
WXLogUtils.d("Previous csslayout was ignored! markLayoutSeen() never called");
}
markUpdateSeen();
}
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) {
if(Thread.currentThread() != Looper.getMainLooper().getThread()){
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() {
if(isCloneThis()){
return this;
}
WXTextDomObject dom = null;
try {
dom = new WXTextDomObject();
copyFields(dom);
dom.hasBeenMeasured = hasBeenMeasured;
dom.atomicReference = new AtomicReference<>(atomicReference.get());
} 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);
if(mText != null){
layout = createLayout(contentWidth, true, layout);
previousWidth = layout.getWidth();
}else{
previousWidth = 0;
}
}
}
/**
* 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);
mNumberOfLines = lines > 0 ? lines : UNSET;
}
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) {
SpannableStringBuilder builder = new SpannableStringBuilder(spanned.subSequence(0, lastLineStart));
Editable lastLine = new SpannableStringBuilder(spanned.subSequence(lastLineStart, lastLineEnd));
builder.append(truncate(lastLine, mTextPaint, layout.getWidth(), textOverflow));
adjustSpansRange(spanned, builder);
spanned = builder;
return new StaticLayout(spanned, mTextPaint, (int) Math.ceil(textWidth),
Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
}
}
return layout;
}
/**
* Truncate the source span to the specified lines.
* Caller of this method must ensure that the lines of text is <strong>greater than desired lines and need truncate</strong>.
* Otherwise, unexpected behavior may happen.
* @param source The source span.
* @param paint the textPaint
* @param desired specified lines.
* @param truncateAt truncate method, null value means clipping overflow text directly, non-null value means using ellipsis strategy to clip
* @return The spans after clipped.
*/
private
@NonNull
Spanned truncate(@Nullable Editable source, @NonNull TextPaint paint,
int desired, @Nullable TextUtils.TruncateAt truncateAt) {
Spanned ret = new SpannedString("");
if (!TextUtils.isEmpty(source) && source.length() > 0) {
if (truncateAt != null) {
source.append(ELLIPSIS);
}
StaticLayout layout;
int startOffset;
while (source.length() > 1) {
startOffset = source.length() -1;
if (truncateAt != null) {
startOffset -= 1;
}
source.delete(startOffset, startOffset+1);
layout = new StaticLayout(source, paint, desired, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
if (layout.getLineCount() <= 1) {
ret = source;
break;
}
}
}
return ret;
}
/**
* Adjust span range after truncate due to the wrong span range during span copy and slicing.
* @param beforeTruncate The span before truncate
* @param afterTruncate The span after truncate
*/
private void adjustSpansRange(@NonNull Spanned beforeTruncate, @NonNull Spannable afterTruncate){
Object[] spans = beforeTruncate.getSpans(0, beforeTruncate.length(), Object.class);
for(Object span:spans){
int start = beforeTruncate.getSpanStart(span);
int end = beforeTruncate.getSpanEnd(span);
if(start == 0 && end == beforeTruncate.length()){
afterTruncate.removeSpan(span);
afterTruncate.setSpan(span, 0, afterTruncate.length(), beforeTruncate.getSpanFlags(span));
}
}
}
/**
* 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.
*/
float getTextWidth(TextPaint textPaint,float outerWidth, boolean forceToDesired) {
if(mText == null){
if(forceToDesired){
return outerWidth;
}
return 0;
}
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 || mTextDecoration == WXTextDecoration.LINETHROUGH) {
ops.add(new SetSpanOperation(start, end, new TextDecorationSpan(mTextDecoration), 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);
}
hasBeenMeasured = false;
}
/**
* 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;
}
}