Commit fbaedef9 authored by Michael W's avatar Michael W
Browse files

DeskClock: Replace ViewPager with manual fragment handling

* Convert to using androidx Fragments
* Group imports properly

Change-Id: Iaadfb417f0ca8638936875113ec2f39853f2a39c
parent 26df7bf2
......@@ -18,9 +18,8 @@
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cities"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="false"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_height"
......
......@@ -25,25 +25,19 @@
app:statusBarBackground="@null"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/content"
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_height="wrap_content"
android:background="@null"
app:elevation="0dp">
<com.google.android.material.appbar.AppBarLayout
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStart="0dp"
tools:ignore="RtlSymmetry"
android:gravity="center">
app:contentInsetStart="0dp"
tools:ignore="RtlSymmetry"
android:gravity="center">
<TextView
android:id="@+id/title_view"
......@@ -52,16 +46,19 @@
android:layout_gravity="center"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.viewpager.widget.ViewPager
android:id="@+id/desk_clock_pager"
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:saveEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
......
......@@ -16,24 +16,24 @@
package com.android.deskclock;
import android.app.LoaderManager;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.loader.content.CursorLoader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.deskclock.alarms.AlarmTimeClickHandler;
import com.android.deskclock.alarms.AlarmUpdateHandler;
......@@ -48,6 +48,7 @@ import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.widget.EmptyViewController;
import com.android.deskclock.widget.toast.SnackbarManager;
import com.android.deskclock.widget.toast.ToastManager;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
......@@ -80,7 +81,7 @@ public final class AlarmClockFragment extends DeskClockFragment implements
private RecyclerView mRecyclerView;
// Data
private Loader mCursorLoader;
private CursorLoader mCursorLoader;
private long mScrollToAlarmId = Alarm.INVALID_ID;
private long mExpandedAlarmId = Alarm.INVALID_ID;
private long mCurrentUpdateToken;
......@@ -102,7 +103,7 @@ public final class AlarmClockFragment extends DeskClockFragment implements
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
mCursorLoader = getLoaderManager().initLoader(0, null, this);
mCursorLoader = (CursorLoader) LoaderManager.getInstance(this).initLoader(0, null, this);
if (savedState != null) {
mExpandedAlarmId = savedState.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID);
}
......@@ -401,6 +402,11 @@ public final class AlarmClockFragment extends DeskClockFragment implements
right.setVisibility(View.INVISIBLE);
}
@Override
public final int getFabTargetVisibility() {
return View.VISIBLE;
}
private void startCreatingAlarm() {
// Clear the currently selected alarm.
mAlarmTimeClickHandler.setSelectedAlarm(null);
......
......@@ -215,6 +215,11 @@ public final class ClockFragment extends DeskClockFragment {
right.setVisibility(INVISIBLE);
}
@Override
public final int getFabTargetVisibility() {
return View.VISIBLE;
}
/**
* Refresh the next alarm time.
*/
......
......@@ -20,7 +20,6 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
......@@ -37,6 +36,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
......@@ -88,9 +88,6 @@ public class DeskClock extends BaseActivity
/** Hides, updates, and shows only the {@link #mLeftButton} and {@link #mRightButton}. */
private final AnimatorSet mUpdateButtonsOnlyAnimation = new AnimatorSet();
/** Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends. */
private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener();
/** Updates the user interface to reflect the selected tab from the backing model. */
private final TabListener mTabChangeWatcher = new TabChangeWatcher();
......@@ -119,15 +116,14 @@ public class DeskClock extends BaseActivity
/** The ViewPager that pages through the fragments representing the content of the tabs. */
private ViewPager mFragmentTabPager;
/** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */
private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
/** The view that displays the current tab's title */
private TextView mTitleView;
/** The bottom navigation bar */
private BottomNavigationView mBottomNavigation;
private FragmentUtils mFragmentUtils;
/** {@code true} when a settings change necessitates recreating this activity. */
private boolean mRecreateActivity;
......@@ -248,18 +244,7 @@ public class DeskClock extends BaseActivity
.after(leftHideAnimation)
.after(rightHideAnimation);
// Customize the view pager.
mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
// Keep all four tabs to minimize jank.
mFragmentTabPager.setOffscreenPageLimit(3);
// Set Accessibility Delegate to null so view pager doesn't intercept movements and
// prevent the fab from being selected.
mFragmentTabPager.setAccessibilityDelegate(null);
// Mirror changes made to the selected page of the view pager into UiDataModel.
mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);
mFragmentUtils = new FragmentUtils(this);
// Mirror changes made to the selected tab into UiDataModel.
mBottomNavigation = findViewById(R.id.bottom_view);
mBottomNavigation.setOnNavigationItemSelectedListener(mNavigationListener);
......@@ -275,27 +260,40 @@ public class DeskClock extends BaseActivity
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
UiDataModel.Tab tab = null;
UiDataModel.Tab selectedTab = null;
switch (item.getItemId()) {
case R.id.page_alarm:
tab = UiDataModel.Tab.ALARMS;
selectedTab = UiDataModel.Tab.ALARMS;
break;
case R.id.page_clock:
tab = UiDataModel.Tab.CLOCKS;
selectedTab = UiDataModel.Tab.CLOCKS;
break;
case R.id.page_timer:
tab = UiDataModel.Tab.TIMERS;
selectedTab = UiDataModel.Tab.TIMERS;
break;
case R.id.page_stopwatch:
tab = UiDataModel.Tab.STOPWATCH;
selectedTab = UiDataModel.Tab.STOPWATCH;
break;
}
if (tab != null) {
UiDataModel.getUiDataModel().setSelectedTab(tab);
if (selectedTab != null) {
UiDataModel.Tab currentTab = UiDataModel.getUiDataModel().getSelectedTab();
DeskClockFragment currentFrag = mFragmentUtils.getDeskClockFragment(currentTab);
DeskClockFragment selectedFrag = mFragmentUtils.getDeskClockFragment(selectedTab);
int currentVisibility = currentFrag.getFabTargetVisibility();
int targetVisibility = selectedFrag.getFabTargetVisibility();
if (currentVisibility != targetVisibility) {
if (targetVisibility == View.VISIBLE) {
mShowAnimation.start();
} else {
mHideAnimation.start();
}
}
UiDataModel.getUiDataModel().setSelectedTab(selectedTab);
return true;
}
......@@ -324,15 +322,6 @@ public class DeskClock extends BaseActivity
if (mRecreateActivity) {
mRecreateActivity = false;
// A runnable must be posted here or the new DeskClock activity will be recreated in a
// paused state, even though it is the foreground activity.
mFragmentTabPager.post(new Runnable() {
@Override
public void run() {
recreate();
}
});
}
}
......@@ -375,7 +364,7 @@ public class DeskClock extends BaseActivity
*/
@Override
public void onDialogLabelSet(Alarm alarm, String label, String tag) {
final Fragment frag = getFragmentManager().findFragmentByTag(tag);
final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
if (frag instanceof AlarmClockFragment) {
((AlarmClockFragment) frag).setLabel(alarm, label);
}
......@@ -468,16 +457,7 @@ public class DeskClock extends BaseActivity
final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
// Update the selected tab in the mBottomNavigation if it does not agree with UiDataModel.
mBottomNavigation.setSelectedItemId(selectedTab.getPageResId());
// Update the selected fragment in the viewpager if it does not agree with UiDataModel.
for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
if (fragment.isTabSelected() && mFragmentTabPager.getCurrentItem() != i) {
mFragmentTabPager.setCurrentItem(i);
break;
}
}
mFragmentUtils.showFragment(selectedTab);
mTitleView.setText(selectedTab.getLabelResId());
}
......@@ -485,14 +465,7 @@ public class DeskClock extends BaseActivity
* @return the DeskClockFragment that is currently selected according to UiDataModel
*/
private DeskClockFragment getSelectedDeskClockFragment() {
for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
if (fragment.isTabSelected()) {
return fragment;
}
}
final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
throw new IllegalStateException("Unable to locate selected fragment (" + selectedTab + ")");
return mFragmentUtils.getCurrentFragment();
}
/**
......@@ -502,93 +475,6 @@ public class DeskClock extends BaseActivity
return Snackbar.make(mSnackbarAnchor, messageId, 5000 /* duration */);
}
/**
* As the view pager changes the selected page, update the model to record the new selected tab.
*/
private final class PageChangeWatcher implements OnPageChangeListener {
/** The last reported page scroll state; used to detect exotic state changes. */
private int mPriorState = SCROLL_STATE_IDLE;
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// Only hide the fab when a non-zero drag distance is detected. This prevents
// over-scrolling from needlessly hiding the fab.
if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
mFabState = FabState.HIDING;
mHideAnimation.start();
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
// The user has tapped a tab button; play the hide and show animations linearly.
mHideAnimation.addListener(mAutoStartShowListener);
mHideAnimation.start();
mFabState = FabState.HIDING;
} else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) {
// The user has interrupted settling on a tab and the fab button must be re-hidden.
if (mShowAnimation.isStarted()) {
mShowAnimation.cancel();
}
if (mHideAnimation.isStarted()) {
// Let the hide animation finish naturally; don't auto show when it ends.
mHideAnimation.removeListener(mAutoStartShowListener);
} else {
// Start and immediately end the hide animation to jump to the hidden state.
mHideAnimation.start();
mHideAnimation.end();
}
mFabState = FabState.HIDING;
} else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
// The user has lifted their finger; show the buttons now or after hide ends.
if (mHideAnimation.isStarted()) {
// Finish the hide animation and then start the show animation.
mHideAnimation.addListener(mAutoStartShowListener);
} else {
updateFab(FAB_AND_BUTTONS_IMMEDIATE);
mShowAnimation.start();
// The animation to show the fab has begun; update the state to showing.
mFabState = FabState.SHOWING;
}
} else if (state == SCROLL_STATE_DRAGGING) {
// The user has started a drag so arm the hide animation.
mFabState = FabState.HIDE_ARMED;
}
// Update the last known state.
mPriorState = state;
}
@Override
public void onPageSelected(int position) {
mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab();
}
}
/**
* If this listener is attached to {@link #mHideAnimation} when it ends, the corresponding
* {@link #mShowAnimation} is automatically started.
*/
private final class AutoStartShowListener extends AnimatorListenerAdapter {
@Override
public void onAnimationEnd(Animator animation) {
// Prepare the hide animation for its next use; by default do not auto-show after hide.
mHideAnimation.removeListener(mAutoStartShowListener);
// Update the buttons now that they are no longer visible.
updateFab(FAB_AND_BUTTONS_IMMEDIATE);
// Automatically start the grow animation now that shrinking is complete.
mShowAnimation.start();
// The animation to show the fab has begun; update the state to showing.
mFabState = FabState.SHOWING;
}
}
/**
* Shows/hides a snackbar as silencing settings are enabled/disabled.
*/
......
......@@ -16,12 +16,12 @@
package com.android.deskclock;
import android.app.Fragment;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import android.view.KeyEvent;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.uidata.UiDataModel.Tab;
......
......@@ -57,4 +57,10 @@ public interface FabController {
* @param right the button to the right of the fab component
*/
void onRightButtonClick(@NonNull Button right);
}
\ No newline at end of file
/**
*
* @return the target visibility of the FAB component
*/
int getFabTargetVisibility();
}
/*
* Copyright (C) 2016 The Android Open Source Project
* Copyright (C) 2020 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -16,27 +17,25 @@
package com.android.deskclock;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import androidx.legacy.app.FragmentCompat;
import androidx.viewpager.widget.PagerAdapter;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.android.deskclock.uidata.UiDataModel;
import java.util.Map;
/**
* This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
* adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
* This class produces the DeskClockFragments that are the content of the DeskClock tabs.
* It presents the tabs in LTR and RTL order depending on the text layout direction for the
* current locale. To prevent issues when switching between LTR and RTL, fragments are registered
* with the manager using position-independent tags, which is an important departure from
* FragmentPagerAdapter.
*/
final class FragmentTabPagerAdapter extends PagerAdapter {
public final class FragmentUtils {
private final DeskClock mDeskClock;
......@@ -46,31 +45,20 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
/** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;
/** The active fragment transaction if one exists. */
private FragmentTransaction mCurrentTransaction;
/** The current fragment displayed to the user. */
private Fragment mCurrentPrimaryItem;
private DeskClockFragment mCurrentPrimaryItem;
FragmentTabPagerAdapter(DeskClock deskClock) {
FragmentUtils(DeskClock deskClock) {
mDeskClock = deskClock;
mFragmentCache = new ArrayMap<>(getCount());
mFragmentManager = deskClock.getFragmentManager();
mFragmentManager = deskClock.getSupportFragmentManager();
}
@Override
public int getCount() {
private int getCount() {
return UiDataModel.getUiDataModel().getTabCount();
}
/**
* @param position the left-to-right index of the fragment to be returned
* @return the fragment displayed at the given {@code position}
*/
DeskClockFragment getDeskClockFragment(int position) {
// Fetch the tab the UiDataModel reports for the position.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
public DeskClockFragment getDeskClockFragment(UiDataModel.Tab tab) {
// First check the local cache for the fragment.
DeskClockFragment fragment = mFragmentCache.get(tab);
if (fragment != null) {
......@@ -91,78 +79,35 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
final String fragmentClassName = tab.getFragmentClassName();
fragment = (DeskClockFragment) Fragment.instantiate(mDeskClock, fragmentClassName);
fragment.setFabContainer(mDeskClock);
mFragmentCache.put(tab, fragment);
return fragment;
}
@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this + " has no id");
}
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
// Use the fragment located in the fragment manager if one exists.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
mCurrentTransaction.attach(fragment);
} else {
fragment = getDeskClockFragment(position);
mCurrentTransaction.add(container.getId(), fragment, tab.name());
}
if (fragment != mCurrentPrimaryItem) {
FragmentCompat.setMenuVisibility(fragment, false);
FragmentCompat.setUserVisibleHint(fragment, false);
}
FragmentTransaction transaction = mFragmentManager.beginTransaction();
transaction.add(R.id.fragment_container, fragment, tab.name());
transaction.commit();
mFragmentCache.put(tab, fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
final DeskClockFragment fragment = (DeskClockFragment) object;
fragment.setFabContainer(null);
mCurrentTransaction.detach(fragment);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
final Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false);
FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false);
}
public void hideAllFragments() {
FragmentTransaction transaction = mFragmentManager.beginTransaction();
for (UiDataModel.Tab tab : UiDataModel.Tab.values()) {
Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
FragmentCompat.setMenuVisibility(fragment, true);
FragmentCompat.setUserVisibleHint(fragment, true);
transaction.hide(fragment);
}
mCurrentPrimaryItem = fragment;
}
transaction.commit();
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
mFragmentManager.executePendingTransactions();
}
public void showFragment(UiDataModel.Tab tab) {
hideAllFragments();
Fragment fragment = getDeskClockFragment(tab);
mFragmentManager.beginTransaction().show(fragment).commit();
mCurrentPrimaryItem = (DeskClockFragment) fragment;