Java tutorial
/* * 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 android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; 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.support.v4.content.LocalBroadcastManager; 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 com.taobao.weex.WXEnvironment; import com.taobao.weex.WXSDKManager; 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.StaticLayoutProxy; import com.taobao.weex.utils.TypefaceUtil; 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; } 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 = WXEnvironment.getGlobalFontFamilyName(); 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<>(); private BroadcastReceiver mTypefaceObserver; /** * 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); registerTypefaceObserverIfNeed(WXStyle.getFontFamily(getStyles())); } 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; } registerTypefaceObserverIfNeed(mFontFamily); } } /** * 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) { boolean forceRtl = false; Object direction = getStyles().get(Constants.Name.DIRECTION); if (direction != null && "text".equals(mType)) { forceRtl = direction.equals(Constants.Name.RTL); } layout = StaticLayoutProxy.create(spanned, mTextPaint, (int) Math.ceil(textWidth), Layout.Alignment.ALIGN_NORMAL, 1, 0, false, forceRtl); } 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)); } } } /** * 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; } @Override public void destroy() { if (WXEnvironment.getApplication() != null && mTypefaceObserver != null) { WXLogUtils.d("WXText", "Unregister the typeface observer"); LocalBroadcastManager.getInstance(WXEnvironment.getApplication()).unregisterReceiver(mTypefaceObserver); mTypefaceObserver = null; } super.destroy(); } private void registerTypefaceObserverIfNeed(String desiredFontFamily) { if (TextUtils.isEmpty(desiredFontFamily)) { return; } if (WXEnvironment.getApplication() == null) { WXLogUtils.w("WXText", "ApplicationContent is null on register typeface observer"); return; } mFontFamily = desiredFontFamily; if (mTypefaceObserver != null) { return; } mTypefaceObserver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String fontFamily = intent.getStringExtra("fontFamily"); if (!mFontFamily.equals(fontFamily)) { return; } if (isDestroy() || getDomContext() == null) { return; } DOMActionContext domActionContext = WXSDKManager.getInstance().getWXDomManager() .getDomContext(getDomContext().getInstanceId()); if (domActionContext == null) { return; } WXDomObject domObject = domActionContext.getDomByRef(getRef()); if (domObject == null) { return; } domObject.markDirty(); domActionContext.markDirty(); WXSDKManager.getInstance().getWXDomManager() .sendEmptyMessageDelayed(WXDomHandler.MsgType.WX_DOM_START_BATCH, 2); if (WXEnvironment.isApkDebugable()) { WXLogUtils.d("WXText", "Font family " + fontFamily + " is available"); } } }; if (WXEnvironment.isApkDebugable()) { WXLogUtils.d("WXText", "Font family register " + desiredFontFamily + " is available" + getRef()); } LocalBroadcastManager.getInstance(WXEnvironment.getApplication()).registerReceiver(mTypefaceObserver, new IntentFilter(TypefaceUtil.ACTION_TYPE_FACE_AVAILABLE)); } }