Fixed SeekBar not animating after device rotation

This commit is contained in:
Phillip Hsu 2016-08-28 22:32:16 -07:00
parent b03c6123e9
commit 2daf03b754
6 changed files with 104 additions and 47 deletions

View File

@ -0,0 +1,29 @@
package com.philliphsu.clock2;
import android.content.Context;
import android.support.v7.widget.AppCompatSeekBar;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* A SeekBar that cannot be touch controlled.
*/
public class UntouchableSeekBar extends AppCompatSeekBar {
public UntouchableSeekBar(Context context) {
super(context);
}
public UntouchableSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UntouchableSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public final boolean onTouchEvent(MotionEvent event) {
return true;
}
}

View File

@ -21,7 +21,11 @@ import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
@ -64,6 +68,9 @@ public class ChronometerWithMillis extends TextView {
private StringBuilder mFormatBuilder; private StringBuilder mFormatBuilder;
private OnChronometerTickListener mOnChronometerTickListener; private OnChronometerTickListener mOnChronometerTickListener;
private StringBuilder mRecycle = new StringBuilder(8); private StringBuilder mRecycle = new StringBuilder(8);
private boolean mApplySizeSpan;
private static final RelativeSizeSpan SIZE_SPAN = new RelativeSizeSpan(0.5f);
private static final int TICK_WHAT = 2; private static final int TICK_WHAT = 2;
@ -223,6 +230,11 @@ public class ChronometerWithMillis extends TextView {
return mRunning; return mRunning;
} }
public void setApplySizeSpan(boolean applySizeSpan) {
mApplySizeSpan = applySizeSpan;
init(); // update text again
}
@Override @Override
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
@ -272,8 +284,15 @@ public class ChronometerWithMillis extends TextView {
// It looks like Google's Clock app strictly uses . // It looks like Google's Clock app strictly uses .
".%02d", // The . before % is not a format specifier ".%02d", // The . before % is not a format specifier
centiseconds); centiseconds);
if (mApplySizeSpan) {
SpannableString span = new SpannableString(centisecondsText);
span.setSpan(SIZE_SPAN, 0, centisecondsText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(TextUtils.concat(text, span), BufferType.SPANNABLE);
} else {
setText(text.concat(centisecondsText)); setText(text.concat(centisecondsText));
} }
}
private void updateRunning() { private void updateRunning() {
boolean running = mVisible && mStarted; boolean running = mVisible && mStarted;

View File

@ -87,6 +87,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
// have a null reference. // have a null reference.
mActivityFab = new WeakReference<>((FloatingActionButton) getActivity().findViewById(R.id.fab)); mActivityFab = new WeakReference<>((FloatingActionButton) getActivity().findViewById(R.id.fab));
mChronometer.setApplySizeSpan(true);
if (mStartTime > 0) { if (mStartTime > 0) {
long base = mStartTime; long base = mStartTime;
if (mPauseTime > 0) { if (mPauseTime > 0) {
@ -95,8 +96,11 @@ public class StopwatchFragment extends RecyclerViewFragment<
} }
mChronometer.setBase(base); mChronometer.setBase(base);
} }
if (mPrefs.getBoolean(KEY_CHRONOMETER_RUNNING, false)) { if (wasChronometerRunning()) {
mChronometer.start(); mChronometer.start();
// Note: mChronometer.isRunning() will return false at this point and
// in other upcoming lifecycle methods because it is not yet visible
// (i.e. mVisible == false).
} }
// Hides the mini fabs prematurely, so when we actually display this tab // Hides the mini fabs prematurely, so when we actually display this tab
// they won't show even briefly at all before hiding. // they won't show even briefly at all before hiding.
@ -115,6 +119,13 @@ public class StopwatchFragment extends RecyclerViewFragment<
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
// Every view that was in our tree is dereferenced for us.
// The reason we can control the animator here is because members
// are not dereferenced here, as evidenced by mStartTime and mPauseTime
// retaining their values.
if (mProgressAnimator != null) {
mProgressAnimator.removeAllListeners();
}
Log.d(TAG, "onDestroyView()"); Log.d(TAG, "onDestroyView()");
Log.d(TAG, "mStartTime = " + mStartTime Log.d(TAG, "mStartTime = " + mStartTime
+ ", mPauseTime = " + mPauseTime); + ", mPauseTime = " + mPauseTime);
@ -127,17 +138,18 @@ public class StopwatchFragment extends RecyclerViewFragment<
@Override @Override
public void onLoadFinished(Loader<LapCursor> loader, LapCursor data) { public void onLoadFinished(Loader<LapCursor> loader, LapCursor data) {
Log.d(TAG, "onLoadFinished()");
super.onLoadFinished(loader, data); super.onLoadFinished(loader, data);
// TODO: Will manipulating the cursor's position here affect the current // TODO: Will manipulating the cursor's position here affect the current
// position in the adapter? Should we make a defensive copy and manipulate // position in the adapter? Should we make a defensive copy and manipulate
// that copy instead? // that copy instead?
if (data.moveToFirst()) { if (data.moveToFirst()) {
mCurrentLap = data.getItem(); mCurrentLap = data.getItem();
Log.d(TAG, "Current lap ID = " + mCurrentLap.getId()); // Log.d(TAG, "Current lap ID = " + mCurrentLap.getId());
} }
if (data.moveToNext()) { if (data.moveToNext()) {
mPreviousLap = data.getItem(); mPreviousLap = data.getItem();
Log.d(TAG, "Previous lap ID = " + mPreviousLap.getId()); // Log.d(TAG, "Previous lap ID = " + mPreviousLap.getId());
} }
if (mCurrentLap != null && mPreviousLap != null) { if (mCurrentLap != null && mPreviousLap != null) {
// We really only want to start a new animator when the NEWLY RETRIEVED current // We really only want to start a new animator when the NEWLY RETRIEVED current
@ -151,7 +163,10 @@ public class StopwatchFragment extends RecyclerViewFragment<
// have different values for, e.g., t1 and pauseTime. // have different values for, e.g., t1 and pauseTime.
// //
// Therefore, we'll just always end the previous animator and start a new one. // Therefore, we'll just always end the previous animator and start a new one.
if (mChronometer.isRunning()) { //
// NOTE: If we just recreated ourselves due to rotation, mChronometer.isRunning() == false,
// because it is not yet visible (i.e. mVisible == false).
if (mChronometer.isRunning() || wasChronometerRunning()) {
startNewProgressBarAnimator(); startNewProgressBarAnimator();
} else { } else {
// I verified the bar was visible already without this, so we probably don't need this, // I verified the bar was visible already without this, so we probably don't need this,
@ -299,10 +314,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
mSeekBar.setVisibility(View.VISIBLE); mSeekBar.setVisibility(View.VISIBLE);
mProgressAnimator = ProgressBarUtils.startNewAnimator( mProgressAnimator = ProgressBarUtils.startNewAnimator(
mSeekBar, getCurrentLapProgressRatio(), timeRemaining); mSeekBar, getCurrentLapProgressRatio(), timeRemaining);
mProgressAnimator.addListener(mAnimatorListener); mProgressAnimator.addListener(new Animator.AnimatorListener() {
}
private final Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
private boolean cancelled; private boolean cancelled;
@Override @Override
@ -333,7 +345,8 @@ public class StopwatchFragment extends RecyclerViewFragment<
public void onAnimationRepeat(Animator animation) { public void onAnimationRepeat(Animator animation) {
} }
}; });
}
private double getCurrentLapProgressRatio() { private double getCurrentLapProgressRatio() {
if (mPreviousLap == null) if (mPreviousLap == null)
@ -356,6 +369,9 @@ public class StopwatchFragment extends RecyclerViewFragment<
.apply(); .apply();
} }
private boolean wasChronometerRunning() {
return mPrefs.getBoolean(KEY_CHRONOMETER_RUNNING, false);
}
// ======================= DO NOT IMPLEMENT ============================ // ======================= DO NOT IMPLEMENT ============================
@Override @Override

View File

@ -17,14 +17,12 @@ import com.philliphsu.clock2.util.ProgressBarUtils;
import butterknife.Bind; import butterknife.Bind;
import butterknife.OnClick; import butterknife.OnClick;
import butterknife.OnTouch;
/** /**
* Created by Phillip Hsu on 7/25/2016. * Created by Phillip Hsu on 7/25/2016.
*/ */
public class TimerViewHolder extends BaseViewHolder<Timer> { public class TimerViewHolder extends BaseViewHolder<Timer> {
private static final String TAG = "TimerViewHolder"; private static final String TAG = "TimerViewHolder";
private static final int MAX_PROGRESS = 100000;
private final AsyncTimersTableUpdateHandler mAsyncTimersTableUpdateHandler; private final AsyncTimersTableUpdateHandler mAsyncTimersTableUpdateHandler;
private TimerController mController; private TimerController mController;
@ -71,11 +69,6 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
mController.stop(); mController.stop();
} }
@OnTouch(R.id.seek_bar)
boolean captureTouchOnSeekBar() {
return true; // Do nothing when the user touches the seek bar
}
private void bindLabel(String label) { private void bindLabel(String label) {
if (!label.isEmpty()) { if (!label.isEmpty()) {
mLabel.setText(label); mLabel.setText(label);
@ -137,9 +130,10 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
if (!timer.isRunning()) { if (!timer.isRunning()) {
// If our scale were 1, then casting ratio to an int will ALWAYS // If our scale were 1, then casting ratio to an int will ALWAYS
// truncate down to zero. // truncate down to zero.
mSeekBar.setMax(100); // mSeekBar.setMax(100);
final int progress = (int) (100 * ratio); // final int progress = (int) (100 * ratio);
mSeekBar.setProgress(progress); // mSeekBar.setProgress(progress);
ProgressBarUtils.setProgress(mSeekBar, ratio);
// mSeekBar.getThumb().mutate().setAlpha(progress == 0 ? 0 : 255); // mSeekBar.getThumb().mutate().setAlpha(progress == 0 ? 0 : 255);
} else { } else {
// mSeekBar.getThumb().mutate().setAlpha(255); // mSeekBar.getThumb().mutate().setAlpha(255);

View File

@ -26,7 +26,7 @@
</LinearLayout> </LinearLayout>
<SeekBar <com.philliphsu.clock2.UntouchableSeekBar
android:id="@+id/seek_bar" android:id="@+id/seek_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -33,9 +33,8 @@
<!--TODO: Consider removing this bottom margin, because the seekbar <!--TODO: Consider removing this bottom margin, because the seekbar
is rendering with HUGE top and bottom padding already. --> is rendering with HUGE top and bottom padding already. -->
<!-- Cannot be touch controlled -->
<!--The default style has padding start and end, so we remove both--> <!--The default style has padding start and end, so we remove both-->
<SeekBar <com.philliphsu.clock2.UntouchableSeekBar
android:id="@+id/seek_bar" android:id="@+id/seek_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"