diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java index 47a8d87f0e67..79ba8a858972 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java @@ -2,7 +2,6 @@ import android.content.Intent; import android.os.Bundle; -import android.os.Parcelable; import android.text.TextUtils; import android.view.MenuItem; import android.view.View; @@ -14,9 +13,10 @@ import androidx.appcompat.app.ActionBar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.lifecycle.Lifecycle; import androidx.lifecycle.ViewModelProvider; -import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -66,7 +66,8 @@ import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.WPSwipeSnackbar; -import org.wordpress.android.widgets.WPViewPagerTransformer; +import org.wordpress.android.widgets.WPViewPager2Transformer; +import org.wordpress.android.widgets.WPViewPager2Transformer.TransformType.SlideOver; import java.util.ArrayList; import java.util.Collections; @@ -103,7 +104,7 @@ public class NotificationsDetailActivity extends LocaleAwareActivity implements @Nullable private String mNoteId; private boolean mIsTappedOnNotification; - @Nullable private ViewPager.OnPageChangeListener mOnPageChangeListener; + @Nullable private ViewPager2.OnPageChangeCallback mOnPageChangeListener; @Nullable private NotificationDetailFragmentAdapter mAdapter; @Nullable private NotificationsDetailActivityBinding mBinding = null; @@ -155,8 +156,7 @@ public void handleOnBackPressed() { // set up the viewpager and adapter for lateral navigation if (mBinding != null) { - mBinding.viewpager.setPageTransformer(false, - new WPViewPagerTransformer(WPViewPagerTransformer.TransformType.SLIDE_OVER)); + mBinding.viewpager.setPageTransformer(new WPViewPager2Transformer(SlideOver.INSTANCE)); } Note note = NotificationsTable.getNoteById(mNoteId); @@ -233,10 +233,10 @@ private void updateUIAndNote(boolean doRefresh) { private void resetOnPageChangeListener() { if (mOnPageChangeListener != null) { if (mBinding != null) { - mBinding.viewpager.removeOnPageChangeListener(mOnPageChangeListener); + mBinding.viewpager.unregisterOnPageChangeCallback(mOnPageChangeListener); } } else { - mOnPageChangeListener = new ViewPager.OnPageChangeListener() { + mOnPageChangeListener = new ViewPager2.OnPageChangeCallback() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @@ -244,7 +244,7 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse @Override public void onPageSelected(int position) { if (mBinding != null && mAdapter != null) { - Fragment fragment = mAdapter.getItem(mBinding.viewpager.getCurrentItem()); + Fragment fragment = mAdapter.createFragment(mBinding.viewpager.getCurrentItem()); boolean hideToolbar = (fragment instanceof ReaderPostDetailFragment); showHideToolbar(hideToolbar); @@ -268,7 +268,7 @@ public void onPageScrollStateChanged(int state) { }; } if (mBinding != null) { - mBinding.viewpager.addOnPageChangeListener(mOnPageChangeListener); + mBinding.viewpager.registerOnPageChangeCallback(mOnPageChangeListener); } } @@ -281,14 +281,14 @@ private void trackCommentNote(@NonNull Note note) { } public void showHideToolbar(boolean hide) { + if (mBinding != null) { + setSupportActionBar(mBinding.toolbarMain); + } if (getSupportActionBar() != null) { if (hide) { getSupportActionBar().hide(); } else { - if (mBinding != null) { - setSupportActionBar(mBinding.toolbarMain); - getSupportActionBar().show(); - } + getSupportActionBar().show(); } getSupportActionBar().setDisplayShowTitleEnabled(!hide); } @@ -320,7 +320,7 @@ protected void onStart() { EventBus.getDefault().register(this); // If the user hasn't used swipe yet and if the adapter is initialised and have at least 2 notifications, // show a hint to promote swipe usage on the ViewPager - if (!AppPrefs.isNotificationsSwipeToNavigateShown() && mAdapter != null && mAdapter.getCount() > 1) { + if (!AppPrefs.isNotificationsSwipeToNavigateShown() && mAdapter != null && 1 < mAdapter.getItemCount()) { if (mBinding != null) { WPSwipeSnackbar.show(mBinding.viewpager); AppPrefs.setNotificationsSwipeToNavigateShown(true); @@ -380,11 +380,12 @@ private NotificationDetailFragmentAdapter buildNoteListAdapterAndSetPosition(Not // apply filter to the list so we show the same items that the list show vertically, but horizontally ArrayList filteredNotes = NotesAdapter.buildFilteredNotesList(notes, filter); - adapter = new NotificationDetailFragmentAdapter(getSupportFragmentManager(), filteredNotes); + adapter = new NotificationDetailFragmentAdapter(getSupportFragmentManager(), getLifecycle(), filteredNotes); if (mBinding != null) { mBinding.viewpager.setAdapter(adapter); - mBinding.viewpager.setCurrentItem(NotificationsUtils.findNoteInNoteArray(filteredNotes, note.getId())); + mBinding.viewpager.setCurrentItem( + NotificationsUtils.findNoteInNoteArray(filteredNotes, note.getId()), false); } return adapter; @@ -395,8 +396,7 @@ private NotificationDetailFragmentAdapter buildNoteListAdapterAndSetPosition(Not * Defaults to NotificationDetailListFragment */ @NonNull - @SuppressWarnings("deprecation") - private Fragment getDetailFragmentForNote(@NonNull Note note) { + private Fragment createDetailFragmentForNote(@NonNull Note note) { Fragment fragment; if (note.isCommentType()) { // show comment detail for comment notifications @@ -589,7 +589,7 @@ public void onEventMainThread(NotificationEvents.NotificationsRefreshError error @Override public void onPositiveClicked(@NonNull String instanceTag) { if (mBinding != null && mAdapter != null) { - Fragment fragment = mAdapter.getItem(mBinding.viewpager.getCurrentItem()); + Fragment fragment = mAdapter.createFragment(mBinding.viewpager.getCurrentItem()); if (fragment instanceof BasicFragmentDialog.BasicDialogPositiveClickInterface) { ((BasicDialogPositiveClickInterface) fragment).onPositiveClicked(instanceTag); } @@ -603,56 +603,30 @@ public void onScrollableViewInitialized(int containerId) { } } - @SuppressWarnings("deprecation") - private class NotificationDetailFragmentAdapter extends FragmentStatePagerAdapter { + private class NotificationDetailFragmentAdapter extends FragmentStateAdapter { + @NonNull private final ArrayList mNoteList; @SuppressWarnings("unchecked") - NotificationDetailFragmentAdapter(FragmentManager fm, ArrayList notes) { - super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + NotificationDetailFragmentAdapter(@NonNull FragmentManager fm, @NonNull Lifecycle lifecycle, + @NonNull ArrayList notes) { + super(fm, lifecycle); mNoteList = (ArrayList) notes.clone(); } @NonNull @Override - public Fragment getItem(int position) { - return getDetailFragmentForNote(mNoteList.get(position)); + public Fragment createFragment(int position) { + return createDetailFragmentForNote(mNoteList.get(position)); } @Override - public int getCount() { + public int getItemCount() { return mNoteList.size(); } - @Override - public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) { - // work around "Fragment no longer exists for key" Android bug - // by catching the IllegalStateException - // https://code.google.com/p/android/issues/detail?id=42601 - try { - AppLog.d(AppLog.T.NOTIFS, "notifications pager > adapter restoreState"); - super.restoreState(state, loader); - } catch (IllegalStateException e) { - AppLog.e(AppLog.T.NOTIFS, e); - } - } - - @Nullable - @Override - public Parcelable saveState() { - AppLog.d(AppLog.T.NOTIFS, "notifications pager > adapter saveState"); - Bundle bundle = (Bundle) super.saveState(); - if (bundle == null) { - bundle = new Bundle(); - } - // This is a possible solution to https://github.com/wordpress-mobile/WordPress-Android/issues/5456 - // See https://issuetracker.google.com/issues/37103380#comment77 for more details - bundle.putParcelableArray("states", null); - return bundle; - } - boolean isValidPosition(int position) { - return (position >= 0 && position < getCount()); + return (position >= 0 && position < getItemCount()); } private Note getNoteAtPosition(int position) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index 5d0e2422014b..571698865778 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -19,7 +19,6 @@ import android.view.ContextThemeWrapper import android.view.Gravity import android.view.LayoutInflater import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -37,7 +36,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat -import androidx.core.view.MenuProvider import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isInvisible @@ -45,7 +43,6 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.Factory import androidx.recyclerview.widget.DefaultItemAnimator @@ -172,7 +169,6 @@ import com.google.android.material.R as MaterialR @Suppress("LargeClass") class ReaderPostDetailFragment : ViewPagerFragment(), WPMainActivity.OnActivityBackPressedListener, - MenuProvider, ScrollDirectionListener, ReaderCustomViewListener, ReaderWebViewPageFinishedListener, @@ -416,7 +412,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), appBar = view.findViewById(R.id.appbar_with_collapsing_toolbar_layout) toolBar = appBar.findViewById(R.id.toolbar_main) - toolBar.setVisible(true) appBar.addOnOffsetChangedListener(appBarLayoutOffsetChangedListener) // Fixes collapsing toolbar layout being obscured by the status bar when drawn behind it @@ -429,7 +424,10 @@ class ReaderPostDetailFragment : ViewPagerFragment(), } // Fixes viewpager not displaying menu items for first fragment + val activity = activity as? AppCompatActivity + activity?.supportActionBar?.hide() toolBar.inflateMenu(R.menu.reader_detail) + toolBar.setOnMenuItemClickListener { handleMenuItemSelected(it)} // for related posts, show an X in the toolbar which closes the activity if (isRelatedPost) { @@ -534,24 +532,8 @@ class ReaderPostDetailFragment : ViewPagerFragment(), activity?.window?.setWindowNavigationBarColor(themeValues.intBackgroundColor) } - override fun onResume() { - super.onResume() - replaceActivityToolbarWithCollapsingToolbar() - } - - private fun replaceActivityToolbarWithCollapsingToolbar() { - val activity = activity as? AppCompatActivity - activity?.supportActionBar?.hide() - - toolBar.setVisible(true) - activity?.setSupportActionBar(toolBar) - - activity?.supportActionBar?.setDisplayShowTitleEnabled(isRelatedPost) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) initLikeFacesRecycler(savedInstanceState) initCommentSnippetRecycler(savedInstanceState) @@ -865,7 +847,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), @Suppress("ForbiddenComment") private fun onPostExecuteShowPost() { // make sure options menu reflects whether we now have a post - activity?.invalidateOptionsMenu() + prepareMenu(toolBar.menu) viewModel.post?.let { if (handleDirectOperation()) return @@ -1079,12 +1061,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), moreMenuPopup?.dismiss() } - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menu.clear() - menuInflater.inflate(R.menu.reader_detail, menu) - } - - override fun onPrepareMenu(menu: Menu) { + private fun prepareMenu(menu: Menu) { val postHasUrl = viewModel.post?.hasUrl() == true val menuBrowse = menu.findItem(R.id.menu_browse) // browse require the post to have a URL (some feed-based posts don't have one) or an intercepted URI @@ -1097,7 +1074,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), menuReadingPreferences?.isVisible = readingPreferencesFeatureConfig.isEnabled() } - override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { + private fun handleMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { R.id.menu_browse -> { val interceptedUri = viewModel.interceptedUri if (viewModel.hasPost) { diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPSwipeSnackbar.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwipeSnackbar.java index a3804cef9e26..a807c3fee765 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/WPSwipeSnackbar.java +++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPSwipeSnackbar.java @@ -1,14 +1,15 @@ package org.wordpress.android.widgets; -import android.annotation.SuppressLint; import android.content.Context; import android.view.Gravity; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; @@ -27,25 +28,53 @@ private WPSwipeSnackbar() { throw new AssertionError(); } - public static Snackbar show(@NonNull ViewPager viewPager) { + /** {@link ViewPager2}-based clone of the original helper method: {@link #show(ViewPager)} */ + @NonNull + public static Snackbar show(@NonNull ViewPager2 viewPager) { + SwipeArrows arrows; + Adapter adapter = viewPager.getAdapter(); + if (adapter == null) { + arrows = SwipeArrows.NONE; + } else { + arrows = getSwipeArrows(adapter.getItemCount(), viewPager.getCurrentItem()); + } + return show(viewPager, arrows); + } + + @NonNull public static Snackbar show(@NonNull ViewPager viewPager) { SwipeArrows arrows; PagerAdapter adapter = viewPager.getAdapter(); - if (adapter == null || adapter.getCount() <= 1) { + if (adapter == null) { arrows = SwipeArrows.NONE; - } else if (viewPager.getCurrentItem() == 0) { + } else { + arrows = getSwipeArrows(adapter.getCount(), viewPager.getCurrentItem()); + } + return show(viewPager, arrows); + } + + @NonNull private static SwipeArrows getSwipeArrows(int itemCount, int currentItem) { + SwipeArrows arrows; + if (itemCount <= 1) { + arrows = SwipeArrows.NONE; + } else if (currentItem == 0) { arrows = SwipeArrows.RIGHT; - } else if (viewPager.getCurrentItem() == (adapter.getCount() - 1)) { + } else if (currentItem == (itemCount - 1)) { arrows = SwipeArrows.LEFT; } else { arrows = SwipeArrows.BOTH; } - return show(viewPager, arrows); + return arrows; + } + + @NonNull private static Snackbar show(@NonNull View view, @NonNull SwipeArrows arrows) { + String text = getSwipeText(view.getContext(), arrows); + Snackbar snackbar = WPSnackbar.make(view, text, BaseTransientBottomBar.LENGTH_LONG); + centerSnackbarText(snackbar); + snackbar.show(); + return snackbar; } - // BaseTransientBottomBar.LENGTH_LONG is pointing to Snackabr.LENGTH_LONG which confuses checkstyle - @SuppressLint("WrongConstant") - private static Snackbar show(@NonNull ViewPager viewPager, @NonNull SwipeArrows arrows) { - Context context = viewPager.getContext(); + @NonNull private static String getSwipeText(@NonNull Context context, @NonNull SwipeArrows arrows) { String swipeText = context.getResources().getString(R.string.swipe_for_more); String arrowLeft = context.getResources().getString(R.string.previous_button); String arrowRight = context.getResources().getString(R.string.next_button); @@ -61,16 +90,12 @@ private static Snackbar show(@NonNull ViewPager viewPager, @NonNull SwipeArrows case BOTH: text = arrowLeft + " " + swipeText + " " + arrowRight; break; + case NONE: default: text = swipeText; break; } - - Snackbar snackbar = Snackbar.make(viewPager, text, BaseTransientBottomBar.LENGTH_LONG); // CHECKSTYLE IGNORE - centerSnackbarText(snackbar); - snackbar.show(); - - return snackbar; + return text; } /* diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager2Transformer.kt b/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager2Transformer.kt new file mode 100644 index 000000000000..6183e8f26c94 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPViewPager2Transformer.kt @@ -0,0 +1,95 @@ +package org.wordpress.android.widgets + +import android.view.View +import androidx.viewpager2.widget.ViewPager2 +import org.wordpress.android.widgets.WPViewPager2Transformer.TransformType.Flow +import org.wordpress.android.widgets.WPViewPager2Transformer.TransformType.Depth +import org.wordpress.android.widgets.WPViewPager2Transformer.TransformType.Zoom +import org.wordpress.android.widgets.WPViewPager2Transformer.TransformType.SlideOver +import kotlin.math.abs +import kotlin.math.max + +/** + * #### Transformer for ViewPager2 + * This is a clone of [WPViewPagerTransformer], with ViewPager2 compatibility. The purpose of this class is to ease the + * migration from ViewPager to ViewPager2 by providing a drop-in replacement for wherever the ViewPager-based + * transformer is used. + */ +class WPViewPager2Transformer(private val mTransformType: TransformType) : ViewPager2.PageTransformer { + sealed class TransformType { + data object Flow: TransformType() { const val ROTATION_FACTOR = -30f } + data object Depth: TransformType() + data object Zoom: TransformType() + data object SlideOver: TransformType() + } + + override fun transformPage(page: View, position: Float) { + val alpha: Float + val scale: Float + val translationX: Float + when (mTransformType) { + Flow -> { + page.rotationY = position * Flow.ROTATION_FACTOR + return + } + + SlideOver -> if (position < 0 && position > -1) { + // this is the page to the left + scale = (abs((abs(position.toDouble()) - 1)) * (1.0f - SCALE_FACTOR_SLIDE) + SCALE_FACTOR_SLIDE) + .toFloat() + alpha = max(MIN_ALPHA_SLIDE.toDouble(), (1 - abs(position.toDouble()))).toFloat() + val pageWidth = page.width + val translateValue = position * -pageWidth + translationX = if (translateValue > -pageWidth) { + translateValue + } else { + 0f + } + } else { + alpha = 1f + scale = 1f + translationX = 0f + } + + Depth -> if (position > 0 && position < 1) { + // moving to the right + alpha = 1 - position + scale = (MIN_SCALE_DEPTH + (1 - MIN_SCALE_DEPTH) * (1 - abs( position.toDouble()))).toFloat() + translationX = page.width * -position + } else { + // use default for all other cases + alpha = 1f + scale = 1f + translationX = 0f + } + + Zoom -> if (position >= -1 && position <= 1) { + scale = max(MIN_SCALE_ZOOM.toDouble(), (1 - abs(position.toDouble()))).toFloat() + alpha = (MIN_ALPHA_ZOOM + (scale - MIN_SCALE_ZOOM) / (1 - MIN_SCALE_ZOOM) * (1 - MIN_ALPHA_ZOOM)) + val vMargin = (page.height * (1 - scale) / 2) + val hMargin = (page.width * (1 - scale) / 2) + translationX = if (position < 0) { + hMargin - vMargin / 2 + } else { + -hMargin + vMargin / 2 + } + } else { + alpha = 1f + scale = 1f + translationX = 0f + } + } + page.setAlpha(alpha) + page.translationX = translationX + page.scaleX = scale + page.scaleY = scale + } + + companion object { + private const val MIN_SCALE_DEPTH = 0.75f + private const val MIN_SCALE_ZOOM = 0.85f + private const val MIN_ALPHA_ZOOM = 0.5f + private const val SCALE_FACTOR_SLIDE = 0.85f + private const val MIN_ALPHA_SLIDE = 0.35f + } +} diff --git a/WordPress/src/main/res/layout/notifications_detail_activity.xml b/WordPress/src/main/res/layout/notifications_detail_activity.xml index 7fdb460ad3ef..af60bb0384d3 100644 --- a/WordPress/src/main/res/layout/notifications_detail_activity.xml +++ b/WordPress/src/main/res/layout/notifications_detail_activity.xml @@ -29,12 +29,11 @@ -