Merge pull request #4175 from ByteHamster/playback-speed-selection
New playback speed dialog
This commit is contained in:
commit
dea32051f6
@ -7,7 +7,6 @@ import android.preference.PreferenceManager;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.PreferenceActivity;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeDown;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeUp;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
|
||||
@ -46,7 +44,6 @@ import static de.test.antennapod.EspressoTestUtils.clickPreference;
|
||||
import static de.test.antennapod.EspressoTestUtils.waitForView;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
@ -235,12 +232,10 @@ public class PreferencesTest {
|
||||
@Test
|
||||
public void testPlaybackSpeeds() {
|
||||
clickPreference(R.string.playback_pref);
|
||||
clickPreference(R.string.media_player);
|
||||
onView(withText(R.string.media_player_exoplayer)).perform(click());
|
||||
clickPreference(R.string.pref_playback_speed_title);
|
||||
onView(isRoot()).perform(waitForView(withText("0.50"), 1000));
|
||||
onView(withText("0.50")).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.cancel_label)).perform(click());
|
||||
clickPreference(R.string.playback_speed);
|
||||
onView(isRoot()).perform(waitForView(withText("0.75"), 1000));
|
||||
onView(withText("0.75")).check(matches(isDisplayed()));
|
||||
onView(withText(R.string.close_label)).perform(click());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -25,6 +25,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -70,7 +71,7 @@ public class SpeedChangeTest {
|
||||
List<FeedItem> queue = DBReader.getQueue();
|
||||
PlaybackPreferences.writeMediaPlaying(queue.get(0).getMedia(), PlayerStatus.PAUSED, false);
|
||||
availableSpeeds = new String[] {"1.00", "2.00", "3.00"};
|
||||
UserPreferences.setPlaybackSpeedArray(availableSpeeds);
|
||||
UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f));
|
||||
|
||||
EspressoTestUtils.tryKillPlaybackService();
|
||||
activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true));
|
||||
|
@ -376,8 +376,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements
|
||||
new SleepTimerDialog().show(getSupportFragmentManager(), "SleepTimerDialog");
|
||||
break;
|
||||
case R.id.audio_controls:
|
||||
boolean isPlayingVideo = controller.getMedia().getMediaType() == MediaType.VIDEO;
|
||||
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(isPlayingVideo);
|
||||
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance();
|
||||
dialog.show(getSupportFragmentManager(), "playback_controls");
|
||||
break;
|
||||
case R.id.open_feed_item:
|
||||
|
@ -11,27 +11,21 @@ import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.view.PlaybackSpeedSeekBar;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PlaybackControlsDialog extends DialogFragment {
|
||||
private static final String ARGUMENT_IS_PLAYING_VIDEO = "isPlayingVideo";
|
||||
|
||||
private PlaybackController controller;
|
||||
private AlertDialog dialog;
|
||||
private boolean isPlayingVideo;
|
||||
|
||||
public static PlaybackControlsDialog newInstance(boolean isPlayingVideo) {
|
||||
public static PlaybackControlsDialog newInstance() {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putBoolean(ARGUMENT_IS_PLAYING_VIDEO, isPlayingVideo);
|
||||
PlaybackControlsDialog dialog = new PlaybackControlsDialog();
|
||||
dialog.setArguments(arguments);
|
||||
return dialog;
|
||||
@ -65,8 +59,6 @@ public class PlaybackControlsDialog extends DialogFragment {
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
isPlayingVideo = getArguments() != null && getArguments().getBoolean(ARGUMENT_IS_PLAYING_VIDEO);
|
||||
|
||||
dialog = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.audio_controls)
|
||||
.setView(R.layout.audio_controls)
|
||||
@ -79,68 +71,18 @@ public class PlaybackControlsDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
private void setupUi() {
|
||||
final SeekBar barPlaybackSpeed = dialog.findViewById(R.id.playback_speed);
|
||||
final TextView butDecSpeed = dialog.findViewById(R.id.butDecSpeed);
|
||||
butDecSpeed.setOnClickListener(v -> {
|
||||
if (controller != null && controller.canSetPlaybackSpeed()) {
|
||||
barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 2);
|
||||
} else {
|
||||
VariableSpeedDialog.showGetPluginDialog(getContext());
|
||||
}
|
||||
});
|
||||
final TextView butIncSpeed = dialog.findViewById(R.id.butIncSpeed);
|
||||
butIncSpeed.setOnClickListener(v -> {
|
||||
if (controller != null && controller.canSetPlaybackSpeed()) {
|
||||
barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 2);
|
||||
} else {
|
||||
VariableSpeedDialog.showGetPluginDialog(getContext());
|
||||
}
|
||||
});
|
||||
|
||||
final TextView txtvPlaybackSpeed = dialog.findViewById(R.id.txtvPlaybackSpeed);
|
||||
float currentSpeed = getCurrentSpeed();
|
||||
|
||||
txtvPlaybackSpeed.setText(String.format(Locale.getDefault(), "%.2fx", currentSpeed));
|
||||
barPlaybackSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (controller != null && controller.canSetPlaybackSpeed()) {
|
||||
float playbackSpeed = (progress + 10) / 20.0f;
|
||||
controller.setPlaybackSpeed(playbackSpeed);
|
||||
PlaybackSpeedSeekBar speedSeekBar = dialog.findViewById(R.id.speed_seek_bar);
|
||||
speedSeekBar.setController(controller);
|
||||
speedSeekBar.setProgressChangedListener(speed
|
||||
-> txtvPlaybackSpeed.setText(String.format(Locale.getDefault(), "%.2fx", speed)));
|
||||
|
||||
PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(playbackSpeed);
|
||||
if (isPlayingVideo) {
|
||||
UserPreferences.setVideoPlaybackSpeed(playbackSpeed);
|
||||
} else {
|
||||
UserPreferences.setPlaybackSpeed(playbackSpeed);
|
||||
}
|
||||
|
||||
String speedStr = String.format(Locale.getDefault(), "%.2fx", playbackSpeed);
|
||||
txtvPlaybackSpeed.setText(speedStr);
|
||||
} else if (fromUser) {
|
||||
float speed = getCurrentSpeed();
|
||||
barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress(Math.round((20 * speed) - 10)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (controller != null && !controller.canSetPlaybackSpeed()) {
|
||||
VariableSpeedDialog.showGetPluginDialog(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
barPlaybackSpeed.setProgress(Math.round((20 * currentSpeed) - 10));
|
||||
|
||||
final SeekBar barLeftVolume = (SeekBar) dialog.findViewById(R.id.volume_left);
|
||||
final SeekBar barLeftVolume = dialog.findViewById(R.id.volume_left);
|
||||
barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage());
|
||||
final SeekBar barRightVolume = (SeekBar) dialog.findViewById(R.id.volume_right);
|
||||
final SeekBar barRightVolume = dialog.findViewById(R.id.volume_right);
|
||||
barRightVolume.setProgress(UserPreferences.getRightVolumePercentage());
|
||||
final CheckBox stereoToMono = (CheckBox) dialog.findViewById(R.id.stereo_to_mono);
|
||||
final CheckBox stereoToMono = dialog.findViewById(R.id.stereo_to_mono);
|
||||
stereoToMono.setChecked(UserPreferences.stereoToMono());
|
||||
if (controller != null && !controller.canDownmix()) {
|
||||
stereoToMono.setEnabled(false);
|
||||
@ -220,13 +162,4 @@ public class PlaybackControlsDialog extends DialogFragment {
|
||||
new Handler().postDelayed(this::setupAudioTracks, 500);
|
||||
});
|
||||
}
|
||||
|
||||
private float getCurrentSpeed() {
|
||||
Playable media = null;
|
||||
if (controller != null) {
|
||||
media = controller.getMedia();
|
||||
}
|
||||
|
||||
return PlaybackSpeedUtils.getCurrentPlaybackSpeed(media);
|
||||
}
|
||||
}
|
||||
|
@ -1,97 +1,171 @@
|
||||
package de.danoeh.antennapod.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.view.ItemOffsetDecoration;
|
||||
import de.danoeh.antennapod.view.PlaybackSpeedSeekBar;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class VariableSpeedDialog {
|
||||
public class VariableSpeedDialog extends DialogFragment {
|
||||
private SpeedSelectionAdapter adapter;
|
||||
private final DecimalFormat speedFormat;
|
||||
private PlaybackController controller;
|
||||
private final List<Float> selectedSpeeds;
|
||||
private PlaybackSpeedSeekBar speedSeekBar;
|
||||
private Chip addCurrentSpeedChip;
|
||||
|
||||
private VariableSpeedDialog() {
|
||||
}
|
||||
|
||||
public static void showDialog(final Context context) {
|
||||
if (UserPreferences.useSonic()
|
||||
|| UserPreferences.useExoplayer()
|
||||
|| Build.VERSION.SDK_INT >= 23) {
|
||||
showSpeedSelectorDialog(context);
|
||||
} else {
|
||||
showGetPluginDialog(context, true);
|
||||
}
|
||||
public VariableSpeedDialog() {
|
||||
DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US);
|
||||
format.setDecimalSeparator('.');
|
||||
speedFormat = new DecimalFormat("0.00", format);
|
||||
selectedSpeeds = new ArrayList<>(UserPreferences.getPlaybackSpeedArray());
|
||||
}
|
||||
|
||||
public static void showGetPluginDialog(final Context context) {
|
||||
showGetPluginDialog(context, false);
|
||||
}
|
||||
|
||||
private static void showGetPluginDialog(final Context context, boolean showSpeedSelector) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.no_playback_plugin_title);
|
||||
builder.setMessage(R.string.no_playback_plugin_or_sonic_msg);
|
||||
builder.setPositiveButton(R.string.enable_sonic, (dialog, which) -> {
|
||||
UserPreferences.enableSonic();
|
||||
if (showSpeedSelector) {
|
||||
showSpeedSelectorDialog(context);
|
||||
}
|
||||
});
|
||||
builder.setNeutralButton(R.string.close_label, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void showSpeedSelectorDialog(final Context context) {
|
||||
DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US);
|
||||
format.setDecimalSeparator('.');
|
||||
DecimalFormat speedFormat = new DecimalFormat("0.00", format);
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
controller = new PlaybackController(getActivity()) {
|
||||
@Override
|
||||
public void setupGUI() {
|
||||
updateSpeed();
|
||||
}
|
||||
|
||||
final String[] speedValues = context.getResources().getStringArray(
|
||||
R.array.playback_speed_values);
|
||||
// According to Java spec these get initialized to false on creation
|
||||
final boolean[] speedChecked = new boolean[speedValues.length];
|
||||
|
||||
// Build the "isChecked" array so that multiChoice dialog is populated correctly
|
||||
List<String> selectedSpeedList = new ArrayList<>();
|
||||
float[] selectedSpeeds = UserPreferences.getPlaybackSpeedArray();
|
||||
for (float speed : selectedSpeeds) {
|
||||
selectedSpeedList.add(speedFormat.format(speed));
|
||||
}
|
||||
|
||||
for (int i = 0; i < speedValues.length; i++) {
|
||||
speedChecked[i] = selectedSpeedList.contains(speedValues[i]);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.set_playback_speed_label);
|
||||
builder.setMultiChoiceItems(R.array.playback_speed_values,
|
||||
speedChecked, (dialog, which, isChecked) -> speedChecked[which] = isChecked);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(android.R.string.ok,
|
||||
(dialog, which) -> {
|
||||
int choiceCount = 0;
|
||||
for (boolean checked : speedChecked) {
|
||||
if (checked) {
|
||||
choiceCount++;
|
||||
}
|
||||
}
|
||||
String[] newSpeedValues = new String[choiceCount];
|
||||
int newSpeedIndex = 0;
|
||||
for (int i = 0; i < speedChecked.length; i++) {
|
||||
if (speedChecked[i]) {
|
||||
newSpeedValues[newSpeedIndex++] = speedValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
UserPreferences.setPlaybackSpeedArray(newSpeedValues);
|
||||
|
||||
});
|
||||
builder.create().show();
|
||||
@Override
|
||||
public void onPlaybackSpeedChange() {
|
||||
updateSpeed();
|
||||
}
|
||||
};
|
||||
controller.init();
|
||||
speedSeekBar.setController(controller);
|
||||
}
|
||||
|
||||
private void updateSpeed() {
|
||||
speedSeekBar.updateSpeed();
|
||||
addCurrentSpeedChip.setText(speedFormat.format(controller.getCurrentPlaybackSpeedMultiplier()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
controller.release();
|
||||
controller = null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setPositiveButton(R.string.close_label, null);
|
||||
|
||||
View root = View.inflate(getContext(), R.layout.speed_select_dialog, null);
|
||||
speedSeekBar = root.findViewById(R.id.speed_seek_bar);
|
||||
RecyclerView selectedSpeedsGrid = root.findViewById(R.id.selected_speeds_grid);
|
||||
selectedSpeedsGrid.setLayoutManager(new GridLayoutManager(getContext(), 3));
|
||||
selectedSpeedsGrid.addItemDecoration(new ItemOffsetDecoration(getContext(), 4));
|
||||
adapter = new SpeedSelectionAdapter();
|
||||
adapter.setHasStableIds(true);
|
||||
selectedSpeedsGrid.setAdapter(adapter);
|
||||
|
||||
addCurrentSpeedChip = root.findViewById(R.id.add_current_speed_chip);
|
||||
addCurrentSpeedChip.setCloseIconVisible(true);
|
||||
addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add_black);
|
||||
addCurrentSpeedChip.setOnCloseIconClickListener(v -> addCurrentSpeed());
|
||||
addCurrentSpeedChip.setOnClickListener(v -> addCurrentSpeed());
|
||||
|
||||
builder.setView(root);
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private void addCurrentSpeed() {
|
||||
float newSpeed = controller.getCurrentPlaybackSpeedMultiplier();
|
||||
if (selectedSpeeds.contains(newSpeed)) {
|
||||
Snackbar.make(addCurrentSpeedChip,
|
||||
getString(R.string.preset_already_exists, newSpeed), Snackbar.LENGTH_LONG).show();
|
||||
} else {
|
||||
selectedSpeeds.add(newSpeed);
|
||||
Collections.sort(selectedSpeeds);
|
||||
UserPreferences.setPlaybackSpeedArray(selectedSpeeds);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedSelectionAdapter extends RecyclerView.Adapter<SpeedSelectionAdapter.ViewHolder> {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
Chip chip = new Chip(getContext());
|
||||
chip.setCloseIconVisible(true);
|
||||
chip.setCloseIconResource(R.drawable.ic_delete_black);
|
||||
return new ViewHolder(chip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
float speed = selectedSpeeds.get(position);
|
||||
|
||||
holder.chip.setText(speedFormat.format(speed));
|
||||
holder.chip.setOnCloseIconClickListener(v -> {
|
||||
selectedSpeeds.remove(speed);
|
||||
UserPreferences.setPlaybackSpeedArray(selectedSpeeds);
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
holder.chip.setOnClickListener(v -> {
|
||||
if (controller != null) {
|
||||
controller.setPlaybackSpeed(speed);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return selectedSpeeds.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return selectedSpeeds.get(position).hashCode();
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
Chip chip;
|
||||
|
||||
ViewHolder(Chip itemView) {
|
||||
super(itemView);
|
||||
chip = itemView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@ -21,17 +20,8 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.CastEnabledActivity;
|
||||
import de.danoeh.antennapod.activity.MainActivity;
|
||||
@ -40,7 +30,6 @@ import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
@ -60,6 +49,13 @@ import io.reactivex.Maybe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows the audio player.
|
||||
@ -206,30 +202,29 @@ public class AudioPlayerFragment extends Fragment implements
|
||||
VariableSpeedDialog.showGetPluginDialog(getContext());
|
||||
return;
|
||||
}
|
||||
float[] availableSpeeds = UserPreferences.getPlaybackSpeedArray();
|
||||
List<Float> availableSpeeds = UserPreferences.getPlaybackSpeedArray();
|
||||
float currentSpeed = controller.getCurrentPlaybackSpeedMultiplier();
|
||||
|
||||
int newSpeedIndex = 0;
|
||||
while (newSpeedIndex < availableSpeeds.length && availableSpeeds[newSpeedIndex] < currentSpeed + EPSILON) {
|
||||
while (newSpeedIndex < availableSpeeds.size()
|
||||
&& availableSpeeds.get(newSpeedIndex) < currentSpeed + EPSILON) {
|
||||
newSpeedIndex++;
|
||||
}
|
||||
|
||||
float newSpeed;
|
||||
if (availableSpeeds.length == 0) {
|
||||
if (availableSpeeds.size() == 0) {
|
||||
newSpeed = 1.0f;
|
||||
} else if (newSpeedIndex == availableSpeeds.length) {
|
||||
newSpeed = availableSpeeds[0];
|
||||
} else if (newSpeedIndex == availableSpeeds.size()) {
|
||||
newSpeed = availableSpeeds.get(0);
|
||||
} else {
|
||||
newSpeed = availableSpeeds[newSpeedIndex];
|
||||
newSpeed = availableSpeeds.get(newSpeedIndex);
|
||||
}
|
||||
|
||||
PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(newSpeed);
|
||||
UserPreferences.setPlaybackSpeed(newSpeed);
|
||||
controller.setPlaybackSpeed(newSpeed);
|
||||
loadMediaInfo();
|
||||
});
|
||||
butPlaybackSpeed.setOnLongClickListener(v -> {
|
||||
VariableSpeedDialog.showDialog(getContext());
|
||||
new VariableSpeedDialog().show(getChildFragmentManager(), null);
|
||||
return true;
|
||||
});
|
||||
butPlaybackSpeed.setVisibility(View.VISIBLE);
|
||||
@ -494,7 +489,7 @@ public class AudioPlayerFragment extends Fragment implements
|
||||
new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog");
|
||||
return true;
|
||||
case R.id.audio_controls:
|
||||
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance(false);
|
||||
PlaybackControlsDialog dialog = PlaybackControlsDialog.newInstance();
|
||||
dialog.show(getChildFragmentManager(), "playback_controls");
|
||||
return true;
|
||||
case R.id.open_feed_item:
|
||||
|
@ -45,7 +45,7 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
findPreference(PREF_PLAYBACK_SPEED_LAUNCHER).setOnPreferenceClickListener(preference -> {
|
||||
VariableSpeedDialog.showDialog(activity);
|
||||
new VariableSpeedDialog().show(getChildFragmentManager(), null);
|
||||
return true;
|
||||
});
|
||||
findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER).setOnPreferenceClickListener(preference -> {
|
||||
|
@ -0,0 +1,24 @@
|
||||
package de.danoeh.antennapod.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Source: https://stackoverflow.com/a/30794046
|
||||
*/
|
||||
public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int itemOffset;
|
||||
|
||||
public ItemOffsetDecoration(@NonNull Context context, int itemOffsetDp) {
|
||||
itemOffset = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
super.getItemOffsets(outRect, view, parent, state);
|
||||
outRect.set(itemOffset, itemOffset, itemOffset, itemOffset);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package de.danoeh.antennapod.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.SeekBar;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
|
||||
|
||||
public class PlaybackSpeedSeekBar extends FrameLayout {
|
||||
private SeekBar seekBar;
|
||||
private PlaybackController controller;
|
||||
private Consumer<Float> progressChangedListener;
|
||||
|
||||
public PlaybackSpeedSeekBar(@NonNull Context context) {
|
||||
super(context);
|
||||
setup();
|
||||
}
|
||||
|
||||
public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setup();
|
||||
}
|
||||
|
||||
public PlaybackSpeedSeekBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setup();
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
View.inflate(getContext(), R.layout.playback_speed_seek_bar, this);
|
||||
seekBar = findViewById(R.id.playback_speed);
|
||||
findViewById(R.id.butDecSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() - 2));
|
||||
findViewById(R.id.butIncSpeed).setOnClickListener(v -> seekBar.setProgress(seekBar.getProgress() + 2));
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (controller != null && controller.canSetPlaybackSpeed()) {
|
||||
float playbackSpeed = (progress + 10) / 20.0f;
|
||||
controller.setPlaybackSpeed(playbackSpeed);
|
||||
|
||||
if (progressChangedListener != null) {
|
||||
progressChangedListener.accept(playbackSpeed);
|
||||
}
|
||||
} else if (fromUser) {
|
||||
seekBar.post(() -> updateSpeed());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (controller != null && !controller.canSetPlaybackSpeed()) {
|
||||
VariableSpeedDialog.showGetPluginDialog(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateSpeed() {
|
||||
if (controller != null) {
|
||||
seekBar.setProgress(Math.round((20 * controller.getCurrentPlaybackSpeedMultiplier()) - 10));
|
||||
}
|
||||
}
|
||||
|
||||
public void setController(PlaybackController controller) {
|
||||
this.controller = controller;
|
||||
updateSpeed();
|
||||
if (progressChangedListener != null && controller != null) {
|
||||
progressChangedListener.accept(controller.getCurrentPlaybackSpeedMultiplier());
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgressChangedListener(Consumer<Float> progressChangedListener) {
|
||||
this.progressChangedListener = progressChangedListener;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -38,47 +39,10 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<de.danoeh.antennapod.view.PlaybackSpeedSeekBar
|
||||
android:id="@+id/speed_seek_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butDecSpeed"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:contentDescription="@string/decrease_speed"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/playback_speed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:max="70"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butIncSpeed"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:gravity="center"
|
||||
android:text="+"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:contentDescription="@string/increase_speed"
|
||||
android:background="?attr/selectableItemBackgroundBorderless" />
|
||||
</LinearLayout>
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -184,7 +184,7 @@
|
||||
android:layout_toStartOf="@id/butRev"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/set_playback_speed_label"
|
||||
android:contentDescription="@string/playback_speed"
|
||||
tools:srcCompat="@drawable/ic_playback_speed_white"/>
|
||||
|
||||
<TextView
|
||||
|
43
app/src/main/res/layout/playback_speed_seek_bar.xml
Normal file
43
app/src/main/res/layout/playback_speed_seek_bar.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butDecSpeed"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:gravity="center"
|
||||
android:text="-"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:contentDescription="@string/decrease_speed"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/playback_speed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:max="70"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butIncSpeed"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:gravity="center"
|
||||
android:text="+"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:contentDescription="@string/increase_speed"
|
||||
android:background="?attr/selectableItemBackgroundBorderless" />
|
||||
</LinearLayout>
|
45
app/src/main/res/layout/speed_select_dialog.xml
Normal file
45
app/src/main/res/layout/speed_select_dialog.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:text="@string/playback_speed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="@style/AntennaPod.TextView.ListItemPrimaryTitle"/>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/add_current_speed_chip"/>
|
||||
</LinearLayout>
|
||||
|
||||
<de.danoeh.antennapod.view.PlaybackSpeedSeekBar
|
||||
android:id="@+id/speed_seek_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp">
|
||||
</de.danoeh.antennapod.view.PlaybackSpeedSeekBar>
|
||||
|
||||
<TextView
|
||||
android:text="@string/speed_presets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/selected_speeds_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
@ -67,7 +67,7 @@
|
||||
<Preference
|
||||
android:key="prefPlaybackSpeedLauncher"
|
||||
android:summary="@string/pref_playback_speed_sum"
|
||||
android:title="@string/pref_playback_speed_title"/>
|
||||
android:title="@string/playback_speed"/>
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="prefPlaybackTimeRespectsSpeed"
|
||||
|
@ -19,10 +19,14 @@ import org.json.JSONException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -414,7 +418,7 @@ public class UserPreferences {
|
||||
return prefs.getBoolean(PREF_PLAYBACK_SKIP_SILENCE, false);
|
||||
}
|
||||
|
||||
public static float[] getPlaybackSpeedArray() {
|
||||
public static List<Float> getPlaybackSpeedArray() {
|
||||
return readPlaybackSpeedArray(prefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null));
|
||||
}
|
||||
|
||||
@ -662,10 +666,13 @@ public class UserPreferences {
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static void setPlaybackSpeedArray(String[] speeds) {
|
||||
public static void setPlaybackSpeedArray(List<Float> speeds) {
|
||||
DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US);
|
||||
format.setDecimalSeparator('.');
|
||||
DecimalFormat speedFormat = new DecimalFormat("0.00", format);
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
for (String speed : speeds) {
|
||||
jsonArray.put(speed);
|
||||
for (float speed : speeds) {
|
||||
jsonArray.put(speedFormat.format(speed));
|
||||
}
|
||||
prefs.edit()
|
||||
.putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
|
||||
@ -775,13 +782,13 @@ public class UserPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] readPlaybackSpeedArray(String valueFromPrefs) {
|
||||
private static List<Float> readPlaybackSpeedArray(String valueFromPrefs) {
|
||||
if (valueFromPrefs != null) {
|
||||
try {
|
||||
JSONArray jsonArray = new JSONArray(valueFromPrefs);
|
||||
float[] selectedSpeeds = new float[jsonArray.length()];
|
||||
List<Float> selectedSpeeds = new ArrayList<>();
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
selectedSpeeds[i] = (float) jsonArray.getDouble(i);
|
||||
selectedSpeeds.add((float) jsonArray.getDouble(i));
|
||||
}
|
||||
return selectedSpeeds;
|
||||
} catch (JSONException e) {
|
||||
@ -790,7 +797,7 @@ public class UserPreferences {
|
||||
}
|
||||
}
|
||||
// If this preference hasn't been set yet, return the default options
|
||||
return new float[] { 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
|
||||
return Arrays.asList(0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f);
|
||||
}
|
||||
|
||||
public static String getMediaPlayer() {
|
||||
|
@ -599,6 +599,13 @@ public class PlaybackController {
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(float speed) {
|
||||
PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed);
|
||||
if (getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO) {
|
||||
UserPreferences.setVideoPlaybackSpeed(speed);
|
||||
} else {
|
||||
UserPreferences.setPlaybackSpeed(speed);
|
||||
}
|
||||
|
||||
if (playbackService != null) {
|
||||
playbackService.setSpeed(speed);
|
||||
} else {
|
||||
|
@ -311,8 +311,9 @@
|
||||
<string name="download_plugin_label">Download Plugin</string>
|
||||
<string name="no_playback_plugin_title">Plugin Not Installed</string>
|
||||
<string name="no_playback_plugin_or_sonic_msg">For variable speed playback to work, we recommend to enable the built-in Sonic mediaplayer.</string>
|
||||
<string name="set_playback_speed_label">Playback Speeds</string>
|
||||
<string name="enable_sonic">Enable Sonic</string>
|
||||
<string name="speed_presets">Presets</string>
|
||||
<string name="preset_already_exists">%1$.2fx is already saved as a preset.</string>
|
||||
|
||||
<!-- Empty list labels -->
|
||||
<string name="no_items_header_label">No queued episodes</string>
|
||||
@ -441,8 +442,7 @@
|
||||
<string name="pref_gpodnet_login_status"><![CDATA[Logged in as <i>%1$s</i> with device <i>%2$s</i>]]></string>
|
||||
<string name="pref_gpodnet_notifications_title">Show sync error notifications</string>
|
||||
<string name="pref_gpodnet_notifications_sum">This setting does not apply to authentication errors.</string>
|
||||
<string name="pref_playback_speed_title">Playback Speeds</string>
|
||||
<string name="pref_playback_speed_sum">Customize the speeds available for variable speed audio playback</string>
|
||||
<string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string>
|
||||
<string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string>
|
||||
<string name="pref_feed_skip">Auto Skip</string>
|
||||
<string name="pref_feed_skip_sum">Skip introductions and ending credits.</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user