Skip to content

Commit 1de5c38

Browse files
committed
Enhance ChaptersSeekBar to render transparent gaps at chapter boundaries and add haptic feedback for chapter navigation
1 parent 3dba420 commit 1de5c38

File tree

2 files changed

+49
-72
lines changed

2 files changed

+49
-72
lines changed

app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import android.os.Looper;
2929
import android.util.Log;
3030
import android.view.GestureDetector;
31+
import android.view.HapticFeedbackConstants;
3132
import android.view.Gravity;
3233
import android.view.KeyEvent;
3334
import android.view.Menu;
@@ -153,6 +154,8 @@ private enum PlayButtonAction {
153154

154155
@NonNull
155156
private List<StreamSegment> currentChapters = Collections.emptyList();
157+
@Nullable
158+
private StreamSegment lastChapterForHaptic = null;
156159

157160

158161
/*//////////////////////////////////////////////////////////////////////////
@@ -592,12 +595,16 @@ public void onProgressChanged(final SeekBar seekBar, final int progress,
592595
binding.currentSeekbarPreviewThumbnail,
593596
binding.subtitleView::getWidth);
594597

595-
// Chapter title tooltip
598+
// Chapter title tooltip + haptic feedback at chapter boundaries
596599
if (!currentChapters.isEmpty()) {
597600
final StreamSegment chapter = getChapterAtMs(progress);
598601
if (chapter != null && chapter.getTitle() != null) {
599602
binding.currentChapterTitle.setText(chapter.getTitle());
600603
}
604+
if (chapter != lastChapterForHaptic) {
605+
lastChapterForHaptic = chapter;
606+
seekBar.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
607+
}
601608
}
602609

603610
adjustSeekbarPreviewContainer();
@@ -1065,17 +1072,9 @@ public void onMetadataChanged(@NonNull final StreamInfo info) {
10651072
// Chapter markers on seekbar
10661073
currentChapters = info.getStreamSegments() != null
10671074
? info.getStreamSegments() : Collections.emptyList();
1068-
Log.d(TAG, "onMetadataChanged: seekBarClass="
1069-
+ binding.playbackSeekBar.getClass().getSimpleName()
1070-
+ " segments=" + currentChapters.size()
1071-
+ " duration=" + info.getDuration());
1072-
if (binding.playbackSeekBar instanceof ChaptersSeekBar) {
1073-
((ChaptersSeekBar) binding.playbackSeekBar)
1074-
.setChapters(currentChapters, info.getDuration());
1075-
} else {
1076-
Log.e(TAG, "onMetadataChanged: playbackSeekBar is NOT a ChaptersSeekBar! "
1077-
+ "Check that player.xml was rebuilt.");
1078-
}
1075+
lastChapterForHaptic = null;
1076+
((ChaptersSeekBar) binding.playbackSeekBar)
1077+
.setChapters(currentChapters, info.getDuration());
10791078
binding.currentChapterTitle.setVisibility(View.GONE);
10801079
}
10811080

app/src/main/java/org/schabi/newpipe/views/ChaptersSeekBar.java

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919

2020
import android.content.Context;
2121
import android.graphics.Canvas;
22-
import android.graphics.Color;
2322
import android.graphics.Paint;
23+
import android.graphics.PorterDuff;
24+
import android.graphics.PorterDuffXfermode;
25+
import android.graphics.drawable.Drawable;
2426
import android.util.AttributeSet;
25-
import android.util.Log;
2627

2728
import androidx.annotation.NonNull;
2829
import androidx.annotation.Nullable;
@@ -33,18 +34,15 @@
3334
import java.util.List;
3435

3536
/**
36-
* A {@link FocusAwareSeekBar} that draws thin white vertical tick marks at chapter boundaries.
37+
* A {@link FocusAwareSeekBar} that renders narrow transparent gaps at chapter boundaries,
38+
* giving the seekbar a segmented "chopped" appearance.
3739
* Call {@link #setChapters(List, long)} whenever a new stream loads.
3840
*/
3941
public final class ChaptersSeekBar extends FocusAwareSeekBar {
4042

41-
private static final String TAG = "ChaptersSeekBar";
43+
private static final float GAP_WIDTH_DP = 2f;
4244

43-
private static final int TICK_ALPHA = 180; // ~70% opacity
44-
private static final float TICK_WIDTH_DP = 2f;
45-
private static final float TICK_HEIGHT_FRACTION = 0.6f; // fraction of view height
46-
47-
private final Paint tickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
45+
private final Paint gapPaint = new Paint();
4846

4947
@NonNull private List<StreamSegment> chapters = Collections.emptyList();
5048
private long durationSeconds = 0;
@@ -68,14 +66,12 @@ public ChaptersSeekBar(@NonNull final Context context,
6866
}
6967

7068
private void init() {
71-
tickPaint.setColor(Color.WHITE);
72-
tickPaint.setAlpha(TICK_ALPHA);
73-
tickPaint.setStyle(Paint.Style.FILL);
74-
Log.d(TAG, "init: ChaptersSeekBar created");
69+
gapPaint.setStyle(Paint.Style.FILL);
70+
gapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
7571
}
7672

7773
/**
78-
* Stores chapter data for rendering tick marks.
74+
* Stores chapter data for rendering segment gaps.
7975
*
8076
* @param newChapters list of {@link StreamSegment}s; may be empty but never null
8177
* @param newDurationSecs total duration in seconds; used to compute fractional positions
@@ -84,64 +80,46 @@ public void setChapters(@NonNull final List<StreamSegment> newChapters,
8480
final long newDurationSecs) {
8581
chapters = newChapters;
8682
durationSeconds = newDurationSecs;
87-
Log.d(TAG, "setChapters: count=" + newChapters.size()
88-
+ " durationSeconds=" + newDurationSecs);
89-
for (final StreamSegment seg : newChapters) {
90-
Log.d(TAG, " chapter: startSec=" + seg.getStartTimeSeconds()
91-
+ " title=" + seg.getTitle());
92-
}
9383
invalidate();
9484
}
9585

9686
@Override
9787
protected void onDraw(@NonNull final Canvas canvas) {
98-
super.onDraw(canvas);
99-
10088
if (chapters.isEmpty() || durationSeconds <= 0) {
101-
Log.d(TAG, "onDraw: skipped — chapters=" + chapters.size()
102-
+ " durationSeconds=" + durationSeconds);
89+
super.onDraw(canvas);
10390
return;
10491
}
10592

106-
final float density = getResources().getDisplayMetrics().density;
107-
final float tickWidthPx = TICK_WIDTH_DP * density;
108-
109-
// Track bounds: AbsSeekBar pads the track by getPaddingLeft/getPaddingRight
110-
final int paddingLeft = getPaddingLeft();
111-
final int paddingRight = getPaddingRight();
112-
final float trackWidth = getWidth() - paddingLeft - paddingRight;
113-
114-
Log.d(TAG, "onDraw: w=" + getWidth() + " h=" + getHeight()
115-
+ " paddingL=" + paddingLeft + " paddingR=" + paddingRight
116-
+ " trackWidth=" + trackWidth + " chapters=" + chapters.size()
117-
+ " durationSeconds=" + durationSeconds);
93+
// Draw the seekbar into an offscreen layer so CLEAR mode can punch transparent gaps
94+
final int sc = canvas.saveLayer(null, null);
95+
super.onDraw(canvas);
11896

119-
if (trackWidth <= 0) {
120-
Log.d(TAG, "onDraw: trackWidth<=0, skipping");
121-
return;
97+
final float density = getResources().getDisplayMetrics().density;
98+
final float gapHalfWidth = (GAP_WIDTH_DP * density) / 2f;
99+
final int paddingLeft = getPaddingLeft();
100+
final float trackWidth = getWidth() - paddingLeft - getPaddingRight();
101+
102+
if (trackWidth > 0) {
103+
for (final StreamSegment seg : chapters) {
104+
final int startSec = seg.getStartTimeSeconds();
105+
// Skip the very first position and anything at or past the end
106+
if (startSec <= 0 || startSec >= durationSeconds) {
107+
continue;
108+
}
109+
final float x = paddingLeft + (startSec / (float) durationSeconds) * trackWidth;
110+
canvas.drawRect(x - gapHalfWidth, 0, x + gapHalfWidth, getHeight(), gapPaint);
111+
}
122112
}
123113

124-
// Center ticks vertically, scaling height as a fraction of the view
125-
final float tickHeight = getHeight() * TICK_HEIGHT_FRACTION;
126-
final float tickTop = (getHeight() - tickHeight) / 2f;
127-
final float tickBottom = tickTop + tickHeight;
128-
129-
for (final StreamSegment seg : chapters) {
130-
final int startSec = seg.getStartTimeSeconds();
131-
// Skip the very beginning and anything at or past the end
132-
if (startSec <= 0 || startSec >= durationSeconds) {
133-
Log.d(TAG, " skipping seg startSec=" + startSec);
134-
continue;
135-
}
136-
final float x = paddingLeft + (startSec / (float) durationSeconds) * trackWidth;
137-
Log.d(TAG, " drawing tick at x=" + x + " for startSec=" + startSec
138-
+ " title=" + seg.getTitle());
139-
canvas.drawRect(
140-
x - tickWidthPx / 2f,
141-
tickTop,
142-
x + tickWidthPx / 2f,
143-
tickBottom,
144-
tickPaint);
114+
canvas.restoreToCount(sc);
115+
116+
// Redraw the thumb on top so it visually overlaps the gaps
117+
final Drawable thumb = getThumb();
118+
if (thumb != null) {
119+
final int thumbSave = canvas.save();
120+
canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop());
121+
thumb.draw(canvas);
122+
canvas.restoreToCount(thumbSave);
145123
}
146124
}
147125
}

0 commit comments

Comments
 (0)