diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java index 8e935e37..df7bbd02 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java @@ -21,9 +21,7 @@ package org.floens.chan.chan; import android.graphics.Typeface; import android.text.SpannableString; import android.text.TextUtils; -import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; @@ -34,6 +32,8 @@ import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.ui.span.AbsoluteSizeSpanHashed; +import org.floens.chan.ui.span.ForegroundColorSpanHashed; import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.utils.Logger; @@ -129,19 +129,19 @@ public class ChanParser { post.subjectSpan = new SpannableString(post.subject); // Do not set another color when the post is in stub mode, it sets text_color_secondary if (!post.filterStub) { - post.subjectSpan.setSpan(new ForegroundColorSpan(theme.subjectColor), 0, post.subjectSpan.length(), 0); + post.subjectSpan.setSpan(new ForegroundColorSpanHashed(theme.subjectColor), 0, post.subjectSpan.length(), 0); } } if (!TextUtils.isEmpty(post.name) && !post.name.equals("Anonymous")) { post.nameSpan = new SpannableString(post.name); - post.nameSpan.setSpan(new ForegroundColorSpan(theme.nameColor), 0, post.nameSpan.length(), 0); + post.nameSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.nameSpan.length(), 0); } if (!TextUtils.isEmpty(post.tripcode)) { post.tripcodeSpan = new SpannableString(post.tripcode); - post.tripcodeSpan.setSpan(new ForegroundColorSpan(theme.nameColor), 0, post.tripcodeSpan.length(), 0); - post.tripcodeSpan.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, post.tripcodeSpan.length(), 0); + post.tripcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.tripcodeSpan.length(), 0); + post.tripcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.tripcodeSpan.length(), 0); } if (!TextUtils.isEmpty(post.id)) { @@ -158,15 +158,15 @@ public class ChanParser { boolean lightColor = (r * 0.299f) + (g * 0.587f) + (b * 0.114f) > 125f; int idBgColor = lightColor ? theme.idBackgroundLight : theme.idBackgroundDark; - post.idSpan.setSpan(new ForegroundColorSpan(idColor), 0, post.idSpan.length(), 0); + post.idSpan.setSpan(new ForegroundColorSpanHashed(idColor), 0, post.idSpan.length(), 0); post.idSpan.setSpan(new BackgroundColorSpan(idBgColor), 0, post.idSpan.length(), 0); - post.idSpan.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, post.idSpan.length(), 0); + post.idSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.idSpan.length(), 0); } if (!TextUtils.isEmpty(post.capcode)) { post.capcodeSpan = new SpannableString("Capcode: " + post.capcode); - post.capcodeSpan.setSpan(new ForegroundColorSpan(theme.capcodeColor), 0, post.capcodeSpan.length(), 0); - post.capcodeSpan.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, post.capcodeSpan.length(), 0); + post.capcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.capcodeColor), 0, post.capcodeSpan.length(), 0); + post.capcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.capcodeSpan.length(), 0); } post.nameTripcodeIdCapcodeSpan = new SpannableString(""); @@ -234,7 +234,7 @@ public class ChanParser { Set classes = span.classNames(); if (classes.contains("deadlink")) { quote = new SpannableString(span.text()); - quote.setSpan(new ForegroundColorSpan(theme.quoteColor), 0, quote.length(), 0); + quote.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, quote.length(), 0); quote.setSpan(new StrikethroughSpan(), 0, quote.length(), 0); } else if (classes.contains("fortune")) { // html looks like

Your fortune: @@ -260,7 +260,7 @@ public class ChanParser { } if (hexColor >= 0 && hexColor <= 0xffffff) { - quote.setSpan(new ForegroundColorSpan(0xff000000 + hexColor), 0, quote.length(), 0); + quote.setSpan(new ForegroundColorSpanHashed(0xff000000 + hexColor), 0, quote.length(), 0); quote.setSpan(new StyleSpan(Typeface.BOLD), 0, quote.length(), 0); } } @@ -268,7 +268,7 @@ public class ChanParser { return null; } else { quote = new SpannableString(span.text()); - quote.setSpan(new ForegroundColorSpan(theme.inlineQuoteColor), 0, quote.length(), 0); + quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0); detectLinks(theme, post, span.text(), quote); } @@ -306,8 +306,8 @@ public class ChanParser { } SpannableString tableTotal = new SpannableString(TextUtils.concat(parts.toArray(new CharSequence[parts.size()]))); - tableTotal.setSpan(new ForegroundColorSpan(theme.inlineQuoteColor), 0, tableTotal.length(), 0); - tableTotal.setSpan(new AbsoluteSizeSpan(sp(12f)), 0, tableTotal.length(), 0); + tableTotal.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, tableTotal.length(), 0); + tableTotal.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, tableTotal.length(), 0); return tableTotal; } @@ -315,7 +315,7 @@ public class ChanParser { Element strong = (Element) node; SpannableString red = new SpannableString(strong.text()); - red.setSpan(new ForegroundColorSpan(theme.quoteColor), 0, red.length(), 0); + red.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, red.length(), 0); red.setSpan(new StyleSpan(Typeface.BOLD), 0, red.length(), 0); return red; @@ -347,7 +347,7 @@ public class ChanParser { String text = getNodeText(pre); SpannableString monospace = new SpannableString(text); monospace.setSpan(new TypefaceSpan("monospace"), 0, monospace.length(), 0); - monospace.setSpan(new AbsoluteSizeSpan(sp(12f)), 0, monospace.length(), 0); + monospace.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, monospace.length(), 0); return monospace; } else { return pre.text(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java index 4cae2b0c..b10d07ac 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java @@ -37,10 +37,8 @@ import android.text.TextPaint; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.method.LinkMovementMethod; -import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ClickableSpan; -import android.text.style.ForegroundColorSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.view.MotionEvent; @@ -59,6 +57,8 @@ import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.ui.span.AbsoluteSizeSpanHashed; +import org.floens.chan.ui.span.ForegroundColorSpanHashed; import org.floens.chan.ui.text.FastTextView; import org.floens.chan.ui.text.FastTextViewMovementMethod; import org.floens.chan.ui.theme.Theme; @@ -355,8 +355,8 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin String noText = "No." + post.no; SpannableString date = new SpannableString(noText + " " + time); - date.setSpan(new ForegroundColorSpan(theme.detailsColor), 0, date.length(), 0); - date.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, date.length(), 0); + date.setSpan(new ForegroundColorSpanHashed(theme.detailsColor), 0, date.length(), 0); + date.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, date.length(), 0); boolean noClickable = ChanSettings.tapNoReply.get(); if (noClickable) { @@ -372,8 +372,8 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin if (postFileName) { String filename = image.spoiler ? getString(R.string.image_spoiler_filename) : image.filename + "." + image.extension; SpannableString fileInfo = new SpannableString("\n" + filename); - fileInfo.setSpan(new ForegroundColorSpan(theme.detailsColor), 0, fileInfo.length(), 0); - fileInfo.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, fileInfo.length(), 0); + fileInfo.setSpan(new ForegroundColorSpanHashed(theme.detailsColor), 0, fileInfo.length(), 0); + fileInfo.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, fileInfo.length(), 0); fileInfo.setSpan(new UnderlineSpan(), 0, fileInfo.length(), 0); titleParts.add(fileInfo); } @@ -382,8 +382,8 @@ public class PostCell extends LinearLayout implements PostCellInterface, PostLin SpannableString fileInfo = new SpannableString((postFileName ? " " : "\n") + image.extension.toUpperCase() + " " + AndroidUtils.getReadableFileSize(image.size, false) + " " + image.imageWidth + "x" + image.imageHeight); - fileInfo.setSpan(new ForegroundColorSpan(theme.detailsColor), 0, fileInfo.length(), 0); - fileInfo.setSpan(new AbsoluteSizeSpan(detailsSizePx), 0, fileInfo.length(), 0); + fileInfo.setSpan(new ForegroundColorSpanHashed(theme.detailsColor), 0, fileInfo.length(), 0); + fileInfo.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, fileInfo.length(), 0); titleParts.add(fileInfo); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/span/AbsoluteSizeSpanHashed.java b/Clover/app/src/main/java/org/floens/chan/ui/span/AbsoluteSizeSpanHashed.java new file mode 100644 index 00000000..c99481a0 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/span/AbsoluteSizeSpanHashed.java @@ -0,0 +1,39 @@ +package org.floens.chan.ui.span; + +import android.os.Parcel; +import android.text.style.AbsoluteSizeSpan; + +/** + * A version of AbsoluteSizeSpan that has proper equals and hashCode implementations. Used to fix the hashcode result from SpannableStringBuilder. + */ +public class AbsoluteSizeSpanHashed extends AbsoluteSizeSpan { + public AbsoluteSizeSpanHashed(int size) { + super(size); + } + + public AbsoluteSizeSpanHashed(int size, boolean dip) { + super(size, dip); + } + + public AbsoluteSizeSpanHashed(Parcel src) { + super(src); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbsoluteSizeSpanHashed that = (AbsoluteSizeSpanHashed) o; + + if (getSize() != that.getSize()) return false; + return getDip() == that.getDip(); + } + + @Override + public int hashCode() { + int result = getSize(); + result = 31 * result + (getDip() ? 1 : 0); + return result; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/span/ForegroundColorSpanHashed.java b/Clover/app/src/main/java/org/floens/chan/ui/span/ForegroundColorSpanHashed.java new file mode 100644 index 00000000..25be65a6 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/span/ForegroundColorSpanHashed.java @@ -0,0 +1,33 @@ +package org.floens.chan.ui.span; + +import android.os.Parcel; +import android.text.style.ForegroundColorSpan; + +/** + * A version of ForegroundColorSpan that has proper equals and hashCode implementations. Used to fix the hashcode result from SpannableStringBuilder. + */ +public class ForegroundColorSpanHashed extends ForegroundColorSpan { + public ForegroundColorSpanHashed(int color) { + super(color); + } + + public ForegroundColorSpanHashed(Parcel src) { + super(src); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ForegroundColorSpanHashed that = (ForegroundColorSpanHashed) o; + + return getForegroundColor() == that.getForegroundColor(); + + } + + @Override + public int hashCode() { + return getForegroundColor(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java index 1d9e865a..91e3eebd 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/text/FastTextView.java @@ -38,11 +38,11 @@ import static org.floens.chan.utils.AndroidUtils.sp; /** * A simple implementation of a TextView that caches the used StaticLayouts for performance.
- * This view was made for {@link org.floens.chan.ui.cell.PostCell} and {@link org.floens.chan.ui.cell.CardPostCell }and may have untested behaviour with other layouts. + * This view was made for {@link org.floens.chan.ui.cell.PostCell} and {@link org.floens.chan.ui.cell.CardPostCell} and may have untested behaviour with other layouts. */ public class FastTextView extends View { private static final String TAG = "FastTextView"; - private static LruCache textCache = new LruCache<>(250); + private static LruCache textCache = new LruCache<>(250); private TextPaint paint; private boolean singleLine; @@ -215,31 +215,16 @@ public class FastTextView extends View { // long start = Time.startTiming(); // The StaticLayouts are cached with the static textCache LRU map + FastTextViewItem item = new FastTextViewItem(text, paint, layoutWidth); - // Use .toString() to make sure we take the hashcode from the string representation - // and not from any spannables that are in it - long cacheKey = text.toString().hashCode(); - cacheKey = 31 * cacheKey + paint.getColor(); - cacheKey = 31 * cacheKey + Float.floatToIntBits(paint.getTextSize()); - cacheKey = 31 * cacheKey + layoutWidth; - - StaticLayout cached = textCache.get(cacheKey); + StaticLayout cached = textCache.get(item); if (cached == null) { // Logger.test("staticlayout cache miss: text = %s", text); cached = getStaticLayout(layoutWidth); - textCache.put(cacheKey, cached); - } else { -// Logger.test("staticlayout cache hit"); - // Make sure the layout has the actual text, color, size and width, hashcodes almost never collide - Paint cachedPaint = cached.getPaint(); - if (!text.toString().equals(cached.getText().toString()) || - cachedPaint.getColor() != paint.getColor() || - cachedPaint.getTextSize() != paint.getTextSize() || - cached.getWidth() != layoutWidth) { - Logger.w(TAG, "Cache miss with the same hashcode %x: \"%s\" \"%s\"!", cacheKey, text.toString(), cached.getText().toString()); - cached = getStaticLayout(layoutWidth); - } - } + textCache.put(item, cached); + }/* else { + Logger.test("staticlayout cache hit"); + }*/ layout = cached; // Time.endTiming(Integer.toHexString(System.identityHashCode(this)) + " staticlayout for width = " + layoutWidth + "\t", start); @@ -257,4 +242,40 @@ public class FastTextView extends View { // Logger.test("new staticlayout width=%d", layoutWidth); return new StaticLayout(text, paint, layoutWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false); } + + private static class FastTextViewItem { + private CharSequence text; + private int color; + private float textSize; + private int layoutWidth; + + public FastTextViewItem(CharSequence text, TextPaint textPaint, int layoutWidth) { + this.text = text; + color = textPaint.getColor(); + textSize = textPaint.getTextSize(); + this.layoutWidth = layoutWidth; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FastTextViewItem that = (FastTextViewItem) o; + + if (color != that.color) return false; + if (Float.compare(that.textSize, textSize) != 0) return false; + if (layoutWidth != that.layoutWidth) return false; + return text.equals(that.text); + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + color; + result = 31 * result + (textSize != +0.0f ? Float.floatToIntBits(textSize) : 0); + result = 31 * result + layoutWidth; + return result; + } + } }