New media player screen (#5075)

Co-authored-by: jonasburian <jonas.burian@protonmail.com>
Co-authored-by: ByteHamster <info@bytehamster.com>
This commit is contained in:
ueen 2021-05-14 21:06:04 +02:00 committed by GitHub
parent fb6bd0cbaa
commit 292c9bf151
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 610 additions and 204 deletions

View File

@ -20,6 +20,7 @@ import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
@ -32,16 +33,23 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
@ -57,12 +65,8 @@ import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.fragment.TransitionEffect;
import de.danoeh.antennapod.preferences.PreferenceUpgrader;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.LockableBottomSheetBehavior;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* The activity that is shown when the user launches the app.
@ -184,6 +188,11 @@ public class MainActivity extends CastEnabledActivity {
if (audioPlayer == null) {
return;
}
if (slideOffset == 0.0f) { //STATE_COLLAPSED
audioPlayer.scrollToPage(AudioPlayerFragment.POS_COVER);
}
float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
audioPlayer.getExternalPlayerHolder().setAlpha(1 - condensedSlideOffset);
audioPlayer.getExternalPlayerHolder().setVisibility(

View File

@ -11,6 +11,7 @@ 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;
@ -20,10 +21,18 @@ import androidx.fragment.app.Fragment;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
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 com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
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;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity;
@ -48,20 +57,13 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.ChapterSeekBar;
import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView;
import de.danoeh.antennapod.view.ChapterSeekBar;
import de.danoeh.antennapod.view.PlayButton;
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.
@ -69,10 +71,9 @@ import java.util.List;
public class AudioPlayerFragment extends Fragment implements
ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
public static final String TAG = "AudioPlayerFragment";
private static final int POS_COVER = 0;
private static final int POS_SHOWNOTES = 1;
private static final int POS_CHAPTERS = 2;
private static final int NUM_CONTENT_FRAGMENTS = 3;
public static final int POS_COVER = 0;
public static final int POS_DESCRIPTION = 1;
private static final int NUM_CONTENT_FRAGMENTS = 2;
private static final float EPSILON = 0.001f;
PlaybackSpeedIndicatorView butPlaybackSpeed;
@ -95,11 +96,9 @@ public class AudioPlayerFragment extends Fragment implements
private PlaybackController controller;
private Disposable disposable;
private boolean showTimeLeft;
private boolean hasChapters = false;
private boolean seekedToChapterStart = false;
private int currentChapterIndex = -1;
private int duration;
private TabLayoutMediator tabLayoutMediator;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@ -155,36 +154,9 @@ public class AudioPlayerFragment extends Fragment implements
}
});
TabLayout tabLayout = root.findViewById(R.id.sliding_tabs);
tabLayoutMediator = new TabLayoutMediator(tabLayout, pager, (tab, position) -> {
tab.view.setAlpha(1.0f);
switch (position) {
case POS_COVER:
tab.setText(R.string.cover_label);
break;
case POS_SHOWNOTES:
tab.setText(R.string.shownotes_label);
break;
case POS_CHAPTERS:
tab.setText(R.string.chapters_label);
if (!hasChapters) {
tab.view.setAlpha(0.5f);
}
break;
default:
break;
}
});
tabLayoutMediator.attach();
return root;
}
private void setHasChapters(boolean hasChapters) {
this.hasChapters = hasChapters;
tabLayoutMediator.detach();
tabLayoutMediator.attach();
}
private void setChapterDividers(Playable media) {
if (media == null) {
@ -193,7 +165,7 @@ public class AudioPlayerFragment extends Fragment implements
float[] dividerPos = null;
if (hasChapters) {
if (media.getChapters() != null) {
List<Chapter> chapters = media.getChapters();
dividerPos = new float[chapters.size()];
@ -201,7 +173,7 @@ public class AudioPlayerFragment extends Fragment implements
dividerPos[i] = chapters.get(i).getStart() / (float) duration;
}
}
sbPosition.setDividerPos(dividerPos);
}
@ -417,16 +389,7 @@ public class AudioPlayerFragment extends Fragment implements
if (controller == null) {
return;
}
duration = controller.getDuration();
if (media != null && media.getChapters() != null) {
setHasChapters(media.getChapters().size() > 0);
currentChapterIndex = ChapterUtils.getCurrentChapterIndex(media, controller.getPosition());
} else {
setHasChapters(false);
currentChapterIndex = -1;
}
updatePosition(new PlaybackPositionEvent(controller.getPosition(), duration));
updatePlaybackSpeedButton(media);
setChapterDividers(media);
@ -472,6 +435,7 @@ public class AudioPlayerFragment extends Fragment implements
int currentPosition = converter.convert(event.getPosition());
int duration = converter.convert(event.getDuration());
int remainingTime = converter.convert(Math.max(event.getDuration() - event.getPosition(), 0));
currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller.getMedia(), currentPosition);
Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == PlaybackService.INVALID_TIME || duration == PlaybackService.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time");
@ -620,14 +584,13 @@ public class AudioPlayerFragment extends Fragment implements
@Override
public Fragment createFragment(int position) {
Log.d(TAG, "getItem(" + position + ")");
switch (position) {
case POS_COVER:
return new CoverFragment();
case POS_SHOWNOTES:
return new ItemDescriptionFragment();
default:
case POS_CHAPTERS:
return new ChaptersFragment();
case POS_DESCRIPTION:
return new ItemDescriptionFragment();
}
}
@ -636,4 +599,21 @@ public class AudioPlayerFragment extends Fragment implements
return NUM_CONTENT_FRAGMENTS;
}
}
public void scrollToPage(int page, boolean smoothScroll) {
if (pager == null) {
return;
}
pager.setCurrentItem(page, smoothScroll);
Fragment visibleChild = getChildFragmentManager().findFragmentByTag("f" + POS_DESCRIPTION);
if (visibleChild instanceof ItemDescriptionFragment) {
((ItemDescriptionFragment) visibleChild).scrollToTop();
}
}
public void scrollToPage(int page) {
scrollToPage(page, false);
}
}

View File

@ -1,48 +1,65 @@
package de.danoeh.antennapod.fragment;
import android.app.Dialog;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.view.EmptyViewHandler;
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;
public class ChaptersFragment extends Fragment {
private static final String TAG = "ChaptersFragment";
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.model.playback.Playable;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class ChaptersFragment extends AppCompatDialogFragment {
public static final String TAG = "ChaptersFragment";
private ChaptersListAdapter adapter;
private PlaybackController controller;
private Disposable disposable;
private int focusedChapter = -1;
private Playable media;
private LinearLayoutManager layoutManager;
private ProgressBar progressBar;
@Nullable
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.simple_list_fragment, container, false);
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.chapters_label))
.setView(onCreateView(getLayoutInflater()))
.setNegativeButton(getString(R.string.cancel_label), null) //dismisses
.create();
}
public View onCreateView(@NonNull LayoutInflater inflater) {
View root = inflater.inflate(R.layout.simple_list_fragment, null, false);
root.findViewById(R.id.toolbar).setVisibility(View.GONE);
RecyclerView recyclerView = root.findViewById(R.id.recyclerView);
progressBar = root.findViewById(R.id.progLoading);
layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(),
@ -58,11 +75,11 @@ public class ChaptersFragment extends Fragment {
});
recyclerView.setAdapter(adapter);
EmptyViewHandler emptyView = new EmptyViewHandler(getContext());
emptyView.attachToRecyclerView(recyclerView);
emptyView.setIcon(R.drawable.ic_bookmark);
emptyView.setTitle(R.string.no_chapters_head_label);
emptyView.setMessage(R.string.no_chapters_label);
progressBar.setVisibility(View.VISIBLE);
RelativeLayout.LayoutParams wrapHeight = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
recyclerView.setLayoutParams(wrapHeight);
return root;
}
@ -136,6 +153,11 @@ public class ChaptersFragment extends Fragment {
if (adapter == null) {
return;
}
if (media.getChapters() != null && media.getChapters().size() <= 0) {
dismiss();
} else {
progressBar.setVisibility(View.GONE);
}
adapter.setMedia(media);
int positionOfCurrentChapter = getCurrentChapter(media);
updateChapterSelection(positionOfCurrentChapter);

View File

@ -1,7 +1,9 @@
package de.danoeh.antennapod.fragment;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
@ -10,17 +12,29 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Space;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
import androidx.fragment.app.Fragment;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.RequestOptions;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
@ -28,16 +42,13 @@ import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.model.feed.Chapter;
import de.danoeh.antennapod.model.playback.Playable;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Displays the cover and the title of a FeedItem.
@ -51,9 +62,16 @@ public class CoverFragment extends Fragment {
private TextView txtvPodcastTitle;
private TextView txtvEpisodeTitle;
private ImageView imgvCover;
private LinearLayout openDescription;
private Space counterweight;
private Space spacer;
private ImageButton butPrevChapter;
private ImageButton butNextChapter;
private LinearLayout episodeDetails;
private LinearLayout chapterControl;
private PlaybackController controller;
private Disposable disposable;
private int displayedChapterIndex = -2;
private int displayedChapterIndex = -1;
private Playable media;
@Override
@ -64,7 +82,30 @@ public class CoverFragment extends Fragment {
txtvPodcastTitle = root.findViewById(R.id.txtvPodcastTitle);
txtvEpisodeTitle = root.findViewById(R.id.txtvEpisodeTitle);
imgvCover = root.findViewById(R.id.imgvCover);
episodeDetails = root.findViewById(R.id.episode_details);
final ImageView descriptionIcon = root.findViewById(R.id.description_icon);
chapterControl = root.findViewById(R.id.chapterButton);
butPrevChapter = root.findViewById(R.id.butPrevChapter);
butNextChapter = root.findViewById(R.id.butNextChapter);
imgvCover.setOnClickListener(v -> onPlayPause());
openDescription = root.findViewById(R.id.openDescription);
counterweight = root.findViewById(R.id.counterweight);
spacer = root.findViewById(R.id.details_spacer);
View.OnClickListener scrollToDesc = view ->
((AudioPlayerFragment) requireParentFragment()).scrollToPage(AudioPlayerFragment.POS_DESCRIPTION, true);
openDescription.setOnClickListener(scrollToDesc);
ColorFilter colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
txtvPodcastTitle.getCurrentTextColor(), BlendModeCompat.SRC_IN);
butNextChapter.setColorFilter(colorFilter);
butPrevChapter.setColorFilter(colorFilter);
descriptionIcon.setColorFilter(colorFilter);
ChaptersFragment chaptersFragment = new ChaptersFragment();
chapterControl.setOnClickListener(v ->
chaptersFragment.show(getChildFragmentManager(), ChaptersFragment.TAG));
butPrevChapter.setOnClickListener(v -> seekToPrevChapter());
butNextChapter.setOnClickListener(v -> seekToNextChapter());
return root;
}
@ -80,6 +121,7 @@ public class CoverFragment extends Fragment {
disposable = Maybe.<Playable>create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
ChapterUtils.loadChapters(media, getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
@ -100,8 +142,73 @@ public class CoverFragment extends Fragment {
+ "\u00A0"
+ StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0"));
txtvEpisodeTitle.setText(media.getEpisodeTitle());
displayedChapterIndex = -2; // Force refresh
displayCoverImage(media.getPosition());
displayedChapterIndex = -1;
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())); //calls displayCoverImage
updateChapterControlVisibility();
}
private void updateChapterControlVisibility() {
if (media.getChapters() != null) {
boolean chapterControlVisible = media.getChapters().size() > 0;
int newVisibility = chapterControlVisible ? View.VISIBLE : View.GONE;
if (chapterControl.getVisibility() != newVisibility) {
chapterControl.setVisibility(newVisibility);
ObjectAnimator.ofFloat(chapterControl,
"alpha",
chapterControlVisible ? 0 : 1,
chapterControlVisible ? 1 : 0)
.start();
}
}
}
private void refreshChapterData(int chapterIndex) {
if (chapterIndex > -1) {
if (media.getPosition() > media.getDuration() || chapterIndex >= media.getChapters().size() - 1) {
displayedChapterIndex = media.getChapters().size() - 1;
butNextChapter.setVisibility(View.INVISIBLE);
} else {
displayedChapterIndex = chapterIndex;
butNextChapter.setVisibility(View.VISIBLE);
}
}
displayCoverImage();
}
private Chapter getCurrentChapter() {
if (media == null || media.getChapters() == null || displayedChapterIndex == -1) {
return null;
}
return media.getChapters().get(displayedChapterIndex);
}
private void seekToPrevChapter() {
Chapter curr = getCurrentChapter();
if (controller == null || curr == null || displayedChapterIndex == -1) {
return;
}
if (displayedChapterIndex < 1) {
controller.seekTo(0);
} else if ((controller.getPosition() - 10000 * controller.getCurrentPlaybackSpeedMultiplier())
< curr.getStart()) {
refreshChapterData(displayedChapterIndex - 1);
controller.seekToChapter(media.getChapters().get(displayedChapterIndex));
} else {
controller.seekToChapter(curr);
}
}
private void seekToNextChapter() {
if (controller == null || media == null || media.getChapters() == null
|| displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media.getChapters().size()) {
return;
}
refreshChapterData(displayedChapterIndex + 1);
controller.seekToChapter(media.getChapters().get(displayedChapterIndex));
}
@Override
@ -139,22 +246,18 @@ public class CoverFragment extends Fragment {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (media == null) {
return;
int newChapterIndex = ChapterUtils.getCurrentChapterIndex(media, event.getPosition());
if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) {
refreshChapterData(newChapterIndex);
}
displayCoverImage(event.getPosition());
}
private void displayCoverImage(int position) {
int chapter = ChapterUtils.getCurrentChapterIndex(media, position);
if (chapter != displayedChapterIndex) {
displayedChapterIndex = chapter;
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transforms(new FitCenter(),
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)));
private void displayCoverImage() {
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.dontAnimate()
.transforms(new FitCenter(),
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)));
RequestBuilder<Drawable> cover = Glide.with(this)
.load(media.getImageLocation())
@ -163,16 +266,16 @@ public class CoverFragment extends Fragment {
.apply(options))
.apply(options);
if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) {
cover.into(imgvCover);
} else {
Glide.with(this)
.load(EmbeddedChapterImage.getModelFor(media, chapter))
.apply(options)
.thumbnail(cover)
.error(cover)
.into(imgvCover);
}
if (displayedChapterIndex == -1 || media == null || media.getChapters() == null
|| TextUtils.isEmpty(media.getChapters().get(displayedChapterIndex).getImageUrl())) {
cover.into(imgvCover);
} else {
Glide.with(this)
.load(EmbeddedChapterImage.getModelFor(media, displayedChapterIndex))
.apply(options)
.thumbnail(cover)
.error(cover)
.into(imgvCover);
}
}
@ -196,6 +299,10 @@ public class CoverFragment extends Fragment {
LinearLayout.LayoutParams textParams = (LinearLayout.LayoutParams) textContainer.getLayoutParams();
double ratio = (float) newConfig.screenHeightDp / (float) newConfig.screenWidthDp;
boolean spacerVisible = true;
ViewGroup detailsParent = (ViewGroup) getView();
int detailsWidth = ViewGroup.LayoutParams.MATCH_PARENT;
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
double percentageWidth = 0.8;
if (ratio <= SIXTEEN_BY_NINE) {
@ -217,6 +324,26 @@ public class CoverFragment extends Fragment {
textParams.weight = 1;
imgvCover.setLayoutParams(params);
}
spacerVisible = false;
detailsParent = textContainer;
detailsWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
}
if (displayedChapterIndex == -1) {
detailsWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
}
spacer.setVisibility(spacerVisible ? View.VISIBLE : View.GONE);
counterweight.setVisibility(spacerVisible ? View.VISIBLE : View.GONE);
LinearLayout.LayoutParams wrapHeight =
new LinearLayout.LayoutParams(detailsWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
episodeDetails.setLayoutParams(wrapHeight);
getView().findViewById(R.id.vertical_divider).setVisibility(spacerVisible ? View.GONE : View.VISIBLE);
if (episodeDetails.getParent() != detailsParent) {
((ViewGroup) episodeDetails.getParent()).removeView(episodeDetails);
detailsParent.addView(episodeDetails);
}
}

View File

@ -8,13 +8,15 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.view.ShownotesWebView;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -143,14 +145,18 @@ public class ItemDescriptionFragment extends Fragment {
&& id.equals(controller.getMedia().getIdentifier().toString())
&& webvDescription != null) {
Log.d(TAG, "Restored scroll Position: " + scrollY);
webvDescription.scrollTo(webvDescription.getScrollX(),
scrollY);
webvDescription.scrollTo(webvDescription.getScrollX(), scrollY);
return true;
}
}
return false;
}
public void scrollToTop() {
webvDescription.scrollTo(0, 0);
savePreference();
}
@Override
public void onStart() {
super.onStart();

View File

@ -20,92 +20,181 @@
package de.danoeh.antennapod.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.viewpager2.widget.ViewPager2;
import de.danoeh.antennapod.R;
import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL;
import static androidx.viewpager2.widget.ViewPager2.ORIENTATION_VERTICAL;
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* <p>This solution has limitations when using multiple levels of nested scrollable elements
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost extends FrameLayout {
*/ // KhaledAlharthi/NestedScrollableHost.java
public class NestedScrollableHost extends FrameLayout {
private ViewPager2 parentViewPager;
private int touchSlop = 0;
private float initialX = 0f;
private float initialY = 0f;
private int preferVertical = 1;
private int preferHorizontal = 1;
private int scrollDirection = 0;
public NestedScrollableHost(@NonNull Context context) {
super(context);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
init(context);
}
public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
setAttributes(context, attrs);
}
public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
setAttributes(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
setAttributes(context, attrs);
}
private void setAttributes(@NonNull Context context, @Nullable AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.NestedScrollableHost,
0, 0);
try {
preferHorizontal = a.getInteger(R.styleable.NestedScrollableHost_preferHorizontal, 1);
preferVertical = a.getInteger(R.styleable.NestedScrollableHost_preferVertical, 1);
scrollDirection = a.getInteger(R.styleable.NestedScrollableHost_scrollDirection, 0);
} finally {
a.recycle();
}
}
private void init(Context context) {
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
View v = (View) getParent();
while (v != null && !(v instanceof ViewPager2) || isntSameDirection(v)) {
v = (View) v.getParent();
}
parentViewPager = (ViewPager2) v;
getViewTreeObserver().removeOnPreDrawListener(this);
return false;
}
});
}
private int touchSlop;
private float initialX = 0f;
private float initialY = 0f;
private ViewPager2 getParentViewPager() {
View v = (View) getParent();
while (v != null && !(v instanceof ViewPager2)) {
v = (View) v.getParent();
private Boolean isntSameDirection(View v) {
int orientation = 0;
switch (scrollDirection) {
default:
case 0:
return false;
case 1:
orientation = ORIENTATION_VERTICAL;
break;
case 2:
orientation = ORIENTATION_HORIZONTAL;
break;
}
return v == null ? null : (ViewPager2) v;
return ((v instanceof ViewPager2) && ((ViewPager2) v).getOrientation() != orientation);
}
public boolean onInterceptTouchEvent(MotionEvent e) {
ViewPager2 parentViewPager = getParentViewPager();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
handleInterceptTouchEvent(ev);
return super.onInterceptTouchEvent(ev);
}
private boolean canChildScroll(int orientation, float delta) {
int direction = (int) -delta;
View child = getChildAt(0);
if (orientation == 0) {
return child.canScrollHorizontally(direction);
} else if (orientation == 1) {
return child.canScrollVertically(direction);
} else {
throw new IllegalArgumentException();
}
}
private void handleInterceptTouchEvent(MotionEvent e) {
if (parentViewPager == null) {
return super.onInterceptTouchEvent(e);
return;
}
int orientation = parentViewPager.getOrientation();
boolean preferedDirection = preferHorizontal + preferVertical > 2;
// Early return if child can't scroll in same direction as parent and theres no prefered scroll direction
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f) && !preferedDirection) {
return;
}
ViewParent parent = getParent();
int orientation = parentViewPager.getOrientation();
if (e.getAction() == MotionEvent.ACTION_DOWN) {
initialX = e.getX();
initialY = e.getY();
parent.requestDisallowInterceptTouchEvent(true);
getParent().requestDisallowInterceptTouchEvent(true);
} else if (e.getAction() == MotionEvent.ACTION_MOVE) {
int dx = (int) (e.getX() - initialX);
int dy = (int) (e.getY() - initialY);
boolean isVpHorizontal = orientation == ORIENTATION_HORIZONTAL;
float dx = e.getX() - initialX;
float dy = e.getY() - initialY;
boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;
// assuming ViewPager2 touch-slop is 2x touch-slop of child
float scaledDx = Math.abs(dx) * (isVpHorizontal ? .5f : 1f);
float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1f : .5f);
float scaledDx = Math.abs(dx) * (isVpHorizontal ? 1f : 0.5f) * preferHorizontal;
float scaledDy = Math.abs(dy) * (isVpHorizontal ? 0.5f : 1f) * preferVertical;
if (scaledDx > touchSlop || scaledDy > touchSlop) {
int value = isVpHorizontal ? dy : dx;
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular
orientation = orientation == ORIENTATION_VERTICAL
? ORIENTATION_HORIZONTAL : ORIENTATION_VERTICAL;
value = isVpHorizontal ? dy : dx;
}
int direction = (int) -Math.copySign(1, value);
View child = getChildAt(0);
if (orientation == ORIENTATION_HORIZONTAL) {
parent.requestDisallowInterceptTouchEvent(child.canScrollHorizontally(direction));
// Gesture is perpendicular, allow all parents to intercept
getParent().requestDisallowInterceptTouchEvent(preferedDirection);
} else {
parent.requestDisallowInterceptTouchEvent(child.canScrollVertically(direction));
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {
// Child can scroll, disallow all parents to intercept
getParent().requestDisallowInterceptTouchEvent(true);
} else {
// Child cannot scroll, allow all parents to intercept
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
}
return super.onInterceptTouchEvent(e);
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape
android:shape="rectangle">
<corners
android:radius="8dp"
android:topRightRadius="8dp"
android:bottomRightRadius="8dp"
android:bottomLeftRadius="8dp" />
<solid android:color="@android:color/white"/>
</shape>
</item>
<item>
<shape
android:shape="rectangle">
<corners
android:radius="8dp"
android:topRightRadius="8dp"
android:bottomRightRadius="8dp"
android:bottomLeftRadius="8dp" />
<stroke
android:width="1dp"
android:color="?android:attr/textColorSecondary" />
<solid android:color="@android:color/transparent"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector>
<item>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="8dp"
android:topRightRadius="8dp"
android:bottomRightRadius="8dp"
android:bottomLeftRadius="8dp" />
<stroke
android:width="1dp"
android:color="@android:color/darker_gray" />
<solid android:color="@android:color/transparent"/>
</shape>
</item>
</selector>

View File

@ -15,16 +15,6 @@
app:navigationIcon="?homeAsUpIndicator"
android:id="@+id/toolbar"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?android:attr/windowBackground"
app:tabBackground="?attr/selectableItemBackground"
app:tabMode="fixed"
app:tabGravity="fill"/>
<FrameLayout
android:id="@+id/playerFragment"
android:layout_width="match_parent"
@ -39,9 +29,10 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@id/playtime_layout"
android:layout_below="@id/sliding_tabs"
android:layout_below="@id/toolbar"
android:layout_marginBottom="12dp"
android:foreground="?android:windowContentOverlay"
android:layout_marginBottom="12dp"/>
android:orientation="vertical" />
<ImageView
android:layout_width="match_parent"

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -10,14 +11,18 @@
android:padding="8dp"
android:gravity="center">
<Space
android:id="@+id/counterweight"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
<de.danoeh.antennapod.ui.common.SquareImageView
android:id="@+id/imgvCover"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="16dp"
android:layout_weight="0"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="no"
@ -29,9 +34,9 @@
android:id="@+id/cover_fragment_text_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
android:layout_marginVertical="8dp"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/txtvPodcastTitle"
@ -40,9 +45,9 @@
android:ellipsize="none"
android:gravity="center_horizontal"
android:maxLines="2"
android:textSize="@dimen/text_size_small"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_small"
tools:text="Podcast" />
<TextView
@ -51,11 +56,114 @@
android:layout_height="wrap_content"
android:ellipsize="none"
android:gravity="center_horizontal"
android:textSize="@dimen/text_size_small"
android:maxLines="2"
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_small"
tools:text="Episode" />
<Space
android:id="@+id/vertical_divider"
android:layout_width="match_parent"
android:layout_height="8dp"
android:visibility="gone" />
</LinearLayout>
<Space
android:id="@+id/details_spacer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/episode_details"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_weight="0"
android:baselineAligned="false"
android:orientation="horizontal"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<LinearLayout
android:id="@+id/openDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:background="@drawable/grey_border"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:minWidth="150dp"
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/description_icon"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:contentDescription="@string/shownotes_contentdescription"
android:padding="2dp"
app:srcCompat="@drawable/ic_info" />
<TextView
android:id="@+id/shownotes_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:gravity="center_horizontal"
android:maxLines="2"
android:text="@string/shownotes_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/chapterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_weight="1"
android:background="@drawable/grey_border"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:minWidth="150dp"
android:orientation="horizontal"
android:visibility="gone">
<ImageButton
android:id="@+id/butPrevChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/prev_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_prev" />
<TextView
android:id="@+id/chapters_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/chapters_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_navdrawer" />
<ImageButton
android:id="@+id/butNextChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/next_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_next" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -185,7 +186,8 @@
<de.danoeh.antennapod.view.NestedScrollableHost
android:layout_below="@id/header"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
app:preferVertical="3">
<de.danoeh.antennapod.view.ShownotesWebView
android:id="@+id/webvDescription"

View File

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<de.danoeh.antennapod.view.NestedScrollableHost xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fillViewport="false"
app:preferVertical="10"
android:nestedScrollingEnabled="true">
<de.danoeh.antennapod.view.NestedScrollableHost
<de.danoeh.antennapod.view.ShownotesWebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"/>
<de.danoeh.antennapod.view.ShownotesWebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</de.danoeh.antennapod.view.NestedScrollableHost>
</androidx.core.widget.NestedScrollView>
</de.danoeh.antennapod.view.NestedScrollableHost>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NestedScrollableHost">
<attr name="scrollDirection" format="enum">
<enum name="both" value="0"/>
<enum name="vertical" value="1"/>
<enum name="horizontal" value="2"/>
</attr>
<attr name="preferHorizontal" format="integer"/>
<attr name="preferVertical" format="integer"/>
</declare-styleable>
</resources>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color" android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6 -6,-6z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

View File

@ -87,7 +87,6 @@
<string name="url_label">URL</string>
<string name="support_funding_label">Support</string>
<string name="support_podcast">Support this Podcast</string>
<string name="cover_label">Cover</string>
<string name="error_label">Error</string>
<string name="error_msg_prefix">An error occurred:</string>
<string name="refresh_label">Refresh</string>
@ -96,6 +95,7 @@
<string name="chapter_duration">Duration: %1$s</string>
<string name="description_label">Description</string>
<string name="shownotes_label">Shownotes</string>
<string name="shownotes_contentdescription">swipe up to read shownotes</string>
<string name="episodes_suffix">\u0020episodes</string>
<string name="processing_label">Processing</string>
<string name="close_label">Close</string>
@ -342,8 +342,6 @@
<string name="no_new_episodes_label">When new episodes arrive, they will be shown here.</string>
<string name="no_fav_episodes_head_label">No favorite episodes</string>
<string name="no_fav_episodes_label">You can add episodes to the favorites by long-pressing them.</string>
<string name="no_chapters_head_label">No chapters</string>
<string name="no_chapters_label">This episode has no chapters.</string>
<string name="no_subscriptions_head_label">No subscriptions</string>
<string name="no_subscriptions_label">To subscribe to a podcast, press the plus icon below.</string>
@ -681,6 +679,8 @@
<string name="position">Position: %1$s</string>
<string name="apply_action">Apply action</string>
<string name="play_chapter">Play chapter</string>
<string name="prev_chapter">Previous chapter</string>
<string name="next_chapter">Next chapter</string>
<!-- Feed settings/information screen -->
<string name="authentication_label">Authentication</string>