Add smartness to SmartLengthInputFilter such as word trimming and runway

This commit is contained in:
HellPie 2018-09-05 22:38:36 +02:00 committed by HellPie
parent c62b523152
commit 0d83ff9c5e
4 changed files with 101 additions and 49 deletions

View File

@ -510,7 +510,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentCollapseButton.setVisibility(View.VISIBLE); contentCollapseButton.setVisibility(View.VISIBLE);
if(statusViewData.isCollapsed()) { if(statusViewData.isCollapsed()) {
contentCollapseButton.setChecked(true); contentCollapseButton.setChecked(true);
statusContent.setFilters(new InputFilter[]{new SmartLengthInputFilter(500)}); statusContent.setFilters(new InputFilter[]{
new SmartLengthInputFilter(SmartLengthInputFilter.LENGTH_DEFAULT)
});
} else { } else {
contentCollapseButton.setChecked(false); contentCollapseButton.setChecked(false);
statusContent.setFilters(new InputFilter[]{}); statusContent.setFilters(new InputFilter[]{});

View File

@ -503,23 +503,28 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setSpoilerText(status.getSpoilerText(), status.getStatusEmojis(), status.isExpanded(), listener); setSpoilerText(status.getSpoilerText(), status.getStatusEmojis(), status.isExpanded(), listener);
} }
if(status.isCollapsible() && (status.isExpanded() || status.getSpoilerText() == null || status.getSpoilerText().isEmpty())) { if(contentCollapseButton != null) {
contentCollapseButton.setOnCheckedChangeListener((buttonView, isChecked) -> { if(status.isCollapsible() && (status.isExpanded() || status.getSpoilerText() == null || status.getSpoilerText().isEmpty())) {
int position = getAdapterPosition(); contentCollapseButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(position != RecyclerView.NO_POSITION) listener.onContentCollapsedChange(isChecked, position); int position = getAdapterPosition();
}); if(position != RecyclerView.NO_POSITION)
listener.onContentCollapsedChange(isChecked, position);
});
contentCollapseButton.setVisibility(View.VISIBLE); contentCollapseButton.setVisibility(View.VISIBLE);
if(status.isCollapsed()) { if(status.isCollapsed()) {
contentCollapseButton.setChecked(true); contentCollapseButton.setChecked(true);
content.setFilters(new InputFilter[] {new SmartLengthInputFilter(500)}); content.setFilters(new InputFilter[]{
new SmartLengthInputFilter(SmartLengthInputFilter.LENGTH_DEFAULT)
});
} else {
contentCollapseButton.setChecked(false);
content.setFilters(new InputFilter[]{});
}
} else { } else {
contentCollapseButton.setChecked(false); contentCollapseButton.setVisibility(View.GONE);
content.setFilters(new InputFilter[] {}); content.setFilters(new InputFilter[]{});
} }
} else {
contentCollapseButton.setVisibility(View.GONE);
content.setFilters(new InputFilter[] {});
} }
setContent(status.getContent(), status.getMentions(), status.getStatusEmojis(), listener); setContent(status.getContent(), status.getMentions(), status.getStatusEmojis(), listener);

View File

