1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-25 20:42:34 +01:00

-Added quadratic slider strategy implementation and tests.

-Modified playback speed control to use quadratic sliders instead of linear.
-Modified number formatters in player helper to use double instead of float.
-Simplified slider behavior in playback parameter dialog.
-Fixed potential NPE in base local fragment.
This commit is contained in:
John Zhen Mo 2018-03-21 20:08:33 -07:00
parent e885822a34
commit 18d019c62a
5 changed files with 353 additions and 160 deletions

View File

@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override @Override
public void showListFooter(final boolean show) { public void showListFooter(final boolean show) {
itemsList.post(() -> itemListAdapter.showFooter(show)); if (itemsList == null) return;
itemsList.post(() -> {
if (itemListAdapter != null) itemListAdapter.showFooter(show);
});
} }
@Override @Override

View File

@ -14,59 +14,64 @@ import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG; import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment { public class PlaybackParameterDialog extends DialogFragment {
private static final String TAG = "PlaybackParameterDialog"; @NonNull private static final String TAG = "PlaybackParameterDialog";
public static final float MINIMUM_PLAYBACK_VALUE = 0.25f; public static final double MINIMUM_PLAYBACK_VALUE = 0.25f;
public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f; public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final String STEP_UP_SIGN = "+"; public static final char STEP_UP_SIGN = '+';
public static final String STEP_DOWN_SIGN = "-"; public static final char STEP_DOWN_SIGN = '-';
public static final float PLAYBACK_STEP_VALUE = 0.05f; public static final double PLAYBACK_STEP_VALUE = 0.05f;
public static final float NIGHTCORE_TEMPO = 1.20f; public static final double NIGHTCORE_TEMPO = 1.20f;
public static final float NIGHTCORE_PITCH_LOWER = 1.15f; public static final double NIGHTCORE_PITCH_LOWER = 1.15f;
public static final float NIGHTCORE_PITCH_UPPER = 1.25f; public static final double NIGHTCORE_PITCH_UPPER = 1.25f;
public static final float DEFAULT_TEMPO = 1.00f; public static final double DEFAULT_TEMPO = 1.00f;
public static final float DEFAULT_PITCH = 1.00f; public static final double DEFAULT_PITCH = 1.00f;
private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; @NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; @NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
public interface Callback { public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch); void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
} }
private Callback callback; @Nullable private Callback callback;
private float initialTempo = DEFAULT_TEMPO; @NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
private float initialPitch = DEFAULT_PITCH; MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
private SeekBar tempoSlider; private double initialTempo = DEFAULT_TEMPO;
private TextView tempoMinimumText; private double initialPitch = DEFAULT_PITCH;
private TextView tempoMaximumText;
private TextView tempoCurrentText;
private TextView tempoStepDownText;
private TextView tempoStepUpText;
private SeekBar pitchSlider; @Nullable private SeekBar tempoSlider;
private TextView pitchMinimumText; @Nullable private TextView tempoMinimumText;
private TextView pitchMaximumText; @Nullable private TextView tempoMaximumText;
private TextView pitchCurrentText; @Nullable private TextView tempoCurrentText;
private TextView pitchStepDownText; @Nullable private TextView tempoStepDownText;
private TextView pitchStepUpText; @Nullable private TextView tempoStepUpText;
private CheckBox unhookingCheckbox; @Nullable private SeekBar pitchSlider;
@Nullable private TextView pitchMinimumText;
@Nullable private TextView pitchMaximumText;
@Nullable private TextView pitchCurrentText;
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
private TextView nightCorePresetText; @Nullable private CheckBox unhookingCheckbox;
private TextView resetPresetText;
public static PlaybackParameterDialog newInstance(final float playbackTempo, @Nullable private TextView nightCorePresetText;
final float playbackPitch) { @Nullable private TextView resetPresetText;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog(); PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo; dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch; dialog.initialPitch = playbackPitch;
@ -91,16 +96,16 @@ public class PlaybackParameterDialog extends DialogFragment {
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH); initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
} }
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putFloat(INITIAL_TEMPO_KEY, initialTempo); outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putFloat(INITIAL_PITCH_KEY, initialPitch); outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -111,7 +116,7 @@ public class PlaybackParameterDialog extends DialogFragment {
@Override @Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupView(view); setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.playback_speed_control) .setTitle(R.string.playback_speed_control)
@ -120,16 +125,16 @@ public class PlaybackParameterDialog extends DialogFragment {
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> .setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch)) setPlaybackParameters(initialTempo, initialPitch))
.setPositiveButton(R.string.finish, (dialogInterface, i) -> .setPositiveButton(R.string.finish, (dialogInterface, i) ->
setPlaybackParameters(getCurrentTempo(), getCurrentPitch())); setCurrentPlaybackParameters());
return dialogBuilder.create(); return dialogBuilder.create();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Dialog Builder // Control Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void setupView(@NonNull View rootView) { private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView); setupHookingControl(rootView);
setupTempoControl(rootView); setupTempoControl(rootView);
setupPitchControl(rootView); setupPitchControl(rootView);
@ -144,45 +149,34 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo)); tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
if (tempoStepUpText != null) {
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
tempoStepUpText.setOnClickListener(view -> tempoStepUpText.setOnClickListener(view -> {
setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE)); onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (tempoStepDownText != null) {
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
tempoStepDownText.setOnClickListener(view -> tempoStepDownText.setOnClickListener(view -> {
setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE)); onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE)); if (tempoSlider != null) {
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo)); tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(strategy.progressOf(initialTempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
} }
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final float currentTempo = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
if (fromUser) { // this change is first in chain
setTempo(currentTempo);
} else {
setPlaybackParameters(currentTempo, getCurrentPitch());
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
} }
private void setupPitchControl(@NonNull View rootView) { private void setupPitchControl(@NonNull View rootView) {
@ -193,32 +187,85 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch)); pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
if (pitchStepUpText != null) {
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE)); pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
pitchStepUpText.setOnClickListener(view -> pitchStepUpText.setOnClickListener(view -> {
setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE)); onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE)); });
pitchStepDownText.setOnClickListener(view ->
setPitch(getCurrentPitch() - PLAYBACK_STEP_VALUE));
pitchSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialPitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
} }
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() { if (pitchStepDownText != null) {
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
pitchStepDownText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(strategy.progressOf(initialPitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
}
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(initialPitch != initialTempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
});
}
}
private void setupPresetControl(@NonNull View rootView) {
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
if (nightCorePresetText != null) {
nightCorePresetText.setOnClickListener(view -> {
final double randomPitch = NIGHTCORE_PITCH_LOWER +
Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
setTempoSlider(NIGHTCORE_TEMPO);
setPitchSlider(randomPitch);
setCurrentPlaybackParameters();
});
}
resetPresetText = rootView.findViewById(R.id.presetReset);
if (resetPresetText != null) {
resetPresetText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO);
setPitchSlider(DEFAULT_PITCH);
setCurrentPlaybackParameters();
});
}
}
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() { return new SeekBar.OnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress); final double currentTempo = strategy.valueOf(progress);
if (fromUser) { // this change is first in chain if (fromUser) {
setPitch(currentPitch); onTempoSliderUpdated(currentTempo);
} else { setCurrentPlaybackParameters();
setPlaybackParameters(getCurrentTempo(), currentPitch);
} }
} }
@ -234,38 +281,30 @@ public class PlaybackParameterDialog extends DialogFragment {
}; };
} }
private void setupHookingControl(@NonNull View rootView) { private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); return new SeekBar.OnSeekBarChangeListener() {
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { @Override
if (isChecked) return; public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// When unchecked, slide back to the minimum of current tempo or pitch final double currentPitch = strategy.valueOf(progress);
final float minimum = Math.min(getCurrentPitch(), getCurrentTempo()); if (fromUser) { // this change is first in chain
setSliders(minimum); onPitchSliderUpdated(currentPitch);
}); setCurrentPlaybackParameters();
}
} }
private void setupPresetControl(@NonNull View rootView) { @Override
nightCorePresetText = rootView.findViewById(R.id.presetNightcore); public void onStartTrackingTouch(SeekBar seekBar) {
nightCorePresetText.setOnClickListener(view -> { // Do Nothing.
final float randomPitch = NIGHTCORE_PITCH_LOWER +
(float) Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
setTempoSlider(NIGHTCORE_TEMPO);
setPitchSlider(randomPitch);
});
resetPresetText = rootView.findViewById(R.id.presetReset);
resetPresetText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO);
setPitchSlider(DEFAULT_PITCH);
});
} }
/*////////////////////////////////////////////////////////////////////////// @Override
// Helper public void onStopTrackingTouch(SeekBar seekBar) {
//////////////////////////////////////////////////////////////////////////*/ // Do Nothing.
}
};
}
private void setTempo(final float newTempo) { private void onTempoSliderUpdated(final double newTempo) {
if (unhookingCheckbox == null) return; if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) { if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo); setSliders(newTempo);
@ -274,7 +313,7 @@ public class PlaybackParameterDialog extends DialogFragment {
} }
} }
private void setPitch(final float newPitch) { private void onPitchSliderUpdated(final double newPitch) {
if (unhookingCheckbox == null) return; if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) { if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch); setSliders(newPitch);
@ -283,25 +322,30 @@ public class PlaybackParameterDialog extends DialogFragment {
} }
} }
private void setSliders(final float newValue) { private void setSliders(final double newValue) {
setTempoSlider(newValue); setTempoSlider(newValue);
setPitchSlider(newValue); setPitchSlider(newValue);
} }
private void setTempoSlider(final float newTempo) { private void setTempoSlider(final double newTempo) {
if (tempoSlider == null) return; if (tempoSlider == null) return;
// seekbar doesn't register progress if it is the same as the existing progress tempoSlider.setProgress(strategy.progressOf(newTempo));
tempoSlider.setProgress(Integer.MAX_VALUE);
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo));
} }
private void setPitchSlider(final float newPitch) { private void setPitchSlider(final double newPitch) {
if (pitchSlider == null) return; if (pitchSlider == null) return;
pitchSlider.setProgress(Integer.MAX_VALUE); pitchSlider.setProgress(strategy.progressOf(newPitch));
pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch));
} }
private void setPlaybackParameters(final float tempo, final float pitch) { /*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
}
private void setPlaybackParameters(final double tempo, final double pitch) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) { if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " + if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " + "tempo=[" + tempo + "], " +
@ -309,40 +353,27 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged(tempo, pitch); callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
} }
} }
private float getCurrentTempo() { private double getCurrentTempo() {
return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, return tempoSlider == null ? initialTempo : strategy.valueOf(
tempoSlider.getProgress()); tempoSlider.getProgress());
} }
private float getCurrentPitch() { private double getCurrentPitch() {
return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, return pitchSlider == null ? initialPitch : strategy.valueOf(
pitchSlider.getProgress()); pitchSlider.getProgress());
} }
/** @NonNull
* Converts from zeroed float with a minimum offset to the nearest rounded slider private static String getStepUpPercentString(final double percent) {
* equivalent integer
* */
private static int getSliderEquivalent(final float minimumValue, final float floatValue) {
return Math.round((floatValue - minimumValue) * 100f);
}
/**
* Converts from slider integer value to an equivalent float value with a given minimum offset
* */
private static float getSliderEquivalent(final float minimumValue, final int intValue) {
return ((float) intValue) / 100f + minimumValue;
}
private static String getStepUpPercentString(final float percent) {
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent); return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
} }
private static String getStepDownPercentString(final float percent) { @NonNull
private static String getStepDownPercentString(final double percent) {
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent); return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
} }
} }

