You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@weex.apache.org by GitBox <gi...@apache.org> on 2018/11/23 11:42:43 UTC

[GitHub] YorkShen closed pull request #1796: * [android] Support richtext component

YorkShen closed pull request #1796: * [android] Support richtext component
URL: https://github.com/apache/incubator-weex/pull/1796
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/android/commons/src/main/java/com/alibaba/weex/commons/adapter/PicassoBasedDrawableLoader.java b/android/commons/src/main/java/com/alibaba/weex/commons/adapter/PicassoBasedDrawableLoader.java
new file mode 100644
index 0000000000..a642dc77c9
--- /dev/null
+++ b/android/commons/src/main/java/com/alibaba/weex/commons/adapter/PicassoBasedDrawableLoader.java
@@ -0,0 +1,108 @@
+/**
+ * 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.alibaba.weex.commons.adapter;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Target;
+import com.taobao.weex.WXSDKManager;
+import com.taobao.weex.adapter.DrawableStrategy;
+import com.taobao.weex.adapter.IDrawableLoader;
+
+public class PicassoBasedDrawableLoader implements IDrawableLoader {
+
+  private Context mContext;
+
+  public PicassoBasedDrawableLoader(Context context) {
+    mContext = context;
+  }
+
+  @Override
+  public void setDrawable(final String url,
+                          final DrawableTarget drawableTarget,
+                          final DrawableStrategy drawableStrategy) {
+    WXSDKManager.getInstance().postOnUiThread(new Runnable() {
+      @Override
+      public void run() {
+        String temp = url;
+        if (url.startsWith("//")) {
+          temp = "http:" + url;
+        }
+
+        /** This is a hack for picasso, as Picasso hold weakReference to Target.
+         * http://stackoverflow.com/questions/24180805/onbitmaploaded-of-target-object-not-called-on-first-load
+         */
+        class PlaceHolderDrawableTarget extends Drawable implements Target {
+
+          @Override
+          public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
+            BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
+            bitmapDrawable.setGravity(Gravity.FILL);
+            drawableTarget.setDrawable(bitmapDrawable, true);
+          }
+
+          @Override
+          public void onBitmapFailed(Drawable errorDrawable) {
+
+          }
+
+          @Override
+          public void onPrepareLoad(Drawable placeHolderDrawable) {
+            drawableTarget.setDrawable(this, true);
+          }
+
+          @Override
+          public void draw(Canvas canvas) {
+
+          }
+
+          @Override
+          public void setAlpha(int alpha) {
+
+          }
+
+          @Override
+          public void setColorFilter(ColorFilter colorFilter) {
+
+          }
+
+          @Override
+          public int getOpacity() {
+            return PixelFormat.UNKNOWN;
+          }
+        }
+        Picasso.
+                with(mContext).
+                load(temp).
+                resize(drawableStrategy.width, drawableStrategy.height).
+                onlyScaleDown().
+                into(new PlaceHolderDrawableTarget());
+      }
+    }, 0);
+
+  }
+}
diff --git a/android/playground/app/src/main/java/com/alibaba/weex/WXApplication.java b/android/playground/app/src/main/java/com/alibaba/weex/WXApplication.java
index 48d59f4e37..15ae440ed0 100644
--- a/android/playground/app/src/main/java/com/alibaba/weex/WXApplication.java
+++ b/android/playground/app/src/main/java/com/alibaba/weex/WXApplication.java
@@ -25,6 +25,7 @@
 import com.alibaba.weex.commons.adapter.DefaultWebSocketAdapterFactory;
 import com.alibaba.weex.commons.adapter.ImageAdapter;
 import com.alibaba.weex.commons.adapter.JSExceptionAdapter;
+import com.alibaba.weex.commons.adapter.PicassoBasedDrawableLoader;
 import com.alibaba.weex.extend.adapter.ApmGenerator;
 import com.alibaba.weex.extend.adapter.DefaultAccessibilityRoleAdapter;
 import com.alibaba.weex.extend.adapter.InterceptWXHttpAdapter;
