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.app.FragmentPagerAdapter;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -59,9 +60,25 @@ public class MainActivity extends BaseActivity {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(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
|
// Create the adapter that will return a fragment for each of the three
|
||||||
// primary sections of the activity.
|
// primary sections of the activity.
|
||||||
@ -76,8 +93,8 @@ public class MainActivity extends BaseActivity {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||||
// Log.d(TAG, String.format("pos = %d, posOffset = %f, posOffsetPixels = %d",
|
Log.d(TAG, String.format("pos = %d, posOffset = %f, posOffsetPixels = %d",
|
||||||
// position, positionOffset, positionOffsetPixels));
|
position, positionOffset, positionOffsetPixels));
|
||||||
int pageBeforeLast = mSectionsPagerAdapter.getCount() - 2;
|
int pageBeforeLast = mSectionsPagerAdapter.getCount() - 2;
|
||||||
if (position <= pageBeforeLast) {
|
if (position <= pageBeforeLast) {
|
||||||
if (position < pageBeforeLast) {
|
if (position < pageBeforeLast) {
|
||||||
@ -101,22 +118,9 @@ public class MainActivity extends BaseActivity {
|
|||||||
// is translated right, back to its original position.
|
// is translated right, back to its original position.
|
||||||
float translationX = positionOffsetPixels / -2f;
|
float translationX = positionOffsetPixels / -2f;
|
||||||
// NOTE: You MUST scale your own additional pixel offsets by positionOffset,
|
// NOTE: You MUST scale your own additional pixel offsets by positionOffset,
|
||||||
// or else the FAB will immediately translate by that many pixels, causing
|
// or else the FAB will immediately translate by that many pixels, appearing
|
||||||
// jitter as you scroll.
|
// to skip/jump.
|
||||||
final int margin;
|
translationX += positionOffset * getFabPixelOffsetForXTranslation();
|
||||||
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);
|
|
||||||
mFab.setTranslationX(translationX);
|
mFab.setTranslationX(translationX);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,10 +128,15 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPageSelected(int position) {
|
public void onPageSelected(int position) {
|
||||||
|
Log.d(TAG, "onPageSelected");
|
||||||
if (position < mSectionsPagerAdapter.getCount() - 1) {
|
if (position < mSectionsPagerAdapter.getCount() - 1) {
|
||||||
mFab.setImageDrawable(mAddItemDrawable);
|
mFab.setImageDrawable(mAddItemDrawable);
|
||||||
}
|
}
|
||||||
Fragment f = mSectionsPagerAdapter.getFragment(mViewPager.getCurrentItem());
|
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) {
|
if (f instanceof BaseFragment) {
|
||||||
((BaseFragment) f).onPageSelected();
|
((BaseFragment) f).onPageSelected();
|
||||||
}
|
}
|
||||||
@ -303,6 +312,28 @@ public class MainActivity extends BaseActivity {
|
|||||||
return super.onOptionsItemSelected(item);
|
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.
|
* A placeholder fragment containing a simple view.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -109,15 +109,30 @@ public class StopwatchFragment extends RecyclerViewFragment<
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
// TODO: Any better alternatives?
|
|
||||||
// TOneverDO: Move to onCreate(). When the device rotates, onCreate() _is_ called,
|
// 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
|
// 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
|
// a null reference. This is probably because this Fragment's onCreate() is called
|
||||||
// BEFORE the Activity's onCreate.
|
// 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));
|
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.
|
// 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();
|
updateFab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,8 +247,10 @@ public class StopwatchFragment extends RecyclerViewFragment<
|
|||||||
// mProgressAnimator.resume();
|
// mProgressAnimator.resume();
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
updateAllFabs();
|
|
||||||
savePrefs();
|
savePrefs();
|
||||||
|
// TOneverDO: Precede savePrefs(), or else we don't save false to KEY_CHRONOMETER_RUNNING
|
||||||
|
/// and updateFab will update the wrong icon.
|
||||||
|
updateAllFabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -306,7 +323,15 @@ public class StopwatchFragment extends RecyclerViewFragment<
|
|||||||
|
|
||||||
private void updateAllFabs() {
|
private void updateAllFabs() {
|
||||||
updateMiniFabs();
|
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() {
|
private void updateMiniFabs() {
|
||||||
@ -317,17 +342,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateFab() {
|
private void updateFab() {
|
||||||
// Avoid changing the icon in premature cases.
|
mActivityFab.get().setImageDrawable(isStopwatchRunning() ? mPauseDrawable : mStartDrawable);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startNewProgressBarAnimator() {
|
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