diff --git a/doric-android/doric/src/main/java/pub/doric/shader/DoricTextView.java b/doric-android/doric/src/main/java/pub/doric/shader/DoricTextView.java new file mode 100644 index 00000000..968494d7 --- /dev/null +++ b/doric-android/doric/src/main/java/pub/doric/shader/DoricTextView.java @@ -0,0 +1,174 @@ +package pub.doric.shader; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; +import android.text.Layout; +import android.util.AttributeSet; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +@SuppressLint("AppCompatCustomView") +public class DoricTextView extends TextView { + + private boolean strikethrough = false; + private boolean underline = false; + + private float shadowAlpha = 0; + private float shadowRadius = 0; + private float shadowDx = 0; + private float shadowDy = 0; + private int shadowColor = Color.TRANSPARENT; + + private float gradientAngle = 0; + private int[] gradientColors = null; + private float[] gradientPositions = null; + + public DoricTextView(Context context) { + super(context); + } + + public DoricTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DoricTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setShadow(float alpha, float radius, float dx, float dy, int color) { + this.shadowAlpha = alpha; + this.shadowRadius = radius; + this.shadowDx = dx; + this.shadowDy = dy; + this.shadowColor = color; + + getPaint().setAlpha((int) (255 * alpha)); + getPaint().setShadowLayer(radius, dx, dy, color); + } + + public boolean hasShadow() { + return this.shadowAlpha > 0; + } + + public void setGradient(float angle, int[] colors, float[] positions) { + this.gradientAngle = angle; + this.gradientColors = colors; + this.gradientPositions = positions; + + invalidate(); + } + + public boolean hasGradient() { + return this.gradientColors != null; + } + + public void setUnderline(boolean underline) { + this.underline = underline; + getPaint().setUnderlineText(underline); + } + + public void setStrikethrough(boolean strikethrough) { + this.strikethrough = strikethrough; + getPaint().setStrikeThruText(strikethrough); + } + + @Override + protected void onDraw(Canvas canvas) { + + if (hasGradient()) { + if (hasShadow() || strikethrough || underline) { + getPaint().setShader(null); + + // draw the shadow + if (hasShadow()) { + // shadowColor must be opaque. + setTextColor(0x00ffffff); + getPaint().setAlpha((int) (255 * shadowAlpha)); + getPaint().setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor); + } + getPaint().setStrikeThruText(strikethrough); + getPaint().setUnderlineText(underline); + + super.onDraw(canvas); + } + + // draw the gradient filled text + if (hasShadow()) { + getPaint().clearShadowLayer(); + } + + // gradient colors must be opaque, too. + setGradientTextColor(this, this.gradientAngle, this.gradientColors, this.gradientPositions); + super.onDraw(canvas); + } else { + super.onDraw(canvas); + } + } + + public static void setGradientTextColor(final TextView textView, final float angle, final int[] colors, final float[] positions) { + final Rect textBound = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + final Layout layout = textView.getLayout(); + + if (layout == null) { + return; + } + + for (int i = 0; i < textView.getLineCount(); i++) { + float left = layout.getLineLeft(i); + float right = layout.getLineRight(i); + if (left < textBound.left) textBound.left = (int) left; + if (right > textBound.right) textBound.right = (int) right; + } + textBound.top = layout.getLineTop(0); + textBound.bottom = layout.getLineBottom(textView.getLineCount() - 1); + if (textView.getIncludeFontPadding()) { + Paint.FontMetrics fontMetrics = textView.getPaint().getFontMetrics(); + textBound.top += (fontMetrics.ascent - fontMetrics.top); + textBound.bottom -= (fontMetrics.bottom - fontMetrics.descent); + } + + double angleInRadians = Math.toRadians(angle); + + double r = Math.sqrt(Math.pow(textBound.bottom - textBound.top, 2) + + Math.pow(textBound.right - textBound.left, 2)) / 2; + + float centerX = textBound.left + (textBound.right - textBound.left) / 2.f; + float centerY = textBound.top + (textBound.bottom - textBound.top) / 2.f; + + float startX = (float) (centerX - r * Math.cos(angleInRadians)); + float startY = (float) (centerY + r * Math.sin(angleInRadians)); + + float endX = (float) (centerX + r * Math.cos(angleInRadians)); + float endY = (float) (centerY - r * Math.sin(angleInRadians)); + + Shader textShader = new LinearGradient(startX, startY, endX, endY, colors, positions, + Shader.TileMode.CLAMP); + + textView.setTextColor(0xffffffff); + textView.getPaint().setShader(textShader); + } + + public void reset() { + strikethrough = false; + underline = false; + + shadowAlpha = 0; + shadowRadius = 0; + shadowDx = 0; + shadowDy = 0; + shadowColor = Color.TRANSPARENT; + + gradientAngle = 0; + gradientColors = null; + gradientPositions = null; + invalidate(); + } +} diff --git a/doric-android/doric/src/main/java/pub/doric/shader/TextNode.java b/doric-android/doric/src/main/java/pub/doric/shader/TextNode.java index b9f178d4..bbf08f5a 100644 --- a/doric-android/doric/src/main/java/pub/doric/shader/TextNode.java +++ b/doric-android/doric/src/main/java/pub/doric/shader/TextNode.java @@ -16,20 +16,14 @@ package pub.doric.shader; import android.graphics.Color; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Html; -import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.ViewTreeObserver; -import android.widget.TextView; import androidx.core.content.res.ResourcesCompat; @@ -60,14 +54,14 @@ import pub.doric.utils.ThreadMode; * @CreateDate: 2019-07-20 */ @DoricPlugin(name = "Text") -public class TextNode extends ViewNode { +public class TextNode extends ViewNode { public TextNode(DoricContext doricContext) { super(doricContext); } @Override - protected TextView build() { - TextView tv = new TextView(getContext()); + protected DoricTextView build() { + DoricTextView tv = new DoricTextView(getContext()); tv.setGravity(Gravity.CENTER); tv.setMaxLines(1); tv.setSingleLine(true); @@ -90,7 +84,7 @@ public class TextNode extends ViewNode { } @Override - protected void blend(final TextView view, final String name, final JSValue prop) { + protected void blend(final DoricTextView view, final String name, final JSValue prop) { switch (name) { case "text": if (!prop.isString()) { @@ -178,7 +172,7 @@ public class TextNode extends ViewNode { } } - setGradientTextColor(view, angle, colors, locations); + mView.setGradient(angle, colors, locations); return true; } @@ -312,40 +306,40 @@ public class TextNode extends ViewNode { break; case "strikethrough": if (prop.isBoolean()) { - view.getPaint().setStrikeThruText(prop.asBoolean().value()); + view.setStrikethrough(prop.asBoolean().value()); } break; case "underline": if (prop.isBoolean()) { - view.getPaint().setUnderlineText(prop.asBoolean().value()); + view.setUnderline(prop.asBoolean().value()); } break; case "htmlText": if (prop.isString()) { getDoricContext().getDriver().asyncCall(new Callable() { - @Override - public Spanned call() { - return HtmlParser.buildSpannedText(prop.asString().value(), - new Html.ImageGetter() { - @Override - public Drawable getDrawable(String source) { - try { - Drawable drawable = Glide.with(view) - .asDrawable() - .load(source) - .submit() - .get(); - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - return drawable; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - }, - new CustomTagHandler()); - } - }, ThreadMode.INDEPENDENT) + @Override + public Spanned call() { + return HtmlParser.buildSpannedText(prop.asString().value(), + new Html.ImageGetter() { + @Override + public Drawable getDrawable(String source) { + try { + Drawable drawable = Glide.with(view) + .asDrawable() + .load(source) + .submit() + .get(); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + return drawable; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }, + new CustomTagHandler()); + } + }, ThreadMode.INDEPENDENT) .setCallback(new AsyncResult.Callback() { @Override public void onResult(final Spanned result) { @@ -389,8 +383,8 @@ public class TextNode extends ViewNode { break; case "shadow": if (prop.isObject()) { - mView.setAlpha((prop.asObject().getProperty("opacity").asNumber().toFloat())); - mView.setShadowLayer( + mView.setShadow( + prop.asObject().getProperty("opacity").asNumber().toFloat(), prop.asObject().getProperty("radius").asNumber().toFloat(), DoricUtils.dp2px(prop.asObject().getProperty("offsetX").asNumber().toFloat()), DoricUtils.dp2px(prop.asObject().getProperty("offsetY").asNumber().toFloat()), @@ -404,57 +398,13 @@ public class TextNode extends ViewNode { } } - public static void setGradientTextColor(final TextView textView, final float angle, final int[] colors, final float[] positions) { - final Rect textBound = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - final Layout layout = textView.getLayout(); - - if (layout == null) { - return; - } - - for (int i = 0; i < textView.getLineCount(); i++) { - float left = layout.getLineLeft(i); - float right = layout.getLineRight(i); - if (left < textBound.left) textBound.left = (int) left; - if (right > textBound.right) textBound.right = (int) right; - } - textBound.top = layout.getLineTop(0); - textBound.bottom = layout.getLineBottom(textView.getLineCount() - 1); - if (textView.getIncludeFontPadding()) { - Paint.FontMetrics fontMetrics = textView.getPaint().getFontMetrics(); - textBound.top += (fontMetrics.ascent - fontMetrics.top); - textBound.bottom -= (fontMetrics.bottom - fontMetrics.descent); - } - - double angleInRadians = Math.toRadians(angle); - - double r = Math.sqrt(Math.pow(textBound.bottom - textBound.top, 2) + - Math.pow(textBound.right - textBound.left, 2)) / 2; - - float centerX = textBound.left + (textBound.right - textBound.left) / 2.f; - float centerY = textBound.top + (textBound.bottom - textBound.top) / 2.f; - - float startX = (float) (centerX - r * Math.cos(angleInRadians)); - float startY = (float) (centerY + r * Math.sin(angleInRadians)); - - float endX = (float) (centerX + r * Math.cos(angleInRadians)); - float endY = (float) (centerY - r * Math.sin(angleInRadians)); - - Shader textShader = new LinearGradient(startX, startY, endX, endY, colors, positions, - Shader.TileMode.CLAMP); - - textView.setTextColor(Color.WHITE); - textView.getPaint().setShader(textShader); - textView.invalidate(); - } - - private static File createFile(byte[] bfile, String filePath,String fileName) throws IOException { + private static File createFile(byte[] bfile, String filePath, String fileName) throws IOException { BufferedOutputStream bos = null; FileOutputStream fos = null; try { File dir = new File(filePath); - if(!dir.exists()){ + if (!dir.exists()) { dir.mkdirs(); } String pathName = filePath + File.separator + fileName; @@ -496,7 +446,5 @@ public class TextNode extends ViewNode { mView.setMaxLines(1); mView.setSingleLine(true); mView.setEllipsize(TextUtils.TruncateAt.END); - mView.getPaint().setStrikeThruText(false); - mView.getPaint().setUnderlineText(false); } } diff --git a/doric-demo/src/TextDemo.ts b/doric-demo/src/TextDemo.ts index ea404194..c695cc83 100644 --- a/doric-demo/src/TextDemo.ts +++ b/doric-demo/src/TextDemo.ts @@ -7,6 +7,31 @@ class TextDemo extends Panel { vlayout( [ title("Text Demo"), + text({ + text: '十万钻石场', + textSize: 60, + font: 'PingFangSC-Medium', + textAlignment: Gravity.Center, + shadow: { + color: Color.parse('#640000'), + opacity: 0.49, + radius: 0.5, + offsetX: 0, + offsetY: 1, + }, + textColor: { + colors: [Color.parse('#FFFFFF'), Color.parse('#FFE6A5'), Color.parse('#FFE39B'), Color.parse('#FFF0CE')], + locations: [0.0, 0.7, 0.9, 1.0], + orientation: GradientOrientation.TOP_BOTTOM, + } + }).apply({ + layoutConfig: { + widthSpec: LayoutSpec.FIT, + heightSpec: LayoutSpec.FIT, + margin: { top: 131 }, + alignment: Gravity.CenterX, + } + }), input({ width: 100, height: 100,