@@ -72,6 +73,7 @@ public void onCreate() {
                            new InitConfig.Builder()
                                //.setImgAdapter(new FrescoImageAdapter())// use fresco adapter
                                .setImgAdapter(new ImageAdapter())
+                                   .setDrawableLoader(new PicassoBasedDrawableLoader(getApplicationContext()))
                                .setWebSocketAdapterFactory(new DefaultWebSocketAdapterFactory())
                                .setJSExceptionAdapter(new JSExceptionAdapter())
                                .setHttpAdapter(new InterceptWXHttpAdapter())
@@ -86,7 +88,6 @@ public void onCreate() {
       WXSDKEngine.registerComponent("synccomponent", WXComponentSyncTest.class);
       WXSDKEngine.registerComponent(WXParallax.PARALLAX, WXParallax.class);
 
-      WXSDKEngine.registerComponent("richtext", RichText.class);
       WXSDKEngine.registerModule("render", RenderModule.class);
       WXSDKEngine.registerModule("event", WXEventModule.class);
       WXSDKEngine.registerModule("syncTest", SyncTestModule.class);
diff --git a/android/sdk/src/main/java/com/taobao/weex/WXSDKEngine.java b/android/sdk/src/main/java/com/taobao/weex/WXSDKEngine.java
index 18749b8cb8..beb6a5023d 100644
--- a/android/sdk/src/main/java/com/taobao/weex/WXSDKEngine.java
+++ b/android/sdk/src/main/java/com/taobao/weex/WXSDKEngine.java
@@ -78,11 +78,13 @@
 import com.taobao.weex.ui.component.WXText;
 import com.taobao.weex.ui.component.WXVideo;
 import com.taobao.weex.ui.component.WXWeb;
+import com.taobao.weex.ui.component.basic.WXBasicComponent;
 import com.taobao.weex.ui.component.list.HorizontalListComponent;
 import com.taobao.weex.ui.component.list.SimpleListComponent;
 import com.taobao.weex.ui.component.list.WXCell;
 import com.taobao.weex.ui.component.list.WXListComponent;
 import com.taobao.weex.ui.component.list.template.WXRecyclerTemplateList;
+import com.taobao.weex.ui.component.richtext.WXRichText;
 import com.taobao.weex.ui.config.AutoScanConfigRegister;
 import com.taobao.weex.ui.module.WXLocaleModule;
 import com.taobao.weex.ui.module.WXMetaModule;
@@ -330,6 +332,15 @@ private static void register() {
               WXBasicComponentType.RECYCLER,
               WXBasicComponentType.WATERFALL);
 
+      registerComponent(
+              new SimpleComponentHolder(
+                      WXRichText.class,
+                      new WXRichText.Creator()
+              ),
+              false,
+              WXBasicComponentType.RICHTEXT
+      );
+
       String simpleList = "simplelist";
       registerComponent(SimpleListComponent.class,false,simpleList);
       registerComponent(WXRecyclerTemplateList.class, false,WXBasicComponentType.RECYCLE_LIST);
diff --git a/android/sdk/src/main/java/com/taobao/weex/adapter/IDrawableLoader.java b/android/sdk/src/main/java/com/taobao/weex/adapter/IDrawableLoader.java
index 2b824f2a48..080d3b58f6 100644
--- a/android/sdk/src/main/java/com/taobao/weex/adapter/IDrawableLoader.java
+++ b/android/sdk/src/main/java/com/taobao/weex/adapter/IDrawableLoader.java
@@ -25,7 +25,7 @@
 public interface IDrawableLoader {
 
   interface DrawableTarget {
-
+    void setDrawable(@Nullable Drawable drawable, boolean resetBounds);
   }
 
   interface StaticTarget extends DrawableTarget{
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/WXBasicComponentType.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/WXBasicComponentType.java
index 6f6fcb6ad6..936206f812 100644
--- a/android/sdk/src/main/java/com/taobao/weex/ui/component/WXBasicComponentType.java
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/WXBasicComponentType.java
@@ -51,6 +51,7 @@
   public static final String LOADING = "loading";
   public static final String LOADING_INDICATOR = "loading-indicator";
   public static final String CYCLE_SLIDER = "cycleslider";
+  public static final String RICHTEXT = "richtext";
 
   public static final String RECYCLE_LIST = "recycle-list";
   public static final String CELL_SLOT = "cell-slot";
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichText.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichText.java
new file mode 100644
index 0000000000..0fccd12b4c
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichText.java
@@ -0,0 +1,82 @@
+/**
+ * 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.ui.component.richtext;
+
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+
+import com.taobao.weex.WXSDKInstance;
+import com.taobao.weex.layout.measurefunc.TextContentBoxMeasurement;
+import com.taobao.weex.ui.ComponentCreator;
+import com.taobao.weex.ui.action.BasicComponentData;
+import com.taobao.weex.ui.component.WXComponent;
+import com.taobao.weex.ui.component.WXText;
+import com.taobao.weex.ui.component.WXVContainer;
+import com.taobao.weex.ui.component.richtext.node.RichTextNode;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class WXRichText extends WXText {
+
+  static class RichTextContentBoxMeasurement extends TextContentBoxMeasurement {
+
+    public RichTextContentBoxMeasurement(WXComponent component) {
+      super(component);
+    }
+
+    @NonNull
+    @Override
+    protected Spanned createSpanned(String text) {
+      if (mComponent.getInstance() != null & mComponent.getInstance().getUIContext() != null &&
+              !TextUtils.isEmpty(mComponent.getInstanceId())) {
+        Spannable spannable = RichTextNode.parse(
+                mComponent.getInstance().getUIContext(),
+                mComponent.getInstanceId(),
+                mComponent.getRef(),
+                text);
+        updateSpannable(spannable, RichTextNode.createSpanFlag(0));
+        return spannable;
+      } else {
+        return new SpannedString("");
+      }
+    }
+  }
+
+  public static class Creator implements ComponentCreator {
+
+    public WXComponent createInstance(WXSDKInstance instance, WXVContainer parent, BasicComponentData basicComponentData) throws IllegalAccessException, InvocationTargetException, InstantiationException {
+      return new WXRichText(instance, parent, basicComponentData);
+    }
+  }
+
+  public WXRichText(WXSDKInstance instance, WXVContainer parent, BasicComponentData basicComponentData) {
+    super(instance, parent, basicComponentData);
+    setContentBoxMeasurement(new RichTextContentBoxMeasurement(this));
+  }
+
+  @Override
+  protected WXRichTextView initComponentHostView(@NonNull Context context) {
+    return new WXRichTextView(context);
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichTextView.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichTextView.java
new file mode 100644
index 0000000000..86f670ba6d
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichTextView.java
@@ -0,0 +1,114 @@
+/**
+ * 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.ui.component.richtext;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import com.taobao.weex.ui.component.richtext.span.ImgSpan;
+import com.taobao.weex.ui.view.WXTextView;
+
+public class WXRichTextView extends WXTextView {
+
+  public WXRichTextView(Context context) {
+    super(context);
+  }
+
+  @Override
+  public boolean onTouchEvent(MotionEvent event) {
+    boolean superResult = super.onTouchEvent(event);
+    boolean handled = false;
+    if (isEnabled() && getTextLayout() != null && getText() instanceof Spannable) {
+      Spannable spannable = (Spannable) getText();
+      handled = updateSelection(event, spannable);
+    }
+    return handled || superResult;
+  }
+
+  @Override
+  protected boolean verifyDrawable(Drawable who) {
+    super.verifyDrawable(who);
+    return true;
+  }
+
+  @Override
+  public void setTextLayout(Layout layout) {
+    super.setTextLayout(layout);
+    if (layout.getText() instanceof Spanned) {
+      Spanned spanned = (Spanned) layout.getText();
+      ImgSpan[] imgSpan = spanned.getSpans(0, spanned.length(), ImgSpan.class);
+      if (imgSpan != null) {
+        for (ImgSpan span : imgSpan) {
+          span.setView(this);
+        }
+      }
+    }
+  }
+
+  /**
+   * Mostly copied from
+   * {@link android.text.method.LinkMovementMethod#onTouchEvent(TextView, Spannable, MotionEvent)}.
+   */
+  private boolean updateSelection(MotionEvent event, Spannable buffer) {
+    int action = event.getActionMasked();
+
+    if (action == MotionEvent.ACTION_UP ||
+        action == MotionEvent.ACTION_DOWN) {
+      int x = (int) event.getX();
+      int y = (int) event.getY();
+
+      x -= getPaddingLeft();
+      y -= getPaddingTop();
+
+      x += getScrollX();
+      y += getScrollY();
+
+      Layout layout = getTextLayout();
+      int line = layout.getLineForVertical(y);
+      int off = layout.getOffsetForHorizontal(line, x);
+
+      ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
+
+      if (link.length != 0) {
+        if (action == MotionEvent.ACTION_UP) {
+          link[0].onClick(this);
+        } else {
+          Selection.setSelection(buffer,
+                                 buffer.getSpanStart(link[0]),
+                                 buffer.getSpanEnd(link[0]));
+        }
+
+        return true;
+      } else {
+        Selection.removeSelection(buffer);
+      }
+    }
+
+    return false;
+  }
+
+
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ANode.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ANode.java
new file mode 100644
index 0000000000..4bda872431
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ANode.java
@@ -0,0 +1,62 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import android.content.Context;
+import android.text.SpannableStringBuilder;
+
+import com.taobao.weex.ui.component.richtext.span.ASpan;
+
+class ANode extends RichTextNode {
+
+  static class ANodeCreator implements RichTextNodeCreator<ANode> {
+
+    @Override
+    public ANode createRichTextNode(Context context, String instanceId, String componentRef) {
+      return new ANode(context, instanceId, componentRef);
+    }
+  }
+
+  public static final String NODE_TYPE = "a";
+  public static final String HREF = "href";
+
+  private ANode(Context context, String instanceId, String componentRef) {
+    super(context, instanceId, componentRef);
+  }
+
+  @Override
+  public String toString() {
+    return "";
+  }
+
+  @Override
+  protected boolean isInternalNode() {
+    return true;
+  }
+
+  @Override
+  protected void updateSpans(SpannableStringBuilder spannableStringBuilder, int level) {
+    super.updateSpans(spannableStringBuilder, level);
+    if (attr != null && attr.containsKey(HREF)) {
+      ASpan aSpan = new ASpan(mInstanceId, attr.get(HREF).toString());
+      spannableStringBuilder.setSpan(aSpan, 0, spannableStringBuilder.length(),
+          createSpanFlag(level));
+    }
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ImgNode.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ImgNode.java
new file mode 100644
index 0000000000..557cdd23da
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ImgNode.java
@@ -0,0 +1,111 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import static com.taobao.weex.utils.WXViewUtils.getRealPxByWidth;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.SpannableStringBuilder;
+import com.taobao.weex.WXSDKEngine;
+import com.taobao.weex.WXSDKInstance;
+import com.taobao.weex.WXSDKManager;
+import com.taobao.weex.adapter.DrawableStrategy;
+import com.taobao.weex.adapter.URIAdapter;
+import com.taobao.weex.common.Constants;
+import com.taobao.weex.ui.component.richtext.span.ImgSpan;
+import com.taobao.weex.ui.component.richtext.span.ItemClickSpan;
+import com.taobao.weex.utils.ImgURIUtil;
+import com.taobao.weex.utils.WXUtils;
+import java.util.LinkedList;
+import java.util.List;
+
+class ImgNode extends RichTextNode {
+
+  static class ImgNodeCreator implements RichTextNodeCreator<ImgNode> {
+
+    @Override
+    public ImgNode createRichTextNode(Context context, String instanceId, String componentRef) {
+      return new ImgNode(context, instanceId, componentRef);
+    }
+  }
+
+  public static final String NODE_TYPE = "image";
+
+  private ImgNode(Context context, String instanceId, String componentRef) {
+    super(context, instanceId, componentRef);
+  }
+
+  @Override
+  public String toString() {
+    return " ";
+  }
+
+  @Override
+  protected boolean isInternalNode() {
+    return false;
+  }
+
+  @Override
+  protected void updateSpans(SpannableStringBuilder spannableStringBuilder, int level) {
+    WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(mInstanceId);
+    if (WXSDKEngine.getDrawableLoader() != null &&
+        style.containsKey(Constants.Name.WIDTH) &&
+        style.containsKey(Constants.Name.HEIGHT) &&
+        attr.containsKey(Constants.Name.SRC) &&
+        instance != null) {
+      List<Object> spans = new LinkedList<>();
+      spans.add(createImgSpan(instance));
+
+      if (attr.containsKey(RichTextNode.PSEUDO_REF)) {
+        spans.add(new ItemClickSpan(mInstanceId, mComponentRef,
+            attr.get(RichTextNode.PSEUDO_REF).toString()));
+      }
+
+      for (Object span : spans) {
+        spannableStringBuilder.setSpan(
+            span, 0, spannableStringBuilder.length(), createSpanFlag(level));
+      }
+    }
+  }
+
+  @NonNull
+  private ImgSpan createImgSpan(WXSDKInstance instance) {
+    int width = (int) getRealPxByWidth(WXUtils.getFloat(style.get(Constants.Name.WIDTH)),
+        instance.getInstanceViewPortWidth());
+    int height = (int) getRealPxByWidth(WXUtils.getFloat(style.get(Constants.Name.HEIGHT)),
+        instance.getInstanceViewPortWidth());
+    ImgSpan imageSpan = new ImgSpan(width, height);
+
+    String url = attr.get(Constants.Name.SRC).toString();
+    Uri rewrited = instance.rewriteUri(Uri.parse(url), URIAdapter.IMAGE);
+    if (Constants.Scheme.LOCAL.equals(rewrited.getScheme())) {
+      Drawable localDrawable = ImgURIUtil.getDrawableFromLoaclSrc(mContext, rewrited);
+      imageSpan.setDrawable(localDrawable, false);
+    } else {
+      DrawableStrategy drawableStrategy = new DrawableStrategy();
+      drawableStrategy.width = width;
+      drawableStrategy.height = height;
+      WXSDKEngine.getDrawableLoader().setDrawable(rewrited.toString(), imageSpan, drawableStrategy);
+    }
+    return imageSpan;
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNode.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNode.java
new file mode 100644
index 0000000000..3ff96221f3
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNode.java
@@ -0,0 +1,220 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import static com.taobao.weex.dom.WXStyle.UNSET;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.taobao.weex.WXSDKInstance;
+import com.taobao.weex.WXSDKManager;
+import com.taobao.weex.common.Constants;
+import com.taobao.weex.dom.WXCustomStyleSpan;
+import com.taobao.weex.dom.WXStyle;
+import com.taobao.weex.utils.WXResourceUtils;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public abstract class RichTextNode {
+
+    public static final String TYPE = "type";
+    public static final String STYLE = "style";
+    public static final String ATTR = "attr";
+    public static final String CHILDREN = "children";
+    public static final String VALUE = Constants.Name.VALUE;
+    public static final String ITEM_CLICK="itemclick";
+    public static final String PSEUDO_REF="pseudoRef";
+    private static final int MAX_LEVEL = Spanned.SPAN_PRIORITY >> Spanned.SPAN_PRIORITY_SHIFT;
+
+    protected final Context mContext;
+    protected final String mInstanceId;
+    protected final String mComponentRef;
+    protected Map<String, Object> style;
+    protected Map<String, Object> attr;
+    protected List<RichTextNode> children;
+
+    protected RichTextNode(Context context, String instanceId, String componentRef) {
+        mContext = context;
+        mInstanceId = instanceId;
+        mComponentRef = componentRef;
+    }
+
+    public static
+    @NonNull
+    Spannable parse(@NonNull Context context, @NonNull String instanceId, @NonNull String componentRef, String json) {
+        JSONArray jsonArray = JSON.parseArray(json);
+        JSONObject jsonObject;
+        List<RichTextNode> nodes;
+        RichTextNode node;
+        if (jsonArray != null && !jsonArray.isEmpty()) {
+            nodes = new ArrayList<>(jsonArray.size());
+            for (int i = 0; i < jsonArray.size(); i++) {
+                jsonObject = jsonArray.getJSONObject(i);
+                if (jsonObject != null) {
+                    node = RichTextNodeManager.createRichTextNode(context, instanceId, componentRef, jsonObject);
+                    if (node != null) {
+                        nodes.add(node);
+                    }
+                }
+            }
+            return parse(nodes);
+        }
+        return new SpannableString("");
+    }
+
+    public static int createSpanFlag(int level) {
+        return createPriorityFlag(level) | Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
+    }
+
+    @Override
+    public abstract String toString();
+
+    protected abstract boolean isInternalNode();
+
+    final void parse(@NonNull Context context, @NonNull String instanceId, @NonNull String componentRef, JSONObject jsonObject) {
+        JSONObject jsonStyle, jsonAttr, child;
+        JSONArray jsonChildren;
+        RichTextNode node;
+        if ((jsonStyle = jsonObject.getJSONObject(STYLE)) != null) {
+            style = new ArrayMap<>();
+            style.putAll(jsonStyle);
+        } else {
+            style = new ArrayMap<>(0);
+        }
+
+        if ((jsonAttr = jsonObject.getJSONObject(ATTR)) != null) {
+            attr = new ArrayMap<>(jsonAttr.size());
+            attr.putAll(jsonAttr);
+        } else {
+            attr = new ArrayMap<>(0);
+        }
+
+        if ((jsonChildren=jsonObject.getJSONArray(CHILDREN))!=null) {
+            children = new ArrayList<>(jsonChildren.size());
+            for (int i = 0; i < jsonChildren.size(); i++) {
+                child = jsonChildren.getJSONObject(i);
+                node = RichTextNodeManager.createRichTextNode(context, instanceId, componentRef, child);
+                if (node != null) {
+                    children.add(node);
+                }
+            }
+        } else {
+            children = new ArrayList<>(0);
+        }
+    }
+
+    protected void updateSpans(SpannableStringBuilder spannableStringBuilder, int level) {
+        WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(mInstanceId);
+        if (style != null && instance != null) {
+            List<Object> spans = new LinkedList<>();
+
+            WXCustomStyleSpan customStyleSpan = createCustomStyleSpan();
+            if (customStyleSpan != null) {
+                spans.add(customStyleSpan);
+            }
+
+            if (style.containsKey(Constants.Name.FONT_SIZE)) {
+                spans.add(new AbsoluteSizeSpan(WXStyle.getFontSize(style, instance.getInstanceViewPortWidth())));
+            }
+
+            if (style.containsKey(Constants.Name.BACKGROUND_COLOR)) {
+                int color = WXResourceUtils.getColor(style.get(Constants.Name.BACKGROUND_COLOR).toString(),
+                    Color.TRANSPARENT);
+                if (color != Color.TRANSPARENT) {
+                    spans.add(new BackgroundColorSpan(color));
+                }
+            }
+
+            if (style.containsKey(Constants.Name.COLOR)) {
+                spans.add(new ForegroundColorSpan(WXResourceUtils.getColor(WXStyle.getTextColor(style))));
+            }
+
+            int spanFlag = createSpanFlag(level);
+            for (Object span : spans) {
+                spannableStringBuilder.setSpan(span, 0, spannableStringBuilder.length(), spanFlag);
+            }
+        }
+    }
+
+    private static int createPriorityFlag(int level) {
+        return level <= MAX_LEVEL ?
+            (MAX_LEVEL - level) << Spanned.SPAN_PRIORITY_SHIFT :
+            MAX_LEVEL << Spanned.SPAN_PRIORITY_SHIFT;
+    }
+
+    private static
+    @NonNull
+    Spannable parse(@NonNull List<RichTextNode> list) {
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
+        for (RichTextNode richTextNode : list) {
+            spannableStringBuilder.append(richTextNode.toSpan(1));
+        }
+        return spannableStringBuilder;
+    }
+
+    private Spannable toSpan(int level) {
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
+        spannableStringBuilder.append(toString());
+        if (isInternalNode() && children != null) {
+            for (RichTextNode child : children) {
+                spannableStringBuilder.append(child.toSpan(level + 1));
+            }
+        }
+        updateSpans(spannableStringBuilder, level);
+        return spannableStringBuilder;
+    }
+
+    private
+    @Nullable
+    WXCustomStyleSpan createCustomStyleSpan() {
+        int fontWeight = UNSET, fontStyle = UNSET;
+        String fontFamily = null;
+        if (style.containsKey(Constants.Name.FONT_WEIGHT)) {
+            fontWeight = WXStyle.getFontWeight(style);
+        }
+        if (style.containsKey(Constants.Name.FONT_STYLE)) {
+            fontStyle = WXStyle.getFontStyle(style);
+        }
+        if (style.containsKey(Constants.Name.FONT_FAMILY)) {
+            fontFamily = WXStyle.getFontFamily(style);
+        }
+        if (fontWeight != UNSET
+            || fontStyle != UNSET
+            || fontFamily != null) {
+            return new WXCustomStyleSpan(fontStyle, fontWeight, fontFamily);
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeCreator.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeCreator.java
new file mode 100644
index 0000000000..26e3055b43
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeCreator.java
@@ -0,0 +1,26 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import android.content.Context;
+
+public interface RichTextNodeCreator<T extends RichTextNode> {
+
+  T createRichTextNode(Context context, String instanceId, String componentRef);
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeManager.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeManager.java
new file mode 100644
index 0000000000..c374487187
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeManager.java
@@ -0,0 +1,62 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+
+import com.alibaba.fastjson.JSONObject;
+import com.taobao.weex.utils.WXLogUtils;
+
+import java.util.Map;
+
+public class RichTextNodeManager {
+
+  private final static Map<String, RichTextNodeCreator>
+      registeredTextNodes = new ArrayMap<>();
+
+  static {
+    registeredTextNodes.put(SpanNode.NODE_TYPE, new SpanNode.SpanNodeCreator());
+    registeredTextNodes.put(ImgNode.NODE_TYPE, new ImgNode.ImgNodeCreator());
+    registeredTextNodes.put(ANode.NODE_TYPE, new ANode.ANodeCreator());
+  }
+
+  public static void registerTextNode(String text, RichTextNodeCreator type) {
+    registeredTextNodes.put(text, type);
+  }
+
+  @Nullable
+  static RichTextNode createRichTextNode(@NonNull Context context, @NonNull String instanceId,
+      @NonNull String componentRef, @Nullable JSONObject jsonObject) {
+    RichTextNode instance = null;
+    try {
+      if (jsonObject != null) {
+        instance = registeredTextNodes.get(jsonObject.getString(RichTextNode.TYPE))
+            .createRichTextNode(context, instanceId, componentRef);
+        instance.parse(context, instanceId, componentRef, jsonObject);
+      }
+    } catch (Exception e) {
+      WXLogUtils.e("Richtext", WXLogUtils.getStackTrace(e));
+      instance = null;
+    }
+    return instance;
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/SpanNode.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/SpanNode.java
new file mode 100644
index 0000000000..4bfe0c9bb0
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/SpanNode.java
@@ -0,0 +1,64 @@
+/**
+ * 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.ui.component.richtext.node;
+
+import android.content.Context;
+import android.text.SpannableStringBuilder;
+
+import com.taobao.weex.common.Constants;
+import com.taobao.weex.dom.TextDecorationSpan;
+import com.taobao.weex.dom.WXStyle;
+
+class SpanNode extends RichTextNode {
+
+  static class SpanNodeCreator implements RichTextNodeCreator<SpanNode> {
+
+    @Override
+    public SpanNode createRichTextNode(Context context, String instanceId, String componentRef) {
+      return new SpanNode(context, instanceId, componentRef);
+    }
+  }
+
+  public static final String NODE_TYPE = "span";
+
+  private SpanNode(Context context, String instanceId, String componentRef) {
+    super(context, instanceId, componentRef);
+  }
+
+  @Override
+  public String toString() {
+    if (attr == null || !attr.containsKey(Constants.Name.VALUE)) {
+      return "";
+    } else {
+      return attr.get(Constants.Name.VALUE).toString();
+    }
+  }
+
+  @Override
+  protected boolean isInternalNode() {
+    return true;
+  }
+
+  @Override
+  protected void updateSpans(SpannableStringBuilder spannableStringBuilder, int level) {
+    super.updateSpans(spannableStringBuilder, level);
+    spannableStringBuilder.setSpan(new TextDecorationSpan(WXStyle.getTextDecoration(style)), 0,
+        spannableStringBuilder.length(), createSpanFlag(level));
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ASpan.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ASpan.java
new file mode 100644
index 0000000000..8f5dd72689
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ASpan.java
@@ -0,0 +1,47 @@
+/**
+ * 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.ui.component.richtext.span;
+
+import android.text.TextPaint;
+import android.text.style.ClickableSpan;
+import android.view.View;
+import com.taobao.weex.utils.ATagUtil;
+
+public class ASpan extends ClickableSpan {
+
+  private String mInstanceId, mURL;
+
+  public ASpan(String instanceId, String url) {
+    mInstanceId = instanceId;
+    mURL = url;
+  }
+
+  @Override
+  public void onClick(View widget) {
+    ATagUtil.onClick(widget, mInstanceId, mURL);
+  }
+
+  /**
+   Override super method and do nothing. As no default color or text-decoration is needed.
+   */
+  @Override
+  public void updateDrawState(TextPaint ds) {
+
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ImgSpan.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ImgSpan.java
new file mode 100644
index 0000000000..f3bdc3c497
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ImgSpan.java
@@ -0,0 +1,96 @@
+/**
+ * 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.ui.component.richtext.span;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.text.style.ReplacementSpan;
+import android.view.View;
+
+import com.taobao.weex.adapter.IDrawableLoader;
+
+
+public class ImgSpan extends ReplacementSpan implements IDrawableLoader.StaticTarget {
+
+  private int width, height;
+  private Drawable mDrawable;
+  private View mView;
+
+  public ImgSpan(int width, int height) {
+    this.width = width;
+    this.height = height;
+  }
+
+  /**
+   * Mostly copied from
+   *
+   * {@link android.text.style.DynamicDrawableSpan#getSize(Paint, CharSequence, int, int, Paint.FontMetricsInt)},
+   * but not use Drawable to calculate size;
+   */
+  @Override
+  public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+    if (fm != null) {
+      fm.ascent = -height;
+      fm.descent = 0;
+
+      fm.top = fm.ascent;
+      fm.bottom = 0;
+    }
+    return width;
+  }
+
+  /**
+   * Mostly copied from
+   * {@link android.text.style.DynamicDrawableSpan#draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)},
+   * except for vertical alignment.
+   */
+  @Override
+  public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
+    if (mDrawable != null) {
+      canvas.save();
+      int transY = bottom - mDrawable.getBounds().bottom;
+      transY -= paint.getFontMetricsInt().descent;
+      canvas.translate(x, transY);
+      mDrawable.draw(canvas);
+      canvas.restore();
+    }
+  }
+
+  @Override
+  public void setDrawable(Drawable drawable, boolean resetBounds) {
+    mDrawable = drawable;
+    if(resetBounds) {
+      mDrawable.setBounds(0, 0, width, height);
+    }
+    setCallback();
+    mDrawable.invalidateSelf();
+  }
+
+  public void setView(View view) {
+    mView = view;
+    setCallback();
+  }
+
+  private void setCallback() {
+    if (mDrawable != null && mView != null) {
+      mDrawable.setCallback(mView);
+    }
+  }
+}
diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ItemClickSpan.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ItemClickSpan.java
new file mode 100644
index 0000000000..c7146da3a1
--- /dev/null
+++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ItemClickSpan.java
@@ -0,0 +1,51 @@
+/**
+ * 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.ui.component.richtext.span;
+
+
+import android.text.style.ClickableSpan;
+import android.view.View;
+import com.taobao.weex.WXSDKInstance;
+import com.taobao.weex.WXSDKManager;
+import com.taobao.weex.ui.component.richtext.node.RichTextNode;
+import com.taobao.weex.utils.WXDataStructureUtil;
+import java.util.Map;
+
+public class ItemClickSpan extends ClickableSpan {
+
+  private final String mPseudoRef;
+  private final String mInstanceId;
+  private final String mComponentRef;
+
+  public ItemClickSpan(String instanceId, String componentRef, String pseudoRef) {
+    this.mPseudoRef = pseudoRef;
+    this.mInstanceId = instanceId;
+    this.mComponentRef = componentRef;
+  }
+
+  @Override
+  public void onClick(View widget) {
+    WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(mInstanceId);
+    if (instance != null && !instance.isDestroy()) {
+      Map<String, Object> param = WXDataStructureUtil.newHashMapWithExpectedSize(1);
+      param.put(RichTextNode.PSEUDO_REF, mPseudoRef);
+      instance.fireEvent(mComponentRef, RichTextNode.ITEM_CLICK, param);
+    }
+  }
+}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services