View File

@ -60,11 +60,11 @@ public class PlayerHelper {
: stringFormatter.format("%02d:%02d", minutes, seconds).toString(); : stringFormatter.format("%02d:%02d", minutes, seconds).toString();
} }
public static String formatSpeed(float speed) { public static String formatSpeed(double speed) {
return speedFormatter.format(speed); return speedFormatter.format(speed);
} }
public static String formatPitch(float pitch) { public static String formatPitch(double pitch) {
return pitchFormatter.format(pitch); return pitchFormatter.format(pitch);
} }

View File

@ -0,0 +1,73 @@
package org.schabi.newpipe.util;
public interface SliderStrategy {
/**
* Converts from zeroed double with a minimum offset to the nearest rounded slider
* equivalent integer
* */
int progressOf(final double value);
/**
* Converts from slider integer value to an equivalent double value with a given
* minimum offset
* */
double valueOf(final int progress);
// TODO: also implement linear strategy when needed
final class Quadratic implements SliderStrategy {
private final double leftGap;
private final double rightGap;
private final double center;
private final int centerProgress;
/**
* Quadratic slider strategy that scales the value of a slider given how far the slider
* progress is from the center of the slider. The further away from the center,
* the faster the interpreted value changes, and vice versa.
*
* @param minimum the minimum value of the interpreted value of the slider.
* @param maximum the maximum value of the interpreted value of the slider.
* @param center center of the interpreted value between the minimum and maximum, which
* will be used as the center value on the slider progress. Doesn't need
* to be the average of the minimum and maximum values, but must be in
* between the two.
* @param maxProgress the maximum possible progress of the slider, this is the
* value that is shown for the UI and controls the granularity of
* the slider. Should be as large as possible to avoid floating
* point round-off error. Using odd number is recommended.
* */
public Quadratic(double minimum, double maximum, double center, int maxProgress) {
if (center < minimum || center > maximum) {
throw new IllegalArgumentException("Center must be in between minimum and maximum");
}
this.leftGap = minimum - center;
this.rightGap = maximum - center;
this.center = center;
this.centerProgress = maxProgress / 2;
}
@Override
public int progressOf(double value) {
final double difference = value - center;
final double root = difference >= 0 ?
Math.sqrt(difference / rightGap) :
-Math.sqrt(Math.abs(difference / leftGap));
final double offset = Math.round(root * centerProgress);
return (int) (centerProgress + offset);
}
@Override
public double valueOf(int progress) {
final int offset = progress - centerProgress;
final double square = Math.pow(((double) offset) / ((double) centerProgress), 2);
final double difference = square * (offset >= 0 ? rightGap : leftGap);
return difference + center;
}
}
}

