From c0553b85b79f18197cade0db694f957ef52aae04 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Mon, 29 Aug 2016 22:37:49 -0700 Subject: [PATCH] Restore Activity FAB's translationX on rotation --- .../com/philliphsu/clock2/MainActivity.java | 71 +++++++++++++------ .../clock2/stopwatch/StopwatchFragment.java | 47 +++++++----- .../res/layout-land/fragment_stopwatch.xml | 71 +++++++++++++++++++ 3 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 app/src/main/res/layout-land/fragment_stopwatch.xml diff --git a/app/src/main/java/com/philliphsu/clock2/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java index 6049991..00a2c7a 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -11,6 +11,7 @@ import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewPager; +import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.MenuItem; @@ -59,9 +60,25 @@ public class MainActivity extends BaseActivity { // } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // TODO: On device rotation, if we were last on stopwatch page, restore the fab's translationX. + final View rootView = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0); + // http://stackoverflow.com/a/24035591/5055032 + // http://stackoverflow.com/a/3948036/5055032 + // The views in our layout have begun drawing. + // There is no lifecycle callback that tells us when our layout finishes drawing; + // in my own test, drawing still isn't finished by onResume(). + // Post a message in the UI events queue to be executed after drawing is complete, + // so that we may get their dimensions. + rootView.post(new Runnable() { + @Override + public void run() { + if (mViewPager.getCurrentItem() == mSectionsPagerAdapter.getCount() - 1) { + // Restore the FAB's translationX from a previous configuration. + mFab.setTranslationX(mViewPager.getWidth() / -2f + getFabPixelOffsetForXTranslation()); + } + } + }); // Create the adapter that will return a fragment for each of the three // primary sections of the activity. @@ -76,8 +93,8 @@ public class MainActivity extends BaseActivity { */ @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { -// Log.d(TAG, String.format("pos = %d, posOffset = %f, posOffsetPixels = %d", -// position, positionOffset, positionOffsetPixels)); + Log.d(TAG, String.format("pos = %d, posOffset = %f, posOffsetPixels = %d", + position, positionOffset, positionOffsetPixels)); int pageBeforeLast = mSectionsPagerAdapter.getCount() - 2; if (position <= pageBeforeLast) { if (position < pageBeforeLast) { @@ -101,22 +118,9 @@ public class MainActivity extends BaseActivity { // is translated right, back to its original position. float translationX = positionOffsetPixels / -2f; // NOTE: You MUST scale your own additional pixel offsets by positionOffset, - // or else the FAB will immediately translate by that many pixels, causing - // jitter as you scroll. - final int margin; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Since each side's margin is the same, any side's would do. - margin = ((ViewGroup.MarginLayoutParams) mFab.getLayoutParams()).rightMargin; - } else { - // Pre-Lollipop has measurement issues with FAB margins. This is - // probably as good as we can get to centering the FAB, without - // hardcoding some small margin value. - margin = 0; - } - // Translation is done relative to a view's left position; by adding - // an offset of half the FAB's width, we effectively rebase the translation - // relative to the view's center position. - translationX += positionOffset * (mFab.getWidth() / 2f + margin); + // or else the FAB will immediately translate by that many pixels, appearing + // to skip/jump. + translationX += positionOffset * getFabPixelOffsetForXTranslation(); mFab.setTranslationX(translationX); } } @@ -124,10 +128,15 @@ public class MainActivity extends BaseActivity { @Override public void onPageSelected(int position) { + Log.d(TAG, "onPageSelected"); if (position < mSectionsPagerAdapter.getCount() - 1) { mFab.setImageDrawable(mAddItemDrawable); } Fragment f = mSectionsPagerAdapter.getFragment(mViewPager.getCurrentItem()); + // NOTE: This callback is fired after a rotation, right after onStart(). + // Unfortunately, the FragmentManager handling the rotation has yet to + // tell our adapter to re-instantiate the Fragments, so our collection + // of fragments is empty. You MUST keep this check so we don't cause a NPE. if (f instanceof BaseFragment) { ((BaseFragment) f).onPageSelected(); } @@ -303,6 +312,28 @@ public class MainActivity extends BaseActivity { return super.onOptionsItemSelected(item); } + /** + * @return the positive offset in pixels required to rebase an X-translation of the FAB + * relative to its center position. An X-translation normally is done relative to a view's + * left position. + */ + private float getFabPixelOffsetForXTranslation() { + final int margin; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Since each side's margin is the same, any side's would do. + margin = ((ViewGroup.MarginLayoutParams) mFab.getLayoutParams()).rightMargin; + } else { + // Pre-Lollipop has measurement issues with FAB margins. This is + // probably as good as we can get to centering the FAB, without + // hardcoding some small margin value. + margin = 0; + } + // X-translation is done relative to a view's left position; by adding + // an offset of half the FAB's width, we effectively rebase the translation + // relative to the view's center position. + return mFab.getWidth() / 2f + margin; + } + /** * A placeholder fragment containing a simple view. */ diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java index 76e2108..853f94b 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java @@ -109,15 +109,30 @@ public class StopwatchFragment extends RecyclerViewFragment< @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - // TODO: Any better alternatives? // TOneverDO: Move to onCreate(). When the device rotates, onCreate() _is_ called, // but trying to find the FAB in the Activity's layout will fail, and we would get back // a null reference. This is probably because this Fragment's onCreate() is called // BEFORE the Activity's onCreate. + // TODO: Any better alternatives to control the Activity's FAB from here? mActivityFab = new WeakReference<>((FloatingActionButton) getActivity().findViewById(R.id.fab)); - if (savedInstanceState != null) { + // There is no documentation for isMenuVisible(), so what exactly does it do? + // My guess is it checks for the Fragment's options menu. But we never initialize this + // Fragment with setHasOptionsMenu(), let alone we don't actually inflate a menu in here. + // My guess is when this Fragment becomes actually visible, it "hooks" onto the menu + // options "internal API" and inflates its menu in there if it has one. + // + // To us, this just makes for a very good visibility check. + if (savedInstanceState != null && isMenuVisible()) { // This is a pretty good indication that we just rotated. -// updateMiniFabs(); // TODO: Do we need this? + // isMenuVisible() filters out the case when you rotate on page 1 and scroll + // to page 2, the icon will prematurely change; that happens because at page 2, + // this Fragment will be instantiated for the first time for the current configuration, + // and so the lifecycle from onCreate() to onActivityCreated() occurs. As such, + // we will have a non-null savedInstanceState and this would call through. + // + // The reason when you open up the app for the first time and scrolling to page 2 + // doesn't prematurely change the icon is the savedInstanceState is null, and so + // this call would be filtered out sufficiently just from the first check. updateFab(); } } @@ -232,8 +247,10 @@ public class StopwatchFragment extends RecyclerViewFragment< // mProgressAnimator.resume(); // } } - updateAllFabs(); savePrefs(); + // TOneverDO: Precede savePrefs(), or else we don't save false to KEY_CHRONOMETER_RUNNING + /// and updateFab will update the wrong icon. + updateAllFabs(); } @Override @@ -306,7 +323,15 @@ public class StopwatchFragment extends RecyclerViewFragment< private void updateAllFabs() { updateMiniFabs(); - updateFab(); + // TODO: If we're calling this method, then chances are we are visible. + // You can verify this yourself by finding all usages. + // isVisible() is good for filtering out calls to this method when this Fragment + // isn't actually visible to the user; however, a side effect is it also filters + // out calls to this method when this Fragment is rotated. Fortunately, we don't + // make any calls to this method after a rotation. + if (isVisible()) { + updateFab(); + } } private void updateMiniFabs() { @@ -317,17 +342,7 @@ public class StopwatchFragment extends RecyclerViewFragment< } private void updateFab() { - // Avoid changing the icon in premature cases. - // isVisible() is good for filtering out calls to this method when this Fragment - // isn't actually visible to the user; however, a side effect is it also filters - // out calls to this method when this Fragment is rotated. - // isMenuVisible() is good for the rotation case; however, a side effect is when you - // rotate on page 1 and scroll to page 2, the icon prematurely changes. Fortunately, - // for every page change after that, the icon no longer prematurely changes. - // TODO: If you can live with that, then move on. - if ((isVisible() || isMenuVisible()) && mActivityFab != null) { - mActivityFab.get().setImageDrawable(isStopwatchRunning() ? mPauseDrawable : mStartDrawable); - } + mActivityFab.get().setImageDrawable(isStopwatchRunning() ? mPauseDrawable : mStartDrawable); } private void startNewProgressBarAnimator() { diff --git a/app/src/main/res/layout-land/fragment_stopwatch.xml b/app/src/main/res/layout-land/fragment_stopwatch.xml new file mode 100644 index 0000000..e1744e9 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_stopwatch.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file