@ -19,58 +19,103 @@ package com.keylesspalace.tusky.util;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned; import android.text.Spanned;
import java.text.BreakIterator;
/**
* A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter
* constraints and adds better visuals such as:
* <ul>
* <li>Ellipsis at the end of the constrained text to show continuation.</li>
* <li>Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.</li>
* <li>Constraints end at the end of the last "word", before a whitespace.</li>
* <li>Expansion of the limit by up to 10 characters to facilitate the previous constraint.</li>
* <li>Constraints are not applied if the percentage of hidden content is too small.</li>
* </ul>
*
* Some of these features are configurable through at instancing time.
*/
public class SmartLengthInputFilter implements InputFilter { public class SmartLengthInputFilter implements InputFilter {
private final int max; /**
* Default for maximum status length on Mastodon and default collapsing
* length on Pleroma.
*/
public static final int LENGTH_DEFAULT = 50;
private final int max;
private final boolean allowRunway;
private final boolean skipIfBadRatio;
/**
* Creates a new {@link SmartLengthInputFilter} instance with a predefined maximum length and
* all the smart constraint features this class supports.
*
* @param max The maximum length before trimming. May change based on other constraints.
*/
public SmartLengthInputFilter(int max) { public SmartLengthInputFilter(int max) {
this.max = max; this(max, true, true);
} }
/** /**
* This method is called when the buffer is going to replace the * Fully configures a new {@link SmartLengthInputFilter} to fine tune the state of the
* range <code>dstart &hellip; dend</code> of <code>dest</code> * supported smart constraints this class supports.
* with the new text from the range <code>start &hellip; end</code>
* of <code>source</code>. Return the CharSequence that you would
* like to have placed there instead, including an empty string
* if appropriate, or <code>null</code> to accept the original
* replacement. Be careful to not to reject 0-length replacements,
* as this is what happens when you delete text. Also beware that
* you should not attempt to make any changes to <code>dest</code>
* from this method; you may only examine it for context.
* <p>
* Note: If <var>source</var> is an instance of {@link Spanned} or
* {@link Spannable}, the span objects in the <var>source</var> should be
* copied into the filtered result (i.e. the non-null return value).
* {@link TextUtils#copySpansFrom} can be used for convenience if the
* span boundary indices would be remaining identical relative to the source.
* *
* @param source * @param max The maximum length before trimming.
* @param start * @param allowRunway Whether to extend {@param max} by an extra 10 characters
* @param end * and trim precisely at the end of the closest word.
* @param dest * @param skipIfBadRatio Whether to skip trimming entirely if the trimmed content
* @param dstart * will be less than 25% of the shown content.
* @param dend
*/ */
public SmartLengthInputFilter(int max, boolean allowRunway, boolean skipIfBadRatio) {
this.max = max;
this.allowRunway = allowRunway;
this.skipIfBadRatio = skipIfBadRatio;
}
/** {@inheritDoc} */
@Override @Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
// Code imported from InputFilter.LengthFilter // Code originally imported from InputFilter.LengthFilter but heavily customized.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
// Changes: int sourceLength = source.length();
// - After the text it adds and ellipsis to make it feel like the text continues
// - Trim invisible characters off the end of the already filtered string
// - Slimmed code for saving LOCs
int keep = max - (dest.length() - (dend - dstart)); int keep = max - (dest.length() - (dend - dstart));
if(keep <= 0) return ""; if(keep <= 0) return "";
if(keep >= end - start) return null; // keep original if(keep >= end - start) return null; // keep original
keep += start; keep += start;
while(Character.isWhitespace(source.charAt(keep - 1))) { // Enable skipping trimming if the ratio is not good enough
--keep; if(skipIfBadRatio && (double)keep / sourceLength > 0.75)
if(keep == start) return ""; return null;
// Enable trimming at the end of the closest word if possible
if(allowRunway && Character.isLetterOrDigit(source.charAt(keep))) {
int boundary;
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and
// unicode support. Using the ICU version of BreakIterator grants better support for
// those without having to add the ICU4J library at a minimum Api trade-off.
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
android.icu.text.BreakIterator iterator = android.icu.text.BreakIterator.getWordInstance();
iterator.setText(source.toString());
boundary = iterator.following(keep);
if(keep - boundary > 10) boundary = iterator.preceding(keep);
} else {
java.text.BreakIterator iterator = BreakIterator.getWordInstance();
iterator.setText(source.toString());
boundary = iterator.following(keep);
if(keep - boundary > 10) boundary = iterator.preceding(keep);
}
keep = boundary;
} else {
// If no runway is allowed simply remove whitespaces if present
while(Character.isWhitespace(source.charAt(keep - 1))) {
--keep;
if(keep == start) return "";
}
} }
if(Character.isHighSurrogate(source.charAt(keep - 1))) { if(Character.isHighSurrogate(source.charAt(keep - 1))) {

View File

@ -59,7 +59,7 @@ public final class ViewDataUtils {
.setApplication(visibleStatus.getApplication()) .setApplication(visibleStatus.getApplication())
.setStatusEmojis(visibleStatus.getEmojis()) .setStatusEmojis(visibleStatus.getEmojis())
.setAccountEmojis(visibleStatus.getAccount().getEmojis()) .setAccountEmojis(visibleStatus.getAccount().getEmojis())
.setCollapsible(collapseLongStatusContent && status.getContent().length() > 500) .setCollapsible(collapseLongStatusContent && status.getContent().length() > SmartLengthInputFilter.LENGTH_DEFAULT)
.setCollapsed(true) .setCollapsed(true)
.createStatusViewData(); .createStatusViewData();
} }