-Added playback speed control dialog to allow full user control over player tempo and pitch parameters.
-Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog. -Changed LIVE button to be no longer clickable when player position is at or beyond default position. -Changed main video player to use AppCompatActivity rather than Activity. -Fixed video player tempo button not updating when player speed parameters change. -Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest. -Fixed inconsistent gradle library naming. -Fixed stetho dependencies incorrect version.
This commit is contained in:
parent
5167fe078b
commit
e885822a34
|
@ -51,9 +51,10 @@ ext {
|
|||
supportLibVersion = '27.1.0'
|
||||
exoPlayerLibVersion = '2.7.1'
|
||||
roomDbLibVersion = '1.0.0'
|
||||
leakCanaryVersion = '1.5.4'
|
||||
okHttpVersion = '1.5.0'
|
||||
icepickVersion = '3.2.0'
|
||||
leakCanaryLibVersion = '1.5.4'
|
||||
okHttpLibVersion = '1.5.0'
|
||||
icepickLibVersion = '3.2.0'
|
||||
stethoLibVersion = '1.5.0'
|
||||
}
|
||||
dependencies {
|
||||
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
|
||||
|
@ -81,8 +82,8 @@ dependencies {
|
|||
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:$okHttpVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:$okHttpVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
|
||||
debugImplementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
|
||||
|
@ -93,13 +94,13 @@ dependencies {
|
|||
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
|
||||
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
|
||||
|
||||
implementation "frankiesardo:icepick:$icepickVersion"
|
||||
annotationProcessor "frankiesardo:icepick-processor:$icepickVersion"
|
||||
implementation "frankiesardo:icepick:$icepickLibVersion"
|
||||
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
|
||||
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
|
||||
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
|
||||
}
|
||||
|
|
|
@ -28,6 +28,12 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".player.old.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
|
|
|
@ -553,7 +553,7 @@ public abstract class BasePlayer implements
|
|||
// Ensure dynamic/livestream timeline changes does not cause negative position
|
||||
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
|
||||
"clamping position to default time.");
|
||||
"clamping position to 0ms.");
|
||||
seekTo(/*clampToTime=*/0);
|
||||
}
|
||||
break;
|
||||
|
@ -639,7 +639,6 @@ public abstract class BasePlayer implements
|
|||
"[" + getTimeString((int)recoveryPositionMillis) + "]");
|
||||
seekTo(recoveryPositionMillis);
|
||||
playQueue.unsetRecovery(currentSourceIndex);
|
||||
isSynchronizing = false;
|
||||
|
||||
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
|
||||
|
@ -1111,6 +1110,24 @@ public abstract class BasePlayer implements
|
|||
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader();
|
||||
}
|
||||
|
||||
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
|
||||
public boolean isLiveEdge() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
|
||||
if (!isLive) return false;
|
||||
|
||||
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
||||
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
if (currentTimeline.isEmpty() || currentWindowIndex < 0 ||
|
||||
currentWindowIndex >= currentTimeline.getWindowCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Timeline.Window timelineWindow = new Timeline.Window();
|
||||
currentTimeline.getWindow(currentWindowIndex, timelineWindow);
|
||||
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -33,6 +32,7 @@ import android.preference.PreferenceManager;
|
|||
import android.provider.Settings;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.util.DisplayMetrics;
|
||||
|
@ -49,6 +49,7 @@ import android.widget.SeekBar;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
@ -57,6 +58,7 @@ import org.schabi.newpipe.R;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
|
@ -87,7 +89,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
|
|||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead {
|
||||
public final class MainVideoPlayer extends AppCompatActivity
|
||||
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
|
||||
private static final String TAG = ".MainVideoPlayer";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
|
||||
|
@ -340,6 +343,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
|
|||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playback Parameters Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
|
@ -630,6 +642,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
|
|||
showControlsThenHide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedClicked() {
|
||||
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
|
||||
.show(getSupportFragmentManager(), TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
super.onStopTrackingTouch(seekBar);
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
|
@ -43,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
|
|||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
|
||||
public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
|
@ -57,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
||||
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
|
||||
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
|
||||
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
|
@ -85,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
private ProgressBar progressBar;
|
||||
|
||||
private TextView playbackSpeedButton;
|
||||
private PopupMenu playbackSpeedPopupMenu;
|
||||
private TextView playbackPitchButton;
|
||||
private PopupMenu playbackPitchPopupMenu;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Abstracts
|
||||
|
@ -317,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
shuffleButton.setOnClickListener(this);
|
||||
playbackSpeedButton.setOnClickListener(this);
|
||||
playbackPitchButton.setOnClickListener(this);
|
||||
|
||||
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
|
||||
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
|
||||
buildPlaybackSpeedMenu();
|
||||
buildPlaybackPitchMenu();
|
||||
}
|
||||
|
||||
private void buildPlaybackSpeedMenu() {
|
||||
if (playbackSpeedPopupMenu == null) return;
|
||||
|
||||
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
|
||||
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
|
||||
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
|
||||
final String formattedSpeed = formatSpeed(playbackSpeed);
|
||||
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
|
||||
item.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) return false;
|
||||
|
||||
player.setPlaybackSpeed(playbackSpeed);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void buildPlaybackPitchMenu() {
|
||||
if (playbackPitchPopupMenu == null) return;
|
||||
|
||||
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
|
||||
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
|
||||
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
|
||||
final String formattedPitch = formatPitch(playbackPitch);
|
||||
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
|
||||
item.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) return false;
|
||||
|
||||
player.setPlaybackPitch(playbackPitch);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||
|
@ -474,10 +433,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
player.onShuffleClicked();
|
||||
|
||||
} else if (view.getId() == playbackSpeedButton.getId()) {
|
||||
playbackSpeedPopupMenu.show();
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
||||
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
|
||||
|
||||
} else if (view.getId() == playbackPitchButton.getId()) {
|
||||
playbackPitchPopupMenu.show();
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
||||
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
|
||||
|
||||
} else if (view.getId() == metadata.getId()) {
|
||||
scrollToSelected();
|
||||
|
@ -488,6 +449,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playback Parameters Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Seekbar Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -539,6 +509,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
progressSeekBar.setProgress(currentProgress);
|
||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
progressLiveSync.setClickable(!player.isLiveEdge());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -49,6 +49,7 @@ import android.widget.TextView;
|
|||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
|
@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
onTextTrackUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
super.onPlaybackParametersChanged(playbackParameters);
|
||||
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
if (DEBUG) {
|
||||
|
@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
|
||||
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
|
||||
}
|
||||
playbackLiveSync.setClickable(!isLiveEdge());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -718,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
wasPlaying = simpleExoPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
private void onPlaybackSpeedClicked() {
|
||||
public void onPlaybackSpeedClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
|
||||
playbackSpeedPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
private static final String TAG = "PlaybackParameterDialog";
|
||||
|
||||
public static final float MINIMUM_PLAYBACK_VALUE = 0.25f;
|
||||
public static final float MAXIMUM_PLAYBACK_VALUE = 3.00f;
|
||||
|
||||
public static final String STEP_UP_SIGN = "+";
|
||||
public static final String STEP_DOWN_SIGN = "-";
|
||||
public static final float PLAYBACK_STEP_VALUE = 0.05f;
|
||||
|
||||
public static final float NIGHTCORE_TEMPO = 1.20f;
|
||||
public static final float NIGHTCORE_PITCH_LOWER = 1.15f;
|
||||
public static final float NIGHTCORE_PITCH_UPPER = 1.25f;
|
||||
|
||||
public static final float DEFAULT_TEMPO = 1.00f;
|
||||
public static final float DEFAULT_PITCH = 1.00f;
|
||||
|
||||
private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
|
||||
private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
|
||||
|
||||
public interface Callback {
|
||||
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
|
||||
}
|
||||
|
||||
private Callback callback;
|
||||
|
||||
private float initialTempo = DEFAULT_TEMPO;
|
||||
private float initialPitch = DEFAULT_PITCH;
|
||||
|
||||
private SeekBar tempoSlider;
|
||||
private TextView tempoMinimumText;
|
||||
private TextView tempoMaximumText;
|
||||
private TextView tempoCurrentText;
|
||||
private TextView tempoStepDownText;
|
||||
private TextView tempoStepUpText;
|
||||
|
||||
private SeekBar pitchSlider;
|
||||
private TextView pitchMinimumText;
|
||||
private TextView pitchMaximumText;
|
||||
private TextView pitchCurrentText;
|
||||
private TextView pitchStepDownText;
|
||||
private TextView pitchStepUpText;
|
||||
|
||||
private CheckBox unhookingCheckbox;
|
||||
|
||||
private TextView nightCorePresetText;
|
||||
private TextView resetPresetText;
|
||||
|
||||
public static PlaybackParameterDialog newInstance(final float playbackTempo,
|
||||
final float playbackPitch) {
|
||||
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
|
||||
dialog.initialTempo = playbackTempo;
|
||||
dialog.initialPitch = playbackPitch;
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context != null && context instanceof Callback) {
|
||||
callback = (Callback) context;
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
initialTempo = savedInstanceState.getFloat(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
||||
initialPitch = savedInstanceState.getFloat(INITIAL_PITCH_KEY, DEFAULT_PITCH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putFloat(INITIAL_TEMPO_KEY, initialTempo);
|
||||
outState.putFloat(INITIAL_PITCH_KEY, initialPitch);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Dialog
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
|
||||
setupView(view);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.playback_speed_control)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||
setPlaybackParameters(initialTempo, initialPitch))
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
||||
setPlaybackParameters(getCurrentTempo(), getCurrentPitch()));
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Dialog Builder
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setupView(@NonNull View rootView) {
|
||||
setupHookingControl(rootView);
|
||||
setupTempoControl(rootView);
|
||||
setupPitchControl(rootView);
|
||||
setupPresetControl(rootView);
|
||||
}
|
||||
|
||||
private void setupTempoControl(@NonNull View rootView) {
|
||||
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
|
||||
tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
|
||||
tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
|
||||
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
|
||||
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
|
||||
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
|
||||
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
|
||||
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
|
||||
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
|
||||
|
||||
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
|
||||
tempoStepUpText.setOnClickListener(view ->
|
||||
setTempo(getCurrentTempo() + PLAYBACK_STEP_VALUE));
|
||||
|
||||
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
|
||||
tempoStepDownText.setOnClickListener(view ->
|
||||
setTempo(getCurrentTempo() - PLAYBACK_STEP_VALUE));
|
||||
|
||||
tempoSlider.setMax(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE));
|
||||
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, initialTempo));
|
||||
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) {
|
||||
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
|
||||
pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
|
||||
pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
|
||||
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
|
||||
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
|
||||
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
|
||||
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
|
||||
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
|
||||
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
|
||||
|
||||
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
|
||||
pitchStepUpText.setOnClickListener(view ->
|
||||
setPitch(getCurrentPitch() + PLAYBACK_STEP_VALUE));
|
||||
|
||||
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() {
|
||||
return new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
final float currentPitch = getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, progress);
|
||||
if (fromUser) { // this change is first in chain
|
||||
setPitch(currentPitch);
|
||||
} else {
|
||||
setPlaybackParameters(getCurrentTempo(), currentPitch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupHookingControl(@NonNull View rootView) {
|
||||
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
||||
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
if (isChecked) return;
|
||||
// When unchecked, slide back to the minimum of current tempo or pitch
|
||||
final float minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
||||
setSliders(minimum);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupPresetControl(@NonNull View rootView) {
|
||||
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
|
||||
nightCorePresetText.setOnClickListener(view -> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Helper
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setTempo(final float newTempo) {
|
||||
if (unhookingCheckbox == null) return;
|
||||
if (!unhookingCheckbox.isChecked()) {
|
||||
setSliders(newTempo);
|
||||
} else {
|
||||
setTempoSlider(newTempo);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPitch(final float newPitch) {
|
||||
if (unhookingCheckbox == null) return;
|
||||
if (!unhookingCheckbox.isChecked()) {
|
||||
setSliders(newPitch);
|
||||
} else {
|
||||
setPitchSlider(newPitch);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSliders(final float newValue) {
|
||||
setTempoSlider(newValue);
|
||||
setPitchSlider(newValue);
|
||||
}
|
||||
|
||||
private void setTempoSlider(final float newTempo) {
|
||||
if (tempoSlider == null) return;
|
||||
// seekbar doesn't register progress if it is the same as the existing progress
|
||||
tempoSlider.setProgress(Integer.MAX_VALUE);
|
||||
tempoSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newTempo));
|
||||
}
|
||||
|
||||
private void setPitchSlider(final float newPitch) {
|
||||
if (pitchSlider == null) return;
|
||||
pitchSlider.setProgress(Integer.MAX_VALUE);
|
||||
pitchSlider.setProgress(getSliderEquivalent(MINIMUM_PLAYBACK_VALUE, newPitch));
|
||||
}
|
||||
|
||||
private void setPlaybackParameters(final float tempo, final float pitch) {
|
||||
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
|
||||
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
|
||||
"tempo=[" + tempo + "], " +
|
||||
"pitch=[" + pitch + "]");
|
||||
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
|
||||
callback.onPlaybackParameterChanged(tempo, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
private float getCurrentTempo() {
|
||||
return tempoSlider == null ? initialTempo : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
|
||||
tempoSlider.getProgress());
|
||||
}
|
||||
|
||||
private float getCurrentPitch() {
|
||||
return pitchSlider == null ? initialPitch : getSliderEquivalent(MINIMUM_PLAYBACK_VALUE,
|
||||
pitchSlider.getProgress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from zeroed float with a minimum offset to the nearest rounded slider
|
||||
* 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);
|
||||
}
|
||||
|
||||
private static String getStepDownPercentString(final float percent) {
|
||||
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:paddingLeft="@dimen/video_item_search_padding"
|
||||
android:paddingRight="@dimen/video_item_search_padding"
|
||||
android:paddingTop="@dimen/video_item_search_padding">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical"
|
||||
android:scrollbarAlwaysDrawVerticalTrack="true">
|
||||
|
||||
<!-- START HERE -->
|
||||
<TextView
|
||||
android:id="@+id/tempoControlText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/playback_tempo"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_alignParentTop="true"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/tempoControl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_below="@id/tempoControlText">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tempoStepDown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="--%"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="-5%"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/tempoDisplay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_toRightOf="@id/tempoStepDown"
|
||||
android:layout_toEndOf="@id/tempoStepDown"
|
||||
android:layout_toLeftOf="@id/tempoStepUp"
|
||||
android:layout_toStartOf="@id/tempoStepUp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tempoMinimumText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="-.--x"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="1.00x"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tempoCurrentText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="---%"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="100%"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tempoMaximumText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="---%"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="300%"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatSeekBar
|
||||
android:id="@+id/tempoSeekbar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tempoCurrentText"
|
||||
android:paddingBottom="4dp"
|
||||
tools:progress="50"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tempoStepUp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="+-%"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="+5%"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/separatorPitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/tempoControl"
|
||||
android:layout_margin="@dimen/video_item_search_padding"
|
||||
android:background="?attr/separator_color"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchControlText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/playback_pitch"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_below="@id/separatorPitch"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/pitchControl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_below="@id/pitchControlText">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchStepDown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="--%"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="-5%"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/pitchDisplay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_toRightOf="@+id/pitchStepDown"
|
||||
android:layout_toEndOf="@+id/pitchStepDown"
|
||||
android:layout_toLeftOf="@+id/pitchStepUp"
|
||||
android:layout_toStartOf="@+id/pitchStepUp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchMinimumText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="---%"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="25%"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchCurrentText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="---%"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="100%"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchMaximumText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="---%"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="300%"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatSeekBar
|
||||
android:id="@+id/pitchSeekbar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/pitchCurrentText"
|
||||
android:paddingBottom="4dp"
|
||||
tools:progress="50"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pitchStepUp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="+-%"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="+5%"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/separatorCheckbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@+id/pitchControl"
|
||||
android:layout_margin="@dimen/video_item_search_padding"
|
||||
android:background="?attr/separator_color"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/unhookCheckbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:text="@string/unhook_checkbox"
|
||||
android:maxLines="1"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_below="@id/separatorCheckbox"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/presetSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_below="@id/unhookCheckbox">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/presetNightcore"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/playback_nightcore"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="?attr/colorAccent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/presetReset"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/playback_default"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="?attr/colorAccent"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- END HERE -->
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
|
@ -456,4 +456,12 @@
|
|||
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
|
||||
|
||||
<string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string>
|
||||
|
||||
<!-- Playback Parameters -->
|
||||
<string name="playback_speed_control">Playback Speed Control</string>
|
||||
<string name="playback_tempo">Tempo</string>
|
||||
<string name="playback_pitch">Pitch</string>
|
||||
<string name="unhook_checkbox">Unhook (may cause distortion)</string>
|
||||
<string name="playback_nightcore">Nightcore</string>
|
||||
<string name="playback_default">Default</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue