From 12e605fdec041e2a661edf45470815595f0fca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=AB=E7=BB=AA=E6=97=BB?= Date: Fri, 23 Nov 2018 19:42:42 +0800 Subject: [PATCH] * [android] Support richtext component (#1796) --- .../adapter/PicassoBasedDrawableLoader.java | 108 +++++++++ .../java/com/alibaba/weex/WXApplication.java | 3 +- .../java/com/taobao/weex/WXSDKEngine.java | 11 + .../taobao/weex/adapter/IDrawableLoader.java | 2 +- .../ui/component/WXBasicComponentType.java | 1 + .../ui/component/richtext/WXRichText.java | 82 +++++++ .../ui/component/richtext/WXRichTextView.java | 114 +++++++++ .../ui/component/richtext/node/ANode.java | 62 +++++ .../ui/component/richtext/node/ImgNode.java | 111 +++++++++ .../component/richtext/node/RichTextNode.java | 220 ++++++++++++++++++ .../richtext/node/RichTextNodeCreator.java | 26 +++ .../richtext/node/RichTextNodeManager.java | 62 +++++ .../ui/component/richtext/node/SpanNode.java | 64 +++++ .../ui/component/richtext/span/ASpan.java | 47 ++++ .../ui/component/richtext/span/ImgSpan.java | 96 ++++++++ .../richtext/span/ItemClickSpan.java | 51 ++++ 16 files changed, 1058 insertions(+), 2 deletions(-) create mode 100644 android/commons/src/main/java/com/alibaba/weex/commons/adapter/PicassoBasedDrawableLoader.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichText.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/WXRichTextView.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ANode.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/ImgNode.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNode.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeCreator.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/RichTextNodeManager.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/node/SpanNode.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ASpan.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ImgSpan.java create mode 100644 android/sdk/src/main/java/com/taobao/weex/ui/component/richtext/span/ItemClickSpan.java 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 class WXBasicComponentType { 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 { + + @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 { + + @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 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 style; + protected Map attr; + protected List 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 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 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 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 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 + 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 { + + @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 param = WXDataStructureUtil.newHashMapWithExpectedSize(1); + param.put(RichTextNode.PSEUDO_REF, mPseudoRef); + instance.fireEvent(mComponentRef, RichTextNode.ITEM_CLICK, param); + } + } +}