From a560b7a1ea70eaee8bea26b854d33ef4bd701dd7 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Fri, 13 Feb 2026 01:23:28 +0200 Subject: [PATCH] Migrate MainFragment to ViewPager2 --- ...agmentStatePagerAdapterMenuWorkaround.java | 344 ------------------ .../newpipe/fragments/MainFragment.java | 66 ++-- app/src/main/res/layout/fragment_main.xml | 2 +- 3 files changed, 29 insertions(+), 383 deletions(-) delete mode 100644 app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java deleted file mode 100644 index 8d03a1486..000000000 --- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.fragment.app; - -import android.os.Bundle; -import android.os.Parcelable; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.os.BundleCompat; -import androidx.lifecycle.Lifecycle; -import androidx.viewpager.widget.PagerAdapter; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; - -// TODO: Replace this deprecated class with its ViewPager2 counterpart - -/** - * This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}. - *

- * It includes a workaround to fix the menu visibility when the adapter is restored. - *

- *

- * When restoring the state of this adapter, all the fragments' menu visibility were set to false, - * effectively disabling the menu from the user until he switched pages or another event - * that triggered the menu to be visible again happened. - *

- *

- * Check out the changes in: - *

- * - * - * @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use - * {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead. - */ -@SuppressWarnings("deprecation") -@Deprecated -public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter { - private static final String TAG = "FragmentStatePagerAdapt"; - private static final boolean DEBUG = false; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}) - private @interface Behavior { } - - /** - * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current - * fragment changes. - * - * @deprecated This behavior relies on the deprecated - * {@link Fragment#setUserVisibleHint(boolean)} API. Use - * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement, - * {@link FragmentTransaction#setMaxLifecycle}. - * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int) - */ - @Deprecated - public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; - - /** - * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED} - * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}. - * - * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int) - */ - public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1; - - private final FragmentManager mFragmentManager; - private final int mBehavior; - private FragmentTransaction mCurTransaction = null; - - private final ArrayList mSavedState = new ArrayList<>(); - private final ArrayList mFragments = new ArrayList<>(); - private Fragment mCurrentPrimaryItem = null; - private boolean mExecutingFinishUpdate; - - /** - * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround} - * that sets the fragment manager for the adapter. This is the equivalent of calling - * {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in - * {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}. - * - *

Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the - * current Fragment changes.

- * - * @param fm fragment manager that will interact with this adapter - * @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with - * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} - */ - @Deprecated - public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) { - this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); - } - - /** - * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}. - * - * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current - * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are - * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is - * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be - * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. - * - * @param fm fragment manager that will interact with this adapter - * @param behavior determines if only current fragments are in a resumed state - */ - public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm, - @Behavior final int behavior) { - mFragmentManager = fm; - mBehavior = behavior; - } - - /** - * @param position the position of the item you want - * @return the {@link Fragment} associated with a specified position - */ - @NonNull - public abstract Fragment getItem(int position); - - @Override - public void startUpdate(@NonNull final ViewGroup container) { - if (container.getId() == View.NO_ID) { - throw new IllegalStateException("ViewPager with adapter " + this - + " requires a view id"); - } - } - - @SuppressWarnings("deprecation") - @NonNull - @Override - public Object instantiateItem(@NonNull final ViewGroup container, final int position) { - // If we already have this item instantiated, there is nothing - // to do. This can happen when we are restoring the entire pager - // from its saved state, where the fragment manager has already - // taken care of restoring the fragments we previously had instantiated. - if (mFragments.size() > position) { - final Fragment f = mFragments.get(position); - if (f != null) { - return f; - } - } - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - - final Fragment fragment = getItem(position); - if (DEBUG) { - Log.v(TAG, "Adding item #" + position + ": f=" + fragment); - } - if (mSavedState.size() > position) { - final Fragment.SavedState fss = mSavedState.get(position); - if (fss != null) { - fragment.setInitialSavedState(fss); - } - } - while (mFragments.size() <= position) { - mFragments.add(null); - } - fragment.setMenuVisibility(false); - if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) { - fragment.setUserVisibleHint(false); - } - - mFragments.set(position, fragment); - mCurTransaction.add(container.getId(), fragment); - - if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); - } - - return fragment; - } - - @Override - public void destroyItem(@NonNull final ViewGroup container, final int position, - @NonNull final Object object) { - final Fragment fragment = (Fragment) object; - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - if (DEBUG) { - Log.v(TAG, "Removing item #" + position + ": f=" + object - + " v=" + ((Fragment) object).getView()); - } - while (mSavedState.size() <= position) { - mSavedState.add(null); - } - mSavedState.set(position, fragment.isAdded() - ? mFragmentManager.saveFragmentInstanceState(fragment) : null); - mFragments.set(position, null); - - mCurTransaction.remove(fragment); - if (fragment.equals(mCurrentPrimaryItem)) { - mCurrentPrimaryItem = null; - } - } - - @Override - @SuppressWarnings({"ReferenceEquality", "deprecation"}) - public void setPrimaryItem(@NonNull final ViewGroup container, final int position, - @NonNull final Object object) { - final Fragment fragment = (Fragment) object; - if (fragment != mCurrentPrimaryItem) { - if (mCurrentPrimaryItem != null) { - mCurrentPrimaryItem.setMenuVisibility(false); - if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); - } else { - mCurrentPrimaryItem.setUserVisibleHint(false); - } - } - fragment.setMenuVisibility(true); - if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); - } else { - fragment.setUserVisibleHint(true); - } - - mCurrentPrimaryItem = fragment; - } - } - - @Override - public void finishUpdate(@NonNull final ViewGroup container) { - if (mCurTransaction != null) { - // We drop any transactions that attempt to be committed - // from a re-entrant call to finishUpdate(). We need to - // do this as a workaround for Robolectric running measure/layout - // calls inline rather than allowing them to be posted - // as they would on a real device. - if (!mExecutingFinishUpdate) { - try { - mExecutingFinishUpdate = true; - mCurTransaction.commitNowAllowingStateLoss(); - } finally { - mExecutingFinishUpdate = false; - } - } - mCurTransaction = null; - } - } - - @Override - public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { - return ((Fragment) object).getView() == view; - } - - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - private final String selectedFragment = "selected_fragment"; - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - @Override - @Nullable - public Parcelable saveState() { - Bundle state = null; - if (!mSavedState.isEmpty()) { - state = new Bundle(); - state.putParcelableArrayList("states", mSavedState); - } - for (int i = 0; i < mFragments.size(); i++) { - final Fragment f = mFragments.get(i); - if (f != null && f.isAdded()) { - if (state == null) { - state = new Bundle(); - } - final String key = "f" + i; - mFragmentManager.putFragment(state, key, f); - - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // Check if it's the same fragment instance - if (f == mCurrentPrimaryItem) { - state.putString(selectedFragment, key); - } - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - } - } - return state; - } - - @Override - public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) { - if (state != null) { - final Bundle bundle = (Bundle) state; - bundle.setClassLoader(loader); - final var states = BundleCompat.getParcelableArrayList(bundle, "states", - Fragment.SavedState.class); - mSavedState.clear(); - mFragments.clear(); - if (states != null) { - mSavedState.addAll(states); - } - final Iterable keys = bundle.keySet(); - for (final String key : keys) { - if (key.startsWith("f")) { - final int index = Integer.parseInt(key.substring(1)); - final Fragment f = mFragmentManager.getFragment(bundle, key); - if (f != null) { - while (mFragments.size() <= index) { - mFragments.add(null); - } - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - final boolean wasSelected = bundle.getString(selectedFragment, "") - .equals(key); - f.setMenuVisibility(wasSelected); - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - mFragments.set(index, f); - } else { - Log.w(TAG, "Bad fragment at key " + key); - } - } - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 1a5e5aa45..fc9904e93 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -26,12 +26,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; import androidx.preference.PreferenceManager; -import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; @@ -106,11 +106,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte binding = FragmentMainBinding.bind(rootView); - binding.mainTabLayout.setupWithViewPager(binding.pager); - binding.mainTabLayout.addOnTabSelectedListener(this); - setupTabs(); updateTabLayoutPosition(); + + binding.pager.setAdapter(pagerAdapter); + new TabLayoutMediator(binding.mainTabLayout, binding.pager, + (tab, position) -> { + tab.setIcon(tabsList.get(position).getTabIconRes(requireContext())); + tab.setContentDescription(tabsList.get(position).getTabName(requireContext())); + } + ).attach(); + binding.mainTabLayout.addOnTabSelectedListener(this); } @Override @@ -189,31 +195,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte tabsList.clear(); tabsList.addAll(tabsManager.getTabs()); - if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) { - pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), - getChildFragmentManager(), tabsList); + if (pagerAdapter == null) { + pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), this, tabsList); + binding.pager.setAdapter(pagerAdapter); + } else if (!pagerAdapter.sameTabs(tabsList)) { + pagerAdapter.updateTabs(tabsList); } - binding.pager.setAdapter(null); - binding.pager.setAdapter(pagerAdapter); - - updateTabsIconAndDescription(); updateTitleForTab(binding.pager.getCurrentItem()); hasTabsChanged = false; } - private void updateTabsIconAndDescription() { - for (int i = 0; i < tabsList.size(); i++) { - final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i); - if (tabToSet != null) { - final Tab tab = tabsList.get(i); - tabToSet.setIcon(tab.getTabIconRes(requireContext())); - tabToSet.setContentDescription(tab.getTabName(requireContext())); - } - } - } - private void updateTitleForTab(final int tabPosition) { setTitle(tabsList.get(tabPosition).getTabName(requireContext())); } @@ -226,7 +219,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte private void updateTabLayoutPosition() { final ScrollableTabLayout tabLayout = binding.mainTabLayout; - final ViewPager viewPager = binding.pager; + final ViewPager2 viewPager = binding.pager; final boolean bottom = mainTabsPositionBottom; // change layout params to make the tab layout appear either at the top or at the bottom @@ -275,8 +268,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte updateTitleForTab(tab.getPosition()); } - public static final class SelectedTabsPagerAdapter - extends FragmentStatePagerAdapterMenuWorkaround { + public static final class SelectedTabsPagerAdapter extends FragmentStateAdapter { private final Context context; private final List internalTabsList; /** @@ -289,16 +281,21 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte private final List localPlaylistFragments = new ArrayList<>(); private SelectedTabsPagerAdapter(final Context context, - final FragmentManager fragmentManager, + final Fragment fragment, final List tabsList) { - super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + super(fragment); this.context = context; this.internalTabsList = new ArrayList<>(tabsList); } + public void updateTabs(final List tabsList) { + internalTabsList.clear(); + internalTabsList.addAll(tabsList); + notifyDataSetChanged(); + } @NonNull @Override - public Fragment getItem(final int position) { + public Fragment createFragment(final int position) { final Tab tab = internalTabsList.get(position); final Fragment fragment; @@ -325,14 +322,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } @Override - public int getItemPosition(@NonNull final Object object) { - // Causes adapter to reload all Fragments when - // notifyDataSetChanged is called - return POSITION_NONE; - } - - @Override - public int getCount() { + public int getItemCount() { return internalTabsList.size(); } diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index ef25f8a89..7f573fcb1 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -17,7 +17,7 @@ app:tabGravity="fill" app:tabMinWidth="60dp" /> -