Restore Activity FAB's translationX on rotation

This commit is contained in:
Phillip Hsu 2016-08-29 22:37:49 -07:00
parent 925424c882
commit c0553b85b7
3 changed files with 153 additions and 36 deletions

View File

@ -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.
*/

View File

@ -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() {

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.philliphsu.clock2.stopwatch.ChronometerWithMillis
android:id="@+id/chronometer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@color/colorPrimary"
android:gravity="center_horizontal"
android:textSize="@dimen/text_size_display_3"
style="@style/TextAppearance.AppCompat.Inverse"
android:layout_marginBottom="8dp"/>
<!-- RecyclerView -->
<include layout="@layout/fragment_recycler_view"/>
</LinearLayout>
<com.philliphsu.clock2.UntouchableSeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_anchor="@id/chronometer"
app:layout_anchorGravity="bottom"
android:paddingStart="0dp"
android:paddingEnd="0dp"/>
<!-- TODO: dimen resource for height -->
<android.support.v7.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_gravity="bottom"
app:columnCount="2">
<android.support.design.widget.FloatingActionButton
android:id="@+id/new_lap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_add_lap_24dp"
android:tint="@android:color/white"
android:visibility="invisible"
app:layout_columnWeight="1"
app:layout_gravity="center"
app:fabSize="mini"
app:backgroundTint="@color/colorPrimary"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_stop_24dp"
android:tint="@android:color/white"
android:visibility="invisible"
app:layout_columnWeight="1"
app:layout_gravity="center"
app:fabSize="mini"
app:backgroundTint="@color/colorPrimary"/>
</android.support.v7.widget.GridLayout>
</android.support.design.widget.CoordinatorLayout>