Ellipsize playlist description if it is longer than 5 lines
The description can be expanded / collapsed via a "show more" / "show less" button.
This commit is contained in:
parent
6c99557553
commit
65eb631711
5 changed files with 249 additions and 118 deletions
|
|
@ -3,7 +3,7 @@ package org.schabi.newpipe.fragments.list.playlist;
|
|||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -19,7 +19,6 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
|
|
@ -52,10 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
|||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -329,13 +328,24 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||
final Description description = result.getDescription();
|
||||
if (description != null && description != Description.EMPTY_DESCRIPTION
|
||||
&& !isBlank(description.getContent())) {
|
||||
TextLinkifier.fromDescription(headerBinding.playlistDescription,
|
||||
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
result.getService(), result.getUrl(),
|
||||
disposables, SET_LINK_MOVEMENT_METHOD);
|
||||
headerBinding.playlistDescription.setVisibility(View.VISIBLE);
|
||||
final TextEllipsizer ellipsizer = new TextEllipsizer(
|
||||
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
|
||||
ellipsizer.setStateChangeListener(isEllipsized ->
|
||||
headerBinding.playlistDescriptionReadMore.setText(
|
||||
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
|
||||
));
|
||||
ellipsizer.setOnContentChanged(canBeEllipsized -> {
|
||||
headerBinding.playlistDescriptionReadMore.setVisibility(
|
||||
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
|
||||
if (Boolean.TRUE.equals(canBeEllipsized)) {
|
||||
ellipsizer.ellipsize();
|
||||
}
|
||||
});
|
||||
ellipsizer.setContent(description);
|
||||
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
|
||||
} else {
|
||||
headerBinding.playlistDescription.setVisibility(View.GONE);
|
||||
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.text.Layout;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
|
|
@ -15,42 +12,28 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||
|
||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||
private static final String ELLIPSIS = "…";
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private final Paint paintAtContentSize;
|
||||
private final float ellipsisWidthPx;
|
||||
|
||||
private final RelativeLayout itemRoot;
|
||||
private final ImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
|
|
@ -61,13 +44,8 @@ public class CommentInfoItemHolder extends InfoItemHolder {
|
|||
private final ImageView itemPinnedView;
|
||||
private final Button repliesButton;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@Nullable
|
||||
private Description commentText;
|
||||
@Nullable
|
||||
private StreamingService streamService;
|
||||
@Nullable
|
||||
private String streamUrl;
|
||||
@NonNull
|
||||
private final TextEllipsizer textEllipsizer;
|
||||
|
||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||
final ViewGroup parent) {
|
||||
|
|
@ -88,9 +66,14 @@ public class CommentInfoItemHolder extends InfoItemHolder {
|
|||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
||||
|
||||
paintAtContentSize = new Paint();
|
||||
paintAtContentSize.setTextSize(itemContentView.getTextSize());
|
||||
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
||||
denyLinkFocus();
|
||||
} else {
|
||||
determineMovementMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -139,16 +122,16 @@ public class CommentInfoItemHolder extends InfoItemHolder {
|
|||
|
||||
|
||||
// setup comment content and click listeners to expand/ellipsize it
|
||||
streamService = getServiceById(item.getServiceId());
|
||||
streamUrl = item.getUrl();
|
||||
commentText = item.getCommentText();
|
||||
ellipsize();
|
||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
||||
textEllipsizer.setStreamUrl(item.getUrl());
|
||||
textEllipsizer.setContent(item.getCommentText());
|
||||
textEllipsizer.ellipsize();
|
||||
|
||||
//noinspection ClickableViewAccessibility
|
||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
toggleEllipsize();
|
||||
textEllipsizer.toggle();
|
||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||
}
|
||||
|
|
@ -202,76 +185,4 @@ public class CommentInfoItemHolder extends InfoItemHolder {
|
|||
denyLinkFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private void ellipsize() {
|
||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
||||
linkifyCommentContentView(v -> {
|
||||
boolean hasEllipsis = false;
|
||||
|
||||
final CharSequence charSeqText = itemContentView.getText();
|
||||
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
// Note that converting to String removes spans (i.e. links), but that's something
|
||||
// we actually want since when the text is ellipsized we want all clicks on the
|
||||
// comment to expand the comment, not to open links.
|
||||
final String text = charSeqText.toString();
|
||||
|
||||
final Layout layout = itemContentView.getLayout();
|
||||
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
|
||||
final float layoutWidth = layout.getWidth();
|
||||
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
|
||||
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||
|
||||
// remove characters up until there is enough space for the ellipsis
|
||||
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
|
||||
int end = lineEnd;
|
||||
float removedCharactersWidth = 0.0f;
|
||||
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
|
||||
&& end >= lineStart) {
|
||||
end -= 1;
|
||||
// recalculate each time to account for ligatures or other similar things
|
||||
removedCharactersWidth = paintAtContentSize.measureText(
|
||||
text.substring(end, lineEnd));
|
||||
}
|
||||
|
||||
// remove trailing spaces and newlines
|
||||
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
final String newVal = text.substring(0, end) + ELLIPSIS;
|
||||
itemContentView.setText(newVal);
|
||||
hasEllipsis = true;
|
||||
}
|
||||
|
||||
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
|
||||
if (hasEllipsis) {
|
||||
denyLinkFocus();
|
||||
} else {
|
||||
determineMovementMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleEllipsize() {
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
||||
expand();
|
||||
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
ellipsize();
|
||||
}
|
||||
}
|
||||
|
||||
private void expand() {
|
||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
||||
linkifyCommentContentView(v -> determineMovementMethod());
|
||||
}
|
||||
|
||||
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
|
||||
disposables.clear();
|
||||
if (commentText != null) {
|
||||
TextLinkifier.fromDescription(itemContentView, commentText,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
|
||||
onCompletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
package org.schabi.newpipe.util.text;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.text.Layout;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
/**
|
||||
* <p>Class to ellipsize text inside a {@link TextView}.</p>
|
||||
* This class provides all utils to automatically ellipsize and expand a text
|
||||
*/
|
||||
public final class TextEllipsizer {
|
||||
private static final int EXPANDED_LINES = Integer.MAX_VALUE;
|
||||
private static final String ELLIPSIS = "…";
|
||||
|
||||
@NonNull private final CompositeDisposable disposable = new CompositeDisposable();
|
||||
|
||||
@NonNull private final TextView view;
|
||||
private final int maxLines;
|
||||
@NonNull private Description content;
|
||||
@Nullable private StreamingService streamingService;
|
||||
@Nullable private String streamUrl;
|
||||
private boolean isEllipsized = false;
|
||||
@Nullable private Boolean caBeEllipsized = null;
|
||||
|
||||
@NonNull private final Paint paintAtContentSize = new Paint();
|
||||
private final float ellipsisWidthPx;
|
||||
@Nullable private Consumer<Boolean> stateChangeListener = null;
|
||||
@Nullable private Consumer<Boolean> onContentChanged;
|
||||
|
||||
public TextEllipsizer(@NonNull final TextView view,
|
||||
final int maxLines,
|
||||
@Nullable final StreamingService streamingService) {
|
||||
this.view = view;
|
||||
this.maxLines = maxLines;
|
||||
this.streamingService = streamingService;
|
||||
|
||||
paintAtContentSize.setTextSize(view.getTextSize());
|
||||
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
||||
}
|
||||
|
||||
public void setOnContentChanged(@Nullable final Consumer<Boolean> onContentChanged) {
|
||||
this.onContentChanged = onContentChanged;
|
||||
}
|
||||
|
||||
public void setContent(@NonNull final Description content) {
|
||||
this.content = content;
|
||||
caBeEllipsized = null;
|
||||
linkifyContentView(v -> {
|
||||
final int currentMaxLines = view.getMaxLines();
|
||||
view.setMaxLines(EXPANDED_LINES);
|
||||
caBeEllipsized = view.getLineCount() > maxLines;
|
||||
view.setMaxLines(currentMaxLines);
|
||||
if (onContentChanged != null) {
|
||||
onContentChanged.accept(caBeEllipsized);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setStreamUrl(@Nullable final String streamUrl) {
|
||||
this.streamUrl = streamUrl;
|
||||
}
|
||||
|
||||
public void setStreamingService(@NonNull final StreamingService streamingService) {
|
||||
this.streamingService = streamingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the {@link TextEllipsizer#content} to its full length.
|
||||
*/
|
||||
public void expand() {
|
||||
view.setMaxLines(EXPANDED_LINES);
|
||||
linkifyContentView(v -> isEllipsized = false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten the {@link TextEllipsizer#content} to the given number of
|
||||
* {@link TextEllipsizer#maxLines maximum lines} and add trailing '{@code …}'
|
||||
* if the text was shorted.
|
||||
*/
|
||||
public void ellipsize() {
|
||||
// expand text to see whether it is necessary to ellipsize the text
|
||||
view.setMaxLines(EXPANDED_LINES);
|
||||
linkifyContentView(v -> {
|
||||
final CharSequence charSeqText = view.getText();
|
||||
if (charSeqText != null && view.getLineCount() > maxLines) {
|
||||
// Note that converting to String removes spans (i.e. links), but that's something
|
||||
// we actually want since when the text is ellipsized we want all clicks on the
|
||||
// comment to expand the comment, not to open links.
|
||||
final String text = charSeqText.toString();
|
||||
|
||||
final Layout layout = view.getLayout();
|
||||
final float lineWidth = layout.getLineWidth(maxLines - 1);
|
||||
final float layoutWidth = layout.getWidth();
|
||||
final int lineStart = layout.getLineStart(maxLines - 1);
|
||||
final int lineEnd = layout.getLineEnd(maxLines - 1);
|
||||
|
||||
// remove characters up until there is enough space for the ellipsis
|
||||
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
|
||||
int end = lineEnd;
|
||||
float removedCharactersWidth = 0.0f;
|
||||
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
|
||||
&& end >= lineStart) {
|
||||
end -= 1;
|
||||
// recalculate each time to account for ligatures or other similar things
|
||||
removedCharactersWidth = paintAtContentSize.measureText(
|
||||
text.substring(end, lineEnd));
|
||||
}
|
||||
|
||||
// remove trailing spaces and newlines
|
||||
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
final String newVal = text.substring(0, end) + ELLIPSIS;
|
||||
view.setText(newVal);
|
||||
isEllipsized = true;
|
||||
} else {
|
||||
isEllipsized = false;
|
||||
}
|
||||
view.setMaxLines(maxLines);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the view between the ellipsed and expanded state.
|
||||
*/
|
||||
public void toggle() {
|
||||
if (isEllipsized) {
|
||||
expand();
|
||||
} else {
|
||||
ellipsize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the {@link view} can be ellipsized.
|
||||
* This is only the case when the {@link content} has more lines
|
||||
* than allowed via {@link maxLines}.
|
||||
* @return {@code true} if the {@link content} has more lines than allowed via {@link maxLines}
|
||||
* and thus can be shortened, {@code false} if the {@code content} fits into the {@link view}
|
||||
* without being shortened and {@code null} if the initialization is not completed yet.
|
||||
*/
|
||||
@Nullable
|
||||
public Boolean canBeEllipsized() {
|
||||
return caBeEllipsized;
|
||||
}
|
||||
|
||||
private void linkifyContentView(final Consumer<View> consumer) {
|
||||
final boolean oldState = isEllipsized;
|
||||
disposable.clear();
|
||||
TextLinkifier.fromDescription(view, content,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY, streamingService, streamUrl, disposable,
|
||||
v -> {
|
||||
consumer.accept(v);
|
||||
notifyStateChangeListener(oldState);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener which is called when the given content is changed,
|
||||
* either from <em>ellipsized</em> to <em>full</em> or vice versa.
|
||||
* @param listener The listener to be called.
|
||||
* The Boolean parameter is the new state.
|
||||
* <em>Ellipsized</em> content is represented as {@code true},
|
||||
* normal or <em>full</em> content by {@code false}.
|
||||
*/
|
||||
public void setStateChangeListener(final Consumer<Boolean> listener) {
|
||||
this.stateChangeListener = listener;
|
||||
}
|
||||
|
||||
public void removeStateChangeListener() {
|
||||
this.stateChangeListener = null;
|
||||
}
|
||||
|
||||
private void notifyStateChangeListener(final boolean oldState) {
|
||||
if (oldState != isEllipsized && stateChangeListener != null) {
|
||||
stateChangeListener.accept(isEllipsized);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue