Skip to content

Commit 569ad54

Browse files
committed
Add BulletCommentsView overlay component
1 parent 7b98b40 commit 569ad54

3 files changed

Lines changed: 441 additions & 0 deletions

File tree

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
package org.schabi.newpipe.views;
2+
3+
import android.animation.Animator;
4+
import android.animation.AnimatorListenerAdapter;
5+
import android.animation.ObjectAnimator;
6+
import android.content.Context;
7+
import android.content.SharedPreferences;
8+
import android.graphics.Color;
9+
import android.graphics.Paint;
10+
import android.graphics.Typeface;
11+
import android.os.Build;
12+
import android.util.AttributeSet;
13+
import android.util.Log;
14+
import android.util.TypedValue;
15+
import android.view.LayoutInflater;
16+
import android.view.View;
17+
import android.view.Gravity;
18+
import android.view.animation.LinearInterpolator;
19+
import android.widget.TextView;
20+
21+
import androidx.annotation.NonNull;
22+
import androidx.constraintlayout.widget.ConstraintLayout;
23+
24+
import androidx.preference.PreferenceManager;
25+
import org.schabi.newpipe.R;
26+
import org.schabi.newpipe.databinding.BulletCommentsPlayerBinding;
27+
import org.schabi.newpipe.extractor.bulletComments.BulletCommentsInfoItem;
28+
29+
import java.time.Duration;
30+
import java.util.AbstractMap;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.Date;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.PriorityQueue;
38+
import java.util.stream.Collectors;
39+
40+
public final class BulletCommentsView extends ConstraintLayout {
41+
private final String TAG = "BulletCommentsView";
42+
private SharedPreferences prefs;
43+
44+
/**
45+
* Tuple of TextView and ObjectAnimator.
46+
*/
47+
private static class AnimatedTextView {
48+
AnimatedTextView(final TextView textView, final ObjectAnimator animator) {
49+
this.textView = textView;
50+
this.animator = animator;
51+
}
52+
53+
public final TextView textView;
54+
public final ObjectAnimator animator;
55+
}
56+
57+
public BulletCommentsView(final Context context) {
58+
super(context);
59+
setClipChildren(false);
60+
init(context);
61+
}
62+
63+
public BulletCommentsView(final Context context,
64+
final AttributeSet attrs) {
65+
super(context, attrs);
66+
setClipChildren(false);
67+
init(context);
68+
}
69+
70+
public BulletCommentsView(final Context context,
71+
final AttributeSet attrs,
72+
final int defStyleAttr) {
73+
super(context, attrs, defStyleAttr);
74+
setClipChildren(false);
75+
init(context);
76+
}
77+
78+
private void init(final Context context) {
79+
final View layout = LayoutInflater.from(context)
80+
.inflate(R.layout.bullet_comments_player, this);
81+
prefs = PreferenceManager.getDefaultSharedPreferences(context);
82+
commentsDuration = prefs.getInt(
83+
context.getString(R.string.top_bottom_bullet_comments_duration_key), 8);
84+
durationFactor = (float) prefs.getInt(
85+
context.getString(R.string.regular_bullet_comments_duration_key), 8)
86+
/ (float) commentsDuration;
87+
outlineRadius = prefs.getInt(
88+
context.getString(R.string.bullet_comments_outline_radius_key), 2);
89+
90+
final boolean limitMaxRows = prefs.getBoolean(
91+
context.getString(R.string.enable_max_rows_customization_key), false);
92+
if (limitMaxRows) {
93+
maxRowsTop = prefs.getInt(
94+
context.getString(R.string.max_bullet_comments_rows_top_key), 15);
95+
maxRowsBottom = prefs.getInt(
96+
context.getString(R.string.max_bullet_comments_rows_bottom_key), 15);
97+
maxRowsRegular = prefs.getInt(
98+
context.getString(R.string.max_bullet_comments_rows_regular_key), 15);
99+
}
100+
101+
font = prefs.getString(context.getString(R.string.bullet_comments_font_key), "default");
102+
opacity = prefs.getInt(context.getString(R.string.bullet_comments_opacity_key), 0xFF);
103+
binding = BulletCommentsPlayerBinding.bind(this);
104+
}
105+
106+
private boolean layoutSet = false;
107+
108+
private void setLayout() {
109+
final int additionalWidth = additionalSpaceRelative * getWidth();
110+
binding.bottomRight.getLayoutParams().width = additionalWidth;
111+
requestLayout();
112+
Log.i(TAG, "Additional width: " + additionalWidth
113+
+ ", container width: " + binding.bulletCommentsContainer.getWidth());
114+
}
115+
116+
private BulletCommentsPlayerBinding binding;
117+
private final int additionalSpaceRelative = 4;
118+
119+
private final int commentsRowsCount = 11;
120+
private int lastCalculatedCommentsRowsCount = 11;
121+
private List<Long> rows = Collections.synchronizedList(new ArrayList<Long>());
122+
private List<Map.Entry<Long, Integer>> rowsRegular =
123+
Collections.synchronizedList(new ArrayList<>());
124+
private final double commentRelativeTextSize = 1 / 13.5;
125+
private PriorityQueue<BulletCommentsInfoItem> bulletCommentsInfoItemRegularPool =
126+
new PriorityQueue<>();
127+
private PriorityQueue<BulletCommentsInfoItem> bulletCommentsInfoItemFixedPool =
128+
new PriorityQueue<>();
129+
130+
private int commentsDuration;
131+
private float durationFactor;
132+
private int outlineRadius;
133+
private String font;
134+
private int opacity; // 0~255, 0: hide
135+
private final List<AnimatedTextView> animatedTextViews = new ArrayList<>();
136+
137+
private int maxRowsTop = 1000000;
138+
private int maxRowsBottom = 1000000;
139+
private int maxRowsRegular = 1000000;
140+
141+
public void clearComments() {
142+
Log.d(TAG, "clearComments() called, animatedViews=" + animatedTextViews.size());
143+
animatedTextViews.clear();
144+
if (binding != null) {
145+
binding.bulletCommentsContainer.removeAllViews();
146+
}
147+
}
148+
149+
public void setPauseComments(final boolean pause) {
150+
if (pause) {
151+
pauseComments();
152+
} else {
153+
resumeComments();
154+
}
155+
}
156+
157+
public void pauseComments() {
158+
animatedTextViews.stream().forEach(s -> s.animator.pause());
159+
}
160+
161+
public void resumeComments() {
162+
animatedTextViews.stream().forEach(s -> s.animator.resume());
163+
}
164+
165+
public void drawComments(@NonNull final BulletCommentsInfoItem[] items,
166+
final Duration drawUntilPosition) {
167+
Log.v(TAG, "drawComments() items=" + items.length
168+
+ " position=" + drawUntilPosition.toMillis() + "ms");
169+
if (binding == null || getWidth() == 0 || getHeight() == 0) {
170+
Log.w(TAG, "drawComments() skipped: view not ready");
171+
return;
172+
}
173+
if (!layoutSet) {
174+
setLayout();
175+
layoutSet = true;
176+
}
177+
bulletCommentsInfoItemRegularPool.addAll(
178+
Arrays.asList(items).stream()
179+
.filter(x -> x.getPosition() == BulletCommentsInfoItem.Position.REGULAR)
180+
.collect(Collectors.toList()));
181+
bulletCommentsInfoItemFixedPool.addAll(
182+
Arrays.asList(items).stream()
183+
.filter(x -> x.getPosition() != BulletCommentsInfoItem.Position.REGULAR)
184+
.collect(Collectors.toList()));
185+
final int height = getHeight();
186+
final int width = getWidth();
187+
final int calculatedCommentRowsCount =
188+
height / Math.min(height, width) * commentsRowsCount;
189+
if (calculatedCommentRowsCount != lastCalculatedCommentsRowsCount) {
190+
lastCalculatedCommentsRowsCount = calculatedCommentRowsCount;
191+
rows.clear();
192+
rowsRegular.clear();
193+
}
194+
while (rowsRegular.size() < calculatedCommentRowsCount) {
195+
rowsRegular.add(new AbstractMap.SimpleEntry<>(0L, 0));
196+
}
197+
while (rows.size() < calculatedCommentRowsCount) {
198+
rows.add(0L);
199+
}
200+
drawCommentsByPool(bulletCommentsInfoItemRegularPool, drawUntilPosition,
201+
height, width, calculatedCommentRowsCount);
202+
drawCommentsByPool(bulletCommentsInfoItemFixedPool, drawUntilPosition,
203+
height, width, calculatedCommentRowsCount);
204+
Log.v(TAG, "drawComments() done, containerChildCount="
205+
+ binding.bulletCommentsContainer.getChildCount());
206+
}
207+
208+
public int tryToDrawComment(final BulletCommentsInfoItem item,
209+
final int calculatedCommentRowsCount,
210+
final int width,
211+
final boolean reallyDo) {
212+
final long current = new Date().getTime();
213+
int row = -1;
214+
final int comparedDuration = (int) (commentsDuration * 1000);
215+
if (item.getPosition().equals(BulletCommentsInfoItem.Position.TOP)
216+
|| item.getPosition().equals(BulletCommentsInfoItem.Position.SUPERCHAT)) {
217+
for (int i = 0; i < Math.min(maxRowsTop, calculatedCommentRowsCount); i++) {
218+
final long last = rows.get(i);
219+
if (current - last >= comparedDuration) {
220+
if (reallyDo) {
221+
rows.set(i, current);
222+
}
223+
row = i;
224+
break;
225+
}
226+
}
227+
} else if (item.getPosition().equals(BulletCommentsInfoItem.Position.REGULAR)) {
228+
for (int i = 0; i < Math.min(maxRowsRegular, calculatedCommentRowsCount); i++) {
229+
final long lastTime = rowsRegular.get(i).getKey();
230+
final long lastLength = rowsRegular.get(i).getValue();
231+
final long t = current - lastTime;
232+
final double tAll = comparedDuration * durationFactor;
233+
final double lx = (lastLength / 25.0 + 1) * width;
234+
final double ly = (item.getCommentText().length() / 25.0 + 1) * width;
235+
final double vx = lx / tAll;
236+
final double vy = ly / tAll;
237+
if ((vy - vx) * (tAll - t) < t * vx - (lastLength / 25.0) * width
238+
&& t * vx - (lastLength / 25.0) * width > 0) {
239+
if (reallyDo) {
240+
rowsRegular.set(i,
241+
new AbstractMap.SimpleEntry<>(current,
242+
item.getCommentText().length()));
243+
}
244+
row = i;
245+
break;
246+
}
247+
}
248+
} else {
249+
for (int i = calculatedCommentRowsCount - 1;
250+
i >= Math.max(0, calculatedCommentRowsCount - maxRowsBottom); i--) {
251+
final long last = rows.get(i);
252+
if (current - last >= comparedDuration) {
253+
if (reallyDo) {
254+
rows.set(i, current);
255+
}
256+
row = i;
257+
break;
258+
}
259+
}
260+
}
261+
return row;
262+
}
263+
264+
private void drawCommentsByPool(final PriorityQueue<BulletCommentsInfoItem> pool,
265+
final Duration drawUntilPosition,
266+
final int height,
267+
final int width,
268+
final int calculatedCommentRowsCount) {
269+
if (binding == null) {
270+
return;
271+
}
272+
final Context context = binding.bulletCommentsContainer.getContext();
273+
int drawn = 0;
274+
while (!pool.isEmpty()
275+
&& (drawUntilPosition.compareTo(Duration.ofSeconds(Long.MAX_VALUE)) == 0
276+
|| pool.peek().getDuration().toMillis() < drawUntilPosition.toMillis())) {
277+
final BulletCommentsInfoItem item = pool.peek();
278+
if (item.isLive()
279+
&& tryToDrawComment(item, calculatedCommentRowsCount, width, false) == -1) {
280+
Log.v(TAG, "drawCommentsByPool() row collision, skipping item");
281+
pool.poll(); // skip this item instead of aborting all
282+
continue;
283+
}
284+
pool.poll();
285+
final TextView textView = new TextView(context);
286+
final Typeface fontToBeUsed;
287+
switch (font) {
288+
case "serif":
289+
fontToBeUsed = Typeface.SERIF;
290+
break;
291+
case "monospace":
292+
fontToBeUsed = Typeface.MONOSPACE;
293+
break;
294+
case "sans-serif":
295+
fontToBeUsed = Typeface.SANS_SERIF;
296+
break;
297+
default:
298+
fontToBeUsed = Typeface.DEFAULT;
299+
break;
300+
}
301+
textView.setGravity(Gravity.CENTER);
302+
int color = item.getArgbColor();
303+
if (opacity != 0xFF) {
304+
color &= 0x00FFFFFF;
305+
color |= ((opacity & 0xFF) << 24);
306+
}
307+
textView.setTextColor(color);
308+
final String commentText = item.getCommentText();
309+
Log.v(TAG, "drawCommentsByPool() text=[" + commentText + "] color="
310+
+ String.format("0x%08X", color) + " pos=" + item.getPosition());
311+
if (commentText.length() == 0) {
312+
Log.v(TAG, "drawCommentsByPool() skipping empty text");
313+
continue;
314+
}
315+
textView.setText(commentText);
316+
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
317+
(float) (Math.min(height, width) * commentRelativeTextSize
318+
* item.getRelativeFontSize()));
319+
textView.setMaxLines(1);
320+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
321+
textView.setTypeface(Typeface.create(fontToBeUsed, Typeface.BOLD,
322+
item.getPosition().equals(BulletCommentsInfoItem.Position.SUPERCHAT)));
323+
} else {
324+
textView.setTypeface(Typeface.create(fontToBeUsed, Typeface.BOLD));
325+
}
326+
final Paint paint = textView.getPaint();
327+
int shadowColor = Color.BLACK & 0x00FFFFFF;
328+
shadowColor |= ((opacity & 0xFF) << 24);
329+
paint.setShadowLayer(outlineRadius, 0, 0, shadowColor);
330+
textView.setLayerType(View.LAYER_TYPE_SOFTWARE, paint);
331+
332+
final int row = tryToDrawComment(item, calculatedCommentRowsCount, width, true);
333+
if (row == -1) {
334+
continue;
335+
}
336+
textView.setX(width);
337+
textView.post(() -> {
338+
final int textWidth = textView.getWidth();
339+
final int textHeight = textView.getHeight();
340+
final ObjectAnimator animator;
341+
if (!item.getPosition().equals(BulletCommentsInfoItem.Position.REGULAR)) {
342+
animator = ObjectAnimator.ofFloat(
343+
textView,
344+
View.TRANSLATION_X,
345+
(float) ((width - textWidth) / 2.0),
346+
(float) ((width - textWidth) / 2.0)
347+
);
348+
} else {
349+
animator = ObjectAnimator.ofFloat(
350+
textView,
351+
View.TRANSLATION_X,
352+
width,
353+
-textWidth
354+
);
355+
}
356+
textView.setY((float) (height * (0.5 + row) / calculatedCommentRowsCount
357+
- textHeight / 2));
358+
359+
final AnimatedTextView animatedTextView = new AnimatedTextView(
360+
textView, animator);
361+
animatedTextViews.add(animatedTextView);
362+
animator.setFrameDelay(1);
363+
animator.setInterpolator(new LinearInterpolator());
364+
animator.setDuration(item.getLastingTime() != -1
365+
? item.getLastingTime()
366+
: (long) (commentsDuration * 1000
367+
* (item.getPosition().equals(BulletCommentsInfoItem.Position.REGULAR)
368+
? durationFactor : 1)));
369+
animator.addListener(new AnimatorListenerAdapter() {
370+
public void onAnimationEnd(final Animator animation) {
371+
binding.bulletCommentsContainer.removeView(textView);
372+
animatedTextViews.remove(animatedTextView);
373+
}
374+
});
375+
animator.start();
376+
});
377+
binding.bulletCommentsContainer.addView(textView);
378+
drawn++;
379+
}
380+
if (drawn > 0) {
381+
Log.v(TAG, "drawCommentsByPool() drawn=" + drawn);
382+
}
383+
}
384+
}

0 commit comments

Comments
 (0)