View File

@ -0,0 +1,86 @@
package org.schabi.newpipe.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class QuadraticSliderStrategyTest {
private final static int STEP = 100;
private final static float DELTA = 1f / (float) STEP;
private final SliderStrategy.Quadratic standard =
new SliderStrategy.Quadratic(0f, 100f, 50f, STEP);
@Test
public void testLeftBound() throws Exception {
assertEquals(standard.progressOf(0), 0);
assertEquals(standard.valueOf(0), 0f, DELTA);
}
@Test
public void testCenter() throws Exception {
assertEquals(standard.progressOf(50), 50);
assertEquals(standard.valueOf(50), 50f, DELTA);
}
@Test
public void testRightBound() throws Exception {
assertEquals(standard.progressOf(100), 100);
assertEquals(standard.valueOf(100), 100f, DELTA);
}
@Test
public void testLeftRegion() throws Exception {
final int leftProgress = standard.progressOf(25);
final double leftValue = standard.valueOf(25);
assertTrue(leftProgress > 0 && leftProgress < 50);
assertTrue(leftValue > 0f && leftValue < 50);
}
@Test
public void testRightRegion() throws Exception {
final int leftProgress = standard.progressOf(75);
final double leftValue = standard.valueOf(75);
assertTrue(leftProgress > 50 && leftProgress < 100);
assertTrue(leftValue > 50f && leftValue < 100);
}
@Test
public void testConversion() throws Exception {
assertEquals(standard.progressOf(standard.valueOf(0)), 0);
assertEquals(standard.progressOf(standard.valueOf(25)), 25);
assertEquals(standard.progressOf(standard.valueOf(50)), 50);
assertEquals(standard.progressOf(standard.valueOf(75)), 75);
assertEquals(standard.progressOf(standard.valueOf(100)), 100);
}
@Test
public void testReverseConversion() throws Exception {
// Need a larger delta since step size / granularity is too small and causes
// floating point round-off errors during conversion
final float largeDelta = 1f;
assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta);
}
@Test
public void testQuadraticPropertyLeftRegion() throws Exception {
final double differenceCloserToCenter =
Math.abs(standard.valueOf(40) - standard.valueOf(45));
final double differenceFurtherFromCenter =
Math.abs(standard.valueOf(10) - standard.valueOf(15));
assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
}
@Test
public void testQuadraticPropertyRightRegion() throws Exception {
final double differenceCloserToCenter =
Math.abs(standard.valueOf(75) - standard.valueOf(70));
final double differenceFurtherFromCenter =
Math.abs(standard.valueOf(95) - standard.valueOf(90));
assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
}
}