diff options
author | ueen <ueli.sarnighausen@online.de> | 2021-05-14 21:06:04 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-14 21:06:04 +0200 |
commit | 292c9bf15136f76fea8928ed7abb2b55fb316678 (patch) | |
tree | 144f3969825b2b910e6bc0640325942bb8ed969e /app/src/main/java/de | |
parent | fb6bd0cbaa909b6067fedef1e8a108130841f3fe (diff) | |
download | AntennaPod-292c9bf15136f76fea8928ed7abb2b55fb316678.zip |
New media player screen (#5075)
Co-authored-by: jonasburian <jonas.burian@protonmail.com>
Co-authored-by: ByteHamster <info@bytehamster.com>
Diffstat (limited to 'app/src/main/java/de')
6 files changed, 388 insertions, 155 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index 6afe27d17..f07ad6ad5 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -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( diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index d5807cd90..d54ea566b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -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); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index a98901081..6a8d648ad 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -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 org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + 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 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.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -public class ChaptersFragment extends Fragment { - private static final String TAG = "ChaptersFragment"; +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); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 5e1cda43c..f60930a35 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -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); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 6c8a967ae..518450f92 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -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(); diff --git a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java b/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java index 0e1846f1c..ff52df71f 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java +++ b/app/src/main/java/de/danoeh/antennapod/view/NestedScrollableHost.java @@ -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 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 instanceof ViewPager2) && ((ViewPager2) v).getOrientation() != orientation); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + handleInterceptTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } - private ViewPager2 getParentViewPager() { - View v = (View) getParent(); - while (v != null && !(v instanceof ViewPager2)) { - v = (View) v.getParent(); + + 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(); } - return v == null ? null : (ViewPager2) v; } - public boolean onInterceptTouchEvent(MotionEvent e) { - ViewPager2 parentViewPager = getParentViewPager(); + private void handleInterceptTouchEvent(MotionEvent e) { if (parentViewPager == null) { - return super.onInterceptTouchEvent(e); + return; } - - ViewParent parent = getParent(); 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; + } + 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); + } } } |