From 15afc017357fabff7a72e1407bc3ebd88372089c Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Sat, 13 Aug 2016 23:15:26 -0700 Subject: [PATCH] Translate FAB when scrolling between stopwatch page --- .../com/philliphsu/clock2/MainActivity.java | 60 ++++++++- .../clock2/stopwatch/StopwatchFragment.java | 122 +++++++++++------- .../res/layout-v21/fragment_stopwatch.xml | 69 ---------- app/src/main/res/layout/activity_main.xml | 4 +- .../main/res/layout/fragment_stopwatch.xml | 85 ++++++------ 5 files changed, 170 insertions(+), 170 deletions(-) delete mode 100644 app/src/main/res/layout-v21/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 da1ef05..945f992 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -1,6 +1,7 @@ package com.philliphsu.clock2; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.design.widget.FloatingActionButton; @@ -57,12 +58,63 @@ public class MainActivity extends BaseActivity { mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); mViewPager.setAdapter(mSectionsPagerAdapter); mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + /** + * @param position Either the current page position if the offset is increasing, + * or the previous page position if it is decreasing. + * @param positionOffset If increasing from [0, 1), scrolling right and position = currentPagePosition + * If decreasing from (1, 0], scrolling left and position = (currentPagePosition - 1) + */ + @Override + public void onPageScrolled(int position, float positionOffset, int 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) { + // When the scrolling is due to tab selection between multiple tabs apart, + // this callback is called for each intermediate page, but each of those pages + // will briefly register a sparsely decreasing range of positionOffsets, always + // from (1, 0). As such, you would notice the FAB to jump back and forth between + // x-positions as each intermediate page is scrolled through. + // This is a visual optimization that ends the translation motion, immediately + // returning the FAB to its target position. + mFab.setTranslationX(0); + } else { + // Initially, the FAB's translationX property is zero because, at its original + // position, it is not translated. setTranslationX() is relative to the view's + // left position, at its original position; this left position is taken to be + // the zero point of the coordinate system relative to this view. As your + // translationX value is increasingly negative, the view is translated left. + // But as translationX is decreasingly negative and down to zero, the view + // 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); + mFab.setTranslationX(translationX); + } + } + } + @Override public void onPageSelected(int position) { - if (position == mSectionsPagerAdapter.getCount() - 1) { - mFab.hide(); - } else { - mFab.show(); + if (position < mSectionsPagerAdapter.getCount() - 1) { + // TODO: Plus icon. Consider caching the Drawable in a member variable. + mFab.setImageResource(android.R.drawable.ic_dialog_email); } } // @Override 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 f198399..92457f2 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java @@ -12,13 +12,14 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; import android.widget.ProgressBar; import com.philliphsu.clock2.R; import com.philliphsu.clock2.RecyclerViewFragment; import com.philliphsu.clock2.util.ProgressBarUtils; +import java.lang.ref.WeakReference; + import butterknife.Bind; import butterknife.OnClick; @@ -43,11 +44,11 @@ public class StopwatchFragment extends RecyclerViewFragment< private AsyncLapsTableUpdateHandler mUpdateHandler; private ObjectAnimator mProgressAnimator; private SharedPreferences mPrefs; + private WeakReference mActivityFab; @Bind(R.id.chronometer) ChronometerWithMillis mChronometer; - @Bind(R.id.new_lap) ImageButton mNewLapButton; - @Bind(R.id.fab) FloatingActionButton mFab; - @Bind(R.id.stop) ImageButton mStopButton; + @Bind(R.id.new_lap) FloatingActionButton mNewLapButton; + @Bind(R.id.stop) FloatingActionButton mStopButton; @Bind(R.id.progress_bar) ProgressBar mProgressBar; /** @@ -62,6 +63,8 @@ public class StopwatchFragment extends RecyclerViewFragment< mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mStartTime = mPrefs.getLong(KEY_START_TIME, 0); mPauseTime = mPrefs.getLong(KEY_PAUSE_TIME, 0); + // TODO: Any better solutions? + mActivityFab = new WeakReference<>((FloatingActionButton) getActivity().findViewById(R.id.fab)); Log.d(TAG, "mStartTime = " + mStartTime + ", mPauseTime = " + mPauseTime); } @@ -82,6 +85,8 @@ public class StopwatchFragment extends RecyclerViewFragment< if (mPrefs.getBoolean(KEY_CHRONOMETER_RUNNING, false)) { mChronometer.start(); } + // Hides the mini fabs prematurely, so when we actually select this tab + // they don't show at all before hiding. updateButtonControls(); return view; } @@ -138,45 +143,8 @@ public class StopwatchFragment extends RecyclerViewFragment< } } - @Nullable @Override - protected LapsAdapter getAdapter() { - if (super.getAdapter() != null) - return super.getAdapter(); - return new LapsAdapter(); - } - - @Override - protected int contentLayout() { - return R.layout.fragment_stopwatch; - } - - @OnClick(R.id.new_lap) - void addNewLap() { - if (!mChronometer.isRunning()) { - Log.d(TAG, "Cannot add new lap"); - return; - } - if (mCurrentLap != null) { - mCurrentLap.end(mChronometer.getText().toString()); - } - mPreviousLap = mCurrentLap; - mCurrentLap = new Lap(); - if (mPreviousLap != null) { -// if (getAdapter().getItemCount() == 0) { -// mUpdateHandler.asyncInsert(mPreviousLap); -// } else { - mUpdateHandler.asyncUpdate(mPreviousLap.getId(), mPreviousLap); -// } - } - mUpdateHandler.asyncInsert(mCurrentLap); - // This would end up being called twice: here, and in onLoadFinished(), because the - // table updates will prompt us to requery. -// startNewProgressBarAnimator(); - } - - @OnClick(R.id.fab) - void startPause() { + public void onFabClick() { if (mChronometer.isRunning()) { mPauseTime = SystemClock.elapsedRealtime(); mChronometer.stop(); @@ -221,6 +189,65 @@ public class StopwatchFragment extends RecyclerViewFragment< savePrefs(); } + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + // We get called multiple times, even when we're not yet visible. + // This can be called before onCreateView() so widgets could be null, esp. if you had + // navigated more than one page away before returning here. That means onDestroyView() + // was called previously. + // We will get called again when we actually have this page selected, and by that time + // onCreateView() will have been called. Wait until we're resumed to call through. + if (isVisibleToUser && isResumed()) { + // At this point, the only thing this does is change the fab icon + // TODO: allow duplicate code and manipulate the fab icon directly? + // TODO: There is noticeable latency between showing this tab and + // changing the icon. Consider writing a callback for this Fragment + // that MainActivity can call in its onPageChangeListener. We don't merely + // want to call such a callback in onPageSelected, because that is fired + // when we reach an idle state, so we'd experience the same latency issue. + // Rather, we should animate the icon change during onPageScrolled. + updateButtonControls(); + } + } + + @Nullable + @Override + protected LapsAdapter getAdapter() { + if (super.getAdapter() != null) + return super.getAdapter(); + return new LapsAdapter(); + } + + @Override + protected int contentLayout() { + return R.layout.fragment_stopwatch; + } + + @OnClick(R.id.new_lap) + void addNewLap() { + if (!mChronometer.isRunning()) { + Log.d(TAG, "Cannot add new lap"); + return; + } + if (mCurrentLap != null) { + mCurrentLap.end(mChronometer.getText().toString()); + } + mPreviousLap = mCurrentLap; + mCurrentLap = new Lap(); + if (mPreviousLap != null) { +// if (getAdapter().getItemCount() == 0) { +// mUpdateHandler.asyncInsert(mPreviousLap); +// } else { + mUpdateHandler.asyncUpdate(mPreviousLap.getId(), mPreviousLap); +// } + } + mUpdateHandler.asyncInsert(mCurrentLap); + // This would end up being called twice: here, and in onLoadFinished(), because the + // table updates will prompt us to requery. +// startNewProgressBarAnimator(); + } + @OnClick(R.id.stop) void stop() { mChronometer.stop(); @@ -246,8 +273,10 @@ public class StopwatchFragment extends RecyclerViewFragment< int vis = started ? View.VISIBLE : View.INVISIBLE; mNewLapButton.setVisibility(vis); mStopButton.setVisibility(vis); - // TODO: pause and start icon, resp. - mFab.setImageResource(mChronometer.isRunning() ? 0 : 0); + if (isVisible()) { // avoid changing the icon prematurely, esp. when we're not on this tab + // TODO: pause and start icon, resp. + mActivityFab.get().setImageResource(mChronometer.isRunning() ? 0 : 0); + } } private void startNewProgressBarAnimator() { @@ -273,11 +302,6 @@ public class StopwatchFragment extends RecyclerViewFragment< // ======================= DO NOT IMPLEMENT ============================ - @Override - public void onFabClick() { - // DO NOT THROW AN UNSUPPORTED OPERATION EXCEPTION. - } - @Override protected void onScrolledToStableId(long id, int position) { throw new UnsupportedOperationException(); diff --git a/app/src/main/res/layout-v21/fragment_stopwatch.xml b/app/src/main/res/layout-v21/fragment_stopwatch.xml deleted file mode 100644 index cc7e233..0000000 --- a/app/src/main/res/layout-v21/fragment_stopwatch.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ff82b0e..0224018 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -43,7 +43,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:layout_margin="@dimen/fab_margin" - android:src="@android:drawable/ic_dialog_email"/> + android:src="@android:drawable/ic_dialog_email" + android:layout_margin="@dimen/fab_margin"/> diff --git a/app/src/main/res/layout/fragment_stopwatch.xml b/app/src/main/res/layout/fragment_stopwatch.xml index 2c848dc..f2c092a 100644 --- a/app/src/main/res/layout/fragment_stopwatch.xml +++ b/app/src/main/res/layout/fragment_stopwatch.xml @@ -9,54 +9,16 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - - - - - - - - - - - - - - - - + android:gravity="center_horizontal" + android:textSize="@dimen/text_size_display_3" + style="@style/TextAppearance.AppCompat.Inverse"/> @@ -67,8 +29,39 @@ android:id="@+id/progress_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_anchor="@id/top_panel" + app:layout_anchor="@id/chronometer" app:layout_anchorGravity="bottom" style="@style/Widget.AppCompat.ProgressBar.Horizontal"/> + + + + + + + + + \ No newline at end of file