Restore Activity FAB's translationX on rotation
This commit is contained in:
parent
925424c882
commit
c0553b85b7
@ -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.
|
||||
*/
|
||||
|
||||
@ -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() {
|
||||
|
||||
71
app/src/main/res/layout-land/fragment_stopwatch.xml
Normal file
71
app/src/main/res/layout-land/fragment_stopwatch.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user