From 77b2f98f1790487e9215651bb2368384a2f77260 Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 9 Oct 2024 05:20:58 +0300 Subject: [PATCH] Quotes in text formatting (AND-222) --- .../android/ui/text/BlockQuoteSpan.java | 70 +++++++++++++++++++ .../android/ui/text/HtmlParser.java | 27 +++++-- .../android/ui/text/SpacerSpan.java | 7 +- .../android/ui/utils/UiUtils.java | 4 +- mastodon/src/main/res/drawable/quote.xml | 10 +++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/text/BlockQuoteSpan.java create mode 100644 mastodon/src/main/res/drawable/quote.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/BlockQuoteSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/BlockQuoteSpan.java new file mode 100644 index 00000000..5cfb7495 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/BlockQuoteSpan.java @@ -0,0 +1,70 @@ +package org.joinmastodon.android.ui.text; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.CharacterStyle; +import android.text.style.LeadingMarginSpan; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.utils.UiUtils; + +import androidx.annotation.NonNull; +import me.grishka.appkit.utils.V; + +public class BlockQuoteSpan extends CharacterStyle implements LeadingMarginSpan{ + private final Context context; + private Drawable icon; + private boolean firstLevel; + private Paint paint=new Paint(); + + public BlockQuoteSpan(Context context, boolean firstLevel){ + this.context=context; + icon=context.getResources().getDrawable(R.drawable.quote, context.getTheme()); + this.firstLevel=firstLevel; + paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3TertiaryContainer)); + paint.setAlpha(51); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(V.dp(3)); + } + + + @Override + public int getLeadingMargin(boolean first){ + return V.dp(firstLevel ? 32 : 18); + } + + @Override + public void drawLeadingMargin(@NonNull Canvas c, @NonNull Paint p, int x, int dir, int top, int baseline, int bottom, @NonNull CharSequence text, int start, int end, boolean first, @NonNull Layout layout){ + if(text instanceof Spanned s && s.getSpanStart(this)==start){ + int level=s.getSpans(start, end, LeadingMarginSpan.class).length-1; + if(dir<0){ // RTL +// c.drawText(this.text, layout.getWidth()-V.dp(32*level)-p.measureText(this.text), baseline, p); + if(level==0){ + icon.setBounds(layout.getWidth()-icon.getIntrinsicWidth(), top, layout.getWidth(), top+icon.getIntrinsicHeight()); + icon.draw(c); + }else{ + float xOffset=layout.getWidth()-V.dp(32+18*(level-1)+1.5f); + c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint); + } + }else{ + if(level==0){ + icon.setBounds(x, top, x+icon.getIntrinsicWidth(), top+icon.getIntrinsicHeight()); + icon.draw(c); + }else{ + float xOffset=x+V.dp(32+18*(level-1)+1.5f); + c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint); + } + } + } + } + + @Override + public void updateDrawState(TextPaint tp){ + tp.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Tertiary)); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index 456b8107..8e703360 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -8,6 +8,8 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.LineHeightSpan; +import android.text.style.RelativeSizeSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.widget.TextView; @@ -105,6 +107,14 @@ public class HtmlParser{ return false; } + private boolean isInsideBlockquote(){ + for(SpanInfo si:openSpans){ + if(si.span instanceof BlockQuoteSpan) + return true; + } + return false; + } + @SuppressLint("DefaultLocale") @Override public void head(@NonNull Node node, int depth){ @@ -158,7 +168,7 @@ public class HtmlParser{ case "code" -> { if(!isInsidePre()){ openSpans.add(new SpanInfo(new MonospaceSpan(context), ssb.length(), el)); - ssb.append(" ", new SpacerSpan(V.dp(4), 1), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } case "pre" -> openSpans.add(new SpanInfo(new CodeBlockSpan(context), ssb.length(), el)); @@ -185,6 +195,11 @@ public class HtmlParser{ copyableText.append(' '); ssb.append(copyableText.toString(), new InvisibleSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } + case "blockquote" -> { + if(ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n') + ssb.append('\n'); + openSpans.add(new SpanInfo(new BlockQuoteSpan(context, !isInsideBlockquote()), ssb.length(), el)); + } } } } @@ -195,9 +210,11 @@ public class HtmlParser{ String name=el.nodeName(); if("span".equals(name) && el.hasClass("ellipsis")){ ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - }else if("p".equals(name) || ("ol".equals(name) || "ul".equals(name))){ - if(node.nextSibling()!=null) - ssb.append("\n\n"); + }else if("p".equals(name) || "ol".equals(name) || "ul".equals(name)){ + if(node.nextSibling()!=null && "body".equals(node.parent().nodeName())){ + ssb.append('\n'); + ssb.append("\n", new SpacerSpan(1, V.dp(8)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } }else if("pre".equals(name)){ if(node.nextSibling()!=null) ssb.append("\n"); @@ -207,7 +224,7 @@ public class HtmlParser{ if(si.element==el){ if(si.span!=null){ if(si.span instanceof MonospaceSpan){ - ssb.append(" ", new SpacerSpan(V.dp(4), 1), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/SpacerSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/SpacerSpan.java index bb99f7f4..6ecbe7b9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/SpacerSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/SpacerSpan.java @@ -17,7 +17,12 @@ public class SpacerSpan extends ReplacementSpan{ @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){ - // TODO height + if(fm!=null && height>0){ + fm.ascent=-height; + fm.descent=0; + fm.top=fm.ascent; + fm.bottom=0; + } return width; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index e064780b..77e0fd5c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -713,8 +713,8 @@ public class UiUtils{ item.setIcon(icon); SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle()); ssb.insert(0, " "); - ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0); - ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0); + ssb.setSpan(new SpacerSpan(V.dp(24), 0), 0, 1, 0); + ssb.append(" ", new SpacerSpan(V.dp(8), 0), 0); item.setTitle(ssb); } } diff --git a/mastodon/src/main/res/drawable/quote.xml b/mastodon/src/main/res/drawable/quote.xml new file mode 100644 index 00000000..2ec877ee --- /dev/null +++ b/mastodon/src/main/res/drawable/quote.xml @@ -0,0 +1,10 @@ + + +