| /** |
| * 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.layout.measurefunc; |
| |
| import android.graphics.Canvas; |
| 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.support.annotation.RestrictTo; |
| import android.support.annotation.RestrictTo.Scope; |
| import android.support.annotation.WorkerThread; |
| |
| import com.taobao.weex.WXSDKManager; |
| import com.taobao.weex.common.Constants; |
| import com.taobao.weex.dom.TextDecorationSpan; |
| import com.taobao.weex.dom.WXAttr; |
| import com.taobao.weex.dom.WXCustomStyleSpan; |
| import com.taobao.weex.dom.WXLineHeightSpan; |
| import com.taobao.weex.dom.WXStyle; |
| import com.taobao.weex.layout.ContentBoxMeasurement; |
| import com.taobao.weex.layout.MeasureMode; |
| import com.taobao.weex.layout.MeasureSize; |
| import com.taobao.weex.ui.component.WXComponent; |
| 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.lang.ref.WeakReference; |
| 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; |
| import static com.taobao.weex.utils.WXUtils.isUndefined; |
| |
| /** |
| * Created by miomin on 2018/3/9. |
| */ |
| |
| public class TextContentBoxMeasurement extends ContentBoxMeasurement { |
| |
| private static final Canvas DUMMY_CANVAS = new Canvas(); |
| |
| public TextContentBoxMeasurement(WXComponent component) { |
| super(component); |
| } |
| |
| 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); |
| } |
| } |
| |
| private static final String ELLIPSIS = "\u2026"; |
| private boolean mIsColorSet = false; |
| private boolean hasBeenMeasured = false; |
| private int mColor; |
| private int mFontStyle = UNSET; |
| 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; |
| private @Nullable |
| Spanned spanned; |
| private @Nullable |
| Layout layout; |
| private AtomicReference<Layout> atomicReference = new AtomicReference<>(); |
| |
| /** |
| * uiThread = false |
| **/ |
| @Override |
| public void layoutBefore() { |
| mTextPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); |
| hasBeenMeasured = false; |
| updateStyleAndText(); |
| spanned = createSpanned(mText); |
| } |
| |
| /** |
| * uiThread = false |
| **/ |
| @Override |
| public void measureInternal(float width, float height, int widthMeasureMode, int heightMeasureMode) { |
| float measureWidth = width, measureHeight = height; |
| hasBeenMeasured = true; |
| float textWidth = getTextWidth(mTextPaint, width, widthMeasureMode == MeasureMode.EXACTLY); |
| |
| if (textWidth > 0 && spanned != null) { |
| layout = createLayout(textWidth,null); |
| previousWidth = layout.getWidth(); |
| if (Float.isNaN(width)) { |
| measureWidth = layout.getWidth(); |
| } else { |
| measureWidth = Math.min(layout.getWidth(), measureWidth); |
| } |
| |
| if (Float.isNaN(height)) { |
| measureHeight = layout.getHeight(); |
| } |
| } else { |
| if (widthMeasureMode == MeasureMode.UNSPECIFIED) { |
| measureWidth = 0; |
| } |
| if (heightMeasureMode == MeasureMode.UNSPECIFIED) { |
| measureHeight = 0; |
| } |
| } |
| mMeasureWidth = measureWidth; |
| mMeasureHeight = measureHeight; |
| } |
| |
| /** |
| * uiThread = false |
| **/ |
| @Override |
| public void layoutAfter(float computedWidth, float computedHeight) { |
| if(mComponent!=null) { |
| if (hasBeenMeasured) { |
| if (layout != null && |
| WXDomUtils |
| .getContentWidth(mComponent.getPadding(), mComponent.getBorder(), computedWidth) |
| != previousWidth) { |
| recalculateLayout(computedWidth); |
| } |
| } else { |
| updateStyleAndText(); |
| recalculateLayout(computedWidth); |
| } |
| 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(); |
| WXSDKManager.getInstance().getWXRenderManager().postOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| if(mComponent!=null) { |
| mComponent.updateExtra(atomicReference.get()); |
| } |
| } |
| }, mComponent.getInstanceId()); |
| } |
| } |
| |
| private void updateStyleAndText() { |
| updateStyleImp(mComponent.getStyles()); |
| mText = WXAttr.getValue(mComponent.getAttrs()); |
| } |
| |
| /** |
| * Force relayout the text, the text must layout before invoke this method. |
| * |
| * Internal method, do not invoke unless you what what you are doing |
| * @param isRTL |
| */ |
| @RestrictTo(Scope.LIBRARY) |
| @WorkerThread |
| public void forceRelayout(){ |
| //Generate Spans |
| layoutBefore(); |
| |
| //Measure |
| measure(previousWidth, Float.NaN, MeasureMode.EXACTLY, MeasureMode.UNSPECIFIED); |
| |
| //Swap text layout to UI Thread |
| layoutAfter(previousWidth, Float.NaN); |
| } |
| |
| /** |
| * 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, mComponent.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, mComponent.isLayoutRTL()); |
| textOverflow = WXStyle.getTextOverflow(style); |
| int lineHeight = WXStyle.getLineHeight(style, mComponent.getViewPortWidth()); |
| if (lineHeight != UNSET) { |
| mLineHeight = lineHeight; |
| } |
| } |
| } |
| |
| /** |
| * 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) { |
| int end = spannable.length(); |
| if (mFontSize == UNSET) { |
| mTextPaint.setTextSize(WXText.sDEFAULT_SIZE); |
| } |
| else{ |
| mTextPaint.setTextSize(mFontSize); |
| } |
| |
| if (mLineHeight != UNSET) { |
| setSpan(spannable, new WXLineHeightSpan(mLineHeight), 0, end, spanFlag); |
| } |
| |
| setSpan(spannable, new AlignmentSpan.Standard(mAlignment), 0, end, spanFlag); |
| |
| if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) { |
| setSpan(spannable, new WXCustomStyleSpan(mFontStyle, mFontWeight, mFontFamily), 0, end, spanFlag); |
| } |
| |
| if (mIsColorSet) { |
| mTextPaint.setColor(mColor); |
| } |
| |
| if (mTextDecoration == WXTextDecoration.UNDERLINE || mTextDecoration == WXTextDecoration.LINETHROUGH) { |
| setSpan(spannable, new TextDecorationSpan(mTextDecoration), 0, end, spanFlag); |
| } |
| } |
| |
| private void setSpan(Spannable spannable, Object what, int start, int end, int spanFlag){ |
| spannable.setSpan(what, start, end, spanFlag); |
| } |
| |
| /** |
| * 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. |
| */ |
| private float getTextWidth(TextPaint textPaint, float outerWidth, boolean forceToDesired) { |
| if (mText == null) { |
| if (forceToDesired) { |
| return outerWidth; |
| } |
| return 0; |
| } |
| else { |
| float textWidth; |
| if (forceToDesired) { |
| textWidth = outerWidth; |
| } else { |
| float desiredWidth = Layout.getDesiredWidth(spanned, textPaint); |
| if (isUndefined(outerWidth) || desiredWidth < outerWidth) { |
| textWidth = desiredWidth; |
| } else { |
| textWidth = outerWidth; |
| } |
| } |
| return textWidth; |
| } |
| } |
| |
| /** |
| * 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(final float textWidth, @Nullable Layout previousLayout) { |
| Layout layout; |
| if (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 = null; |
| if (lastLineStart > 0) { |
| builder = new SpannableStringBuilder(spanned.subSequence(0, lastLineStart)); |
| } else { |
| builder = new SpannableStringBuilder(); |
| } |
| Editable lastLine = new SpannableStringBuilder(spanned.subSequence(lastLineStart, lastLineEnd)); |
| builder.append(truncate(lastLine, mTextPaint, (int) Math.ceil(textWidth), 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); |
| Object[] spans = source.getSpans(0, source.length(), Object.class); |
| for (Object span : spans) { |
| int start = source.getSpanStart(span); |
| int end = source.getSpanEnd(span); |
| if (start == 0 && end == source.length() - 1) { |
| source.removeSpan(span); |
| source.setSpan(span, 0, source.length(), source.getSpanFlags(span)); |
| } |
| } |
| } |
| |
| 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, false); |
| 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)); |
| } |
| } |
| } |
| |
| private void recalculateLayout(float computedWidth) { |
| float contentWidth = WXDomUtils.getContentWidth(mComponent.getPadding(), mComponent.getBorder(), computedWidth); |
| if (contentWidth > 0) { |
| spanned = createSpanned(mText); |
| if (spanned != null) { |
| layout = createLayout(contentWidth, layout); |
| previousWidth = layout.getWidth(); |
| } else { |
| previousWidth = 0; |
| } |
| } |
| } |
| |
| /** |
| * 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("TextWarmUp", e); |
| result = false; |
| } |
| return result; |
| } |
| |
| /** |
| * 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; |
| } |
| hasBeenMeasured = false; |
| } |
| } |