package com.pluscubed.plustimer.ui.currentsessiontimer;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Property;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.couchbase.lite.CouchbaseLiteException;
import com.pluscubed.plustimer.R;
import com.pluscubed.plustimer.base.BasePresenterFragment;
import com.pluscubed.plustimer.base.PresenterFactory;
import com.pluscubed.plustimer.base.RecyclerViewUpdate;
import com.pluscubed.plustimer.model.PuzzleType;
import com.pluscubed.plustimer.model.ScrambleAndSvg;
import com.pluscubed.plustimer.model.Session;
import com.pluscubed.plustimer.model.Solve;
import com.pluscubed.plustimer.utils.PrefUtils;
import com.pluscubed.plustimer.utils.Utils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import rx.Single;
import rx.SingleSubscriber;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
/**
* TimerFragment
*/
public class CurrentSessionTimerFragment extends BasePresenterFragment<CurrentSessionTimerPresenter, CurrentSessionTimerView>
implements CurrentSessionTimerView {
public static final String TAG = "CS_TIMER_FRAGMENT";
private static final long HOLD_TIME = 550000000L;
private static final int REFRESH_RATE = 15;
private static final String STATE_IMAGE_DISPLAYED = "scramble_image_displayed_boolean";
private static final String STATE_START_TIME = "start_time_long";
private static final String STATE_RUNNING = "running_boolean";
private static final String STATE_INSPECTING = "inspecting_boolean";
private static final String STATE_INSPECTION_START_TIME = "inspection_start_time_long";
//Preferences
private boolean mHoldToStartEnabled;
private boolean mInspectionEnabled;
private boolean mTwoRowTimeEnabled;
private PrefUtils.TimerUpdate mUpdateTimePref;
private boolean mMillisecondsEnabled;
private boolean mMonospaceScrambleFontEnabled;
private int mTimerTextSize;
private int mScrambleTextSize;
private boolean mKeepScreenOn;
private boolean mSignEnabled;
//Retained Fragment
private CurrentSessionTimerRetainedFragment mRetainedFragment;
//Views
private TextView mTimerText;
private TextView mTimerText2;
private TextView mScrambleText;
private View mScrambleTextShadow;
private RecyclerView mTimeBarRecycler;
private ImageView mScrambleImage;
private TextView mStatsSolvesText;
private TextView mStatsText;
private LinearLayout mLastBarLinearLayout;
private Button mLastDnfButton;
private Button mLastPlusTwoButton;
private Button mLastDeleteButton;
private FrameLayout mDynamicStatusBarFrame;
private TextView mDynamicStatusBarText;
//Handler
private Handler mUiHandler;
//Dynamic status variables
private boolean mDynamicStatusBarVisible;
private boolean mHoldTiming;
private long mHoldTimerStartTimestamp;
private boolean mInspecting;
private long mInspectionStartTimestamp;
private long mInspectionStopTimestamp;
private long mTimingStartTimestamp;
private boolean mFromSavedInstanceState;
private boolean mTiming;
private boolean mScrambleImageDisplay;
private boolean mLateStartPenalty;
private boolean mBldMode;
private final Runnable timerRunnable = new Runnable() {
@Override
public void run() {
if (mUpdateTimePref != PrefUtils.TimerUpdate.OFF) {
if (!mBldMode) {
setTimerText(Utils.timeStringsFromNsSplitByDecimal(
System.nanoTime() - mTimingStartTimestamp,
mMillisecondsEnabled));
} else {
setTimerText(Utils.timeStringsFromNsSplitByDecimal(
System.nanoTime() - mInspectionStartTimestamp,
mMillisecondsEnabled));
}
setTimerTextToPrefSize();
mUiHandler.postDelayed(this, REFRESH_RATE);
} else {
setTimerText(new String[]{getString(R.string.timing), ""});
mTimerText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 100);
}
}
};
private AnimatorSet mLastBarAnimationSet;
private ValueAnimator mScrambleAnimator;
private ObjectAnimator mScrambleElevationAnimator;
private int mGreen500;
//Runnables
private final Runnable holdTimerRunnable = new Runnable() {
@Override
public void run() {
setTextColor(mGreen500);
setTimerTextToPrefSize();
if (!mInspecting) {
playExitAnimations();
getActivityCallback().lockDrawerAndViewPager(true);
}
}
};
private final Runnable inspectionRunnable = new Runnable() {
@Override
public void run() {
String[] array = Utils.timeStringsFromNsSplitByDecimal(
16000000000L - (System.nanoTime() - mInspectionStartTimestamp),
mMillisecondsEnabled);
array[1] = "";
if (15000000000L - (System.nanoTime() - mInspectionStartTimestamp) > 0) {
//If inspection proceeding normally
setTimerText(array);
} else {
if (17000000000L - (System.nanoTime() - mInspectionStartTimestamp) > 0) {
//If late start
mLateStartPenalty = true;
setTimerText(new String[]{"+2", ""});
} else {
//If past 17 seconds which means DNF
stopHoldTimer();
stopInspection();
playEnterAnimations();
PuzzleType.getCurrent(getContextCompat())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.subscribe(session -> {
try {
session.newSolve(getActivity())
.setScramble(mRetainedFragment.getCurrentScrambleAndSvg().getScramble())
.setRawTime(0)
.setPenalty(Solve.PENALTY_DNF)
.build();
} catch (CouchbaseLiteException | IOException e) {
e.printStackTrace();
}
});
//Add the solve to the current session with the current
// scramble/scramble image and DNF
//mPresenter.onTimingFinished(s);
resetTimer();
invalidateTimerText();
if (mRetainedFragment.isScrambling()) {
setScrambleText(getString(R.string.scrambling));
}
mRetainedFragment.postSetScrambleViewsToCurrent();
return;
}
}
//If proceeding normally or +2
setTimerTextToPrefSize();
mUiHandler.postDelayed(this, REFRESH_RATE);
}
};
private TimeBarRecyclerAdapter mTimeBarAdapter;
private int mRed500;
//Generate string with specified current averages and mean of current session
private Single<String> buildStatsWithAveragesOf(Context context,
Integer... currentAverageSpecs) {
Arrays.sort(currentAverageSpecs, Collections.reverseOrder());
return PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.flatMapObservable(session -> session.getSortedSolves(getActivity())).toList().toSingle()
.map(solves -> {
String s = "";
for (int i : currentAverageSpecs) {
if (solves.size() >= i) {
s += String.format(context.getString(R.string.cao), i) + ": " +
Session.getStringCurrentAverageOf(solves, i, mMillisecondsEnabled).toBlocking().value() + "\n";
}
}
if (solves.size() > 0) {
s += context.getString(R.string.mean) + Session.getStringMean(solves, mMillisecondsEnabled);
}
return s;
});
}
public CurrentSessionTimerPresenter getPresenter() {
return presenter;
}
/**
* Set timer textviews using an array. Hides/shows lower textview
* depending on preferences
* and whether the second array item is blank.
*
* @param array An array of 2 strings
*/
void setTimerText(String[] array) {
if (mTwoRowTimeEnabled) {
mTimerText.setText(array[0]);
mTimerText2.setText(array[1]);
if (array[1].equals("") || (mTiming && mUpdateTimePref != PrefUtils.TimerUpdate.ON)) {
mTimerText2.setVisibility(View.GONE);
} else {
mTimerText2.setVisibility(View.VISIBLE);
}
} else {
mTimerText2.setVisibility(View.GONE);
mTimerText.setText(array[0]);
if (!array[1].equals("") && !(mTiming && (mUpdateTimePref != PrefUtils.TimerUpdate
.ON))) {
mTimerText.append("." + array[1]);
}
}
}
public void setScrambleText(String text) {
mScrambleText.setText(text);
invalidateScrambleShadow(false);
}
private void invalidateScrambleShadow(final boolean overrideShowShadow) {
Runnable animate = () -> {
if (mScrambleElevationAnimator != null) {
mScrambleElevationAnimator.cancel();
}
Property<View, Float> property;
View view;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
property = View.TRANSLATION_Z;
view = mScrambleText;
} else {
property = View.ALPHA;
view = mScrambleTextShadow;
}
mScrambleElevationAnimator = ObjectAnimator.ofFloat(view,
property, getScrambleTextElevationOrShadowAlpha(overrideShowShadow));
mScrambleElevationAnimator.setDuration(150);
mScrambleElevationAnimator.start();
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mScrambleText.postOnAnimation(animate);
} else {
mScrambleText.post(animate);
}
}
//Taken from http://stackoverflow.com/questions/3619693
private int getRelativeTop(View view) {
if (view.getParent() == view.getRootView())
return view.getTop();
else
return view.getTop() + getRelativeTop((View) view.getParent());
}
private float getScrambleTextElevationOrShadowAlpha(boolean override) {
boolean overlap = getRelativeTop(mScrambleText) + Utils.getTextViewHeight(mScrambleText)
> getRelativeTop(mTimerText);
float rootFrameTranslationY = getActivityCallback() != null ?
getActivityCallback().getContentFrameLayout().getTranslationY() : 0f;
boolean shadowShown = overlap || rootFrameTranslationY != 0 || override;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return shadowShown ? Utils.convertDpToPx(getActivity(), 2) : 0f;
} else {
return shadowShown ? 1f : 0f;
}
}
//Set scramble text and scramble image to current ones
public void setScrambleTextAndImageToCurrent() {
ScrambleAndSvg currentScrambleAndSvg = mRetainedFragment.getCurrentScrambleAndSvg();
if (currentScrambleAndSvg != null) {
SVG svg = null;
try {
svg = SVG.getFromString(currentScrambleAndSvg.getSvg());
} catch (SVGParseException e) {
e.printStackTrace();
}
Drawable drawable = null;
if (svg != null) {
drawable = new PictureDrawable(svg.renderToPicture());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mScrambleImage.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
mScrambleImage.setImageDrawable(drawable);
Utils.getUiScramble(getActivity(),
currentScrambleAndSvg.getScramble(), mSignEnabled,
PuzzleType.getCurrentId(getActivity()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setScrambleText);
} else {
mRetainedFragment.generateNextScramble();
mRetainedFragment.postSetScrambleViewsToCurrent();
}
}
@Override
public void updateStatsAndTimerText(RecyclerViewUpdate mode, Solve solve) {
//Update stats
PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.map(Session::getNumberOfSolves)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleSubscriber<Integer>() {
@Override
public void onSuccess(Integer numberOfSolves) {
mStatsSolvesText.setText(getString(R.string.solves_colon) + numberOfSolves);
}
@Override
public void onError(Throwable error) {
}
});
buildStatsWithAveragesOf(getActivity(), 5, 12, 100)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleSubscriber<String>() {
@Override
public void onSuccess(String stats) {
mStatsText.setText(stats);
}
@Override
public void onError(Throwable error) {
}
});
if (!mTiming && !mInspecting) {
if (solve != null && mode == RecyclerViewUpdate.INSERT) {
setTimerTextFromSolve(solve);
} else {
invalidateTimerText();
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
//Toggle image button
case R.id.menu_activity_current_session_scramble_image_menuitem:
toggleScrambleImage();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
//Set up UIHandler
mUiHandler = new Handler(Looper.getMainLooper());
initSharedPrefs();
if (savedInstanceState != null) {
mScrambleImageDisplay = savedInstanceState.getBoolean
(STATE_IMAGE_DISPLAYED);
mTimingStartTimestamp = savedInstanceState.getLong
(STATE_START_TIME);
mTiming = savedInstanceState.getBoolean(STATE_RUNNING);
mInspecting = savedInstanceState.getBoolean(STATE_INSPECTING);
mInspectionStartTimestamp = savedInstanceState.getLong
(STATE_INSPECTION_START_TIME);
mFromSavedInstanceState = true;
} else {
mFromSavedInstanceState = false;
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_current_session_timer, container, false);
mRed500 = ContextCompat.getColor(getActivity(), R.color.red_500);
mGreen500 = ContextCompat.getColor(getActivity(), R.color.green);
mTimerText = (TextView) v.findViewById(R.id.fragment_current_session_timer_time_textview);
mTimerText2 = (TextView) v.findViewById(R.id.fragment_current_session_timer_timeSecondary_textview);
mScrambleText = (TextView) v.findViewById(R.id.fragment_current_session_timer_scramble_textview);
mScrambleTextShadow = v.findViewById(R.id.fragment_current_session_timer_scramble_shadow);
mScrambleImage = (ImageView) v.findViewById(R.id.fragment_current_session_timer_scramble_imageview);
mTimeBarRecycler = (RecyclerView) v.findViewById(R.id.fragment_current_session_timer_timebar_recycler);
mStatsText = (TextView) v.findViewById(R.id.fragment_current_session_timer_stats_textview);
mStatsSolvesText = (TextView) v.findViewById(R.id.fragment_current_session_timer_stats_solves_number_textview);
mLastBarLinearLayout = (LinearLayout) v.findViewById(R.id.fragment_current_session_timer_last_linearlayout);
mLastDnfButton = (Button) v.findViewById(R.id.fragment_current_session_timer_last_dnf_button);
mLastPlusTwoButton = (Button) v.findViewById(R.id.fragment_current_session_timer_last_plustwo_button);
mLastDeleteButton = (Button) v.findViewById(R.id.fragment_current_session_timer_last_delete_button);
mLastDnfButton.setOnClickListener(view -> PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleSubscriber<Session>() {
@Override
public void onSuccess(Session session) {
try {
Solve solve = session.getLastSolve(getActivity()).toBlocking().first();
solve.setPenalty(getActivity(), Solve.PENALTY_DNF);
Session.notifyListeners(session.getId(), solve, RecyclerViewUpdate.SINGLE_CHANGE);
} catch (CouchbaseLiteException | IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(Throwable error) {
}
}));
mLastPlusTwoButton.setOnClickListener(view -> {
PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleSubscriber<Session>() {
@Override
public void onSuccess(Session session) {
try {
Solve solve = session.getLastSolve(getActivity()).toBlocking().first();
solve.setPenalty(getActivity(), Solve.PENALTY_PLUSTWO);
Session.notifyListeners(session.getId(), solve, RecyclerViewUpdate.SINGLE_CHANGE);
} catch (CouchbaseLiteException | IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(Throwable error) {
}
});
playLastBarExitAnimation();
});
mLastDeleteButton.setOnClickListener(v1 -> {
PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.subscribe(new SingleSubscriber<Session>() {
@Override
public void onSuccess(Session session) {
try {
session.deleteSolve(getActivity(), session.getLastSolve(getActivity()).toBlocking().first().getId());
} catch (CouchbaseLiteException | IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(Throwable error) {
}
});
playLastBarExitAnimation();
});
LinearLayoutManager timeBarLayoutManager = new LinearLayoutManager(getActivity(),
LinearLayoutManager.HORIZONTAL, false) {
//TODO: Smooth scroll so empty space for insertion opens up
//Take a look at onLayoutChildren so insertion animation is nice.
};
//timeBarLayoutManager.setStackFromEnd(true);
mTimeBarRecycler.setLayoutManager(timeBarLayoutManager);
mTimeBarRecycler.setHasFixedSize(true);
mTimeBarAdapter = new TimeBarRecyclerAdapter(getActivity(), savedInstanceState);
mTimeBarRecycler.setAdapter(mTimeBarAdapter);
mDynamicStatusBarFrame = (FrameLayout) v.findViewById(R.id.fragment_current_session_timer_dynamic_status_frame);
mDynamicStatusBarText = (TextView) v.findViewById(R.id.fragment_current_session_timer_dynamic_status_text);
mRetainedFragment = getActivityCallback().getTimerRetainedFragment();
mRetainedFragment.setTargetFragment(this, 0);
//When the root view is touched...
v.setOnTouchListener((v1, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
return onTimerTouchDown();
}
case MotionEvent.ACTION_UP: {
onTimerTouchUp();
return false;
}
default:
return false;
}
});
if (!mFromSavedInstanceState) {
//When the fragment is initializing, disable action bar and
// generate a scramble.
mRetainedFragment.resetScramblerThread();
enableMenuItems(false);
setScrambleText(getString(R.string.scrambling));
} else {
if (mInspecting) {
mUiHandler.post(inspectionRunnable);
}
if (mTiming) {
mUiHandler.post(timerRunnable);
}
if (mTiming || mInspecting) {
enableMenuItems(false);
} else if (!mRetainedFragment.isScrambling()) {
enableMenuItems(true);
}
if (mInspecting || mTiming || !mRetainedFragment.isScrambling()) {
// If timer is timing/inspecting, then update text/image to
// current. If timer is not timing/inspecting and not scrambling,
// then update scramble views to current.
setScrambleTextAndImageToCurrent();
} else {
setScrambleText(getString(R.string.scrambling));
}
}
//If the scramble image is currently displayed and it is not scrambling,
// then make sure it is set to visible; otherwise, set to gone.
showScrambleImage(mScrambleImageDisplay && !mRetainedFragment.isScrambling());
mScrambleImage.setOnClickListener(v1 -> toggleScrambleImage());
return v;
}
public void setInitialized() {
//TODO: Update bld mode when puzzle type changes
updateBld();
if (!mFromSavedInstanceState) {
mRetainedFragment.generateNextScramble();
mRetainedFragment.postSetScrambleViewsToCurrent();
}
}
@Override
public void updateBld() {
PuzzleType.getCurrent(getActivity())
.subscribe(puzzleType -> mBldMode = puzzleType.isBld());
}
@Override
public Activity getContextCompat() {
return getActivity();
}
@Override
public void onResume() {
super.onResume();
initSharedPrefs();
if (mKeepScreenOn) {
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
if (mMonospaceScrambleFontEnabled) {
mScrambleText.setTypeface(Typeface.MONOSPACE);
} else {
mScrambleText.setTypeface(Typeface.DEFAULT);
}
mScrambleText.setTextSize(mScrambleTextSize);
//TODO
//When Settings change
//onPuzzleTypeChanged();
}
@Override
protected PresenterFactory<CurrentSessionTimerPresenter> getPresenterFactory() {
return new CurrentSessionTimerPresenter.Factory();
}
@Override
protected void onPresenterPrepared(CurrentSessionTimerPresenter presenter) {
mTimeBarAdapter.onPresenterPrepared(presenter);
}
@Override
protected void onPresenterDestroyed() {
mTimeBarAdapter.onPresenterDestroyed();
}
@Override
public void onPause() {
super.onPause();
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
//PuzzleType.getCurrentId().saveCurrentSession(getActivity());
stopHoldTimer();
//presenter.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
mTimeBarAdapter.onSaveInstanceState(outState);
outState.putBoolean(STATE_IMAGE_DISPLAYED, mScrambleImageDisplay);
outState.putLong(STATE_START_TIME, mTimingStartTimestamp);
outState.putBoolean(STATE_RUNNING, mTiming);
outState.putBoolean(STATE_INSPECTING, mInspecting);
outState.putLong(STATE_INSPECTION_START_TIME, mInspectionStartTimestamp);
super.onSaveInstanceState(outState);
}
public void scrollRecyclerView(int position) {
mTimeBarRecycler.smoothScrollToPosition(position);
}
public TimeBarRecyclerAdapter getTimeBarAdapter() {
return (TimeBarRecyclerAdapter) mTimeBarRecycler.getAdapter();
}
@Override
public void onDestroy() {
super.onDestroy();
//When destroyed, stop timer runnable
mUiHandler.removeCallbacksAndMessages(null);
mRetainedFragment.setTargetFragment(null, 0);
}
void initSharedPrefs() {
mInspectionEnabled = PrefUtils.isInspectionEnabled(getActivity());
mHoldToStartEnabled = PrefUtils.isHoldToStartEnabled(getActivity());
mTwoRowTimeEnabled = getResources().getConfiguration().orientation == 1
&& PrefUtils.isTwoRowTimeEnabled(getActivity());
mUpdateTimePref = PrefUtils.getTimerUpdateMode(getActivity());
mMillisecondsEnabled = PrefUtils.isDisplayMillisecondsEnabled(getActivity());
mTimerTextSize = PrefUtils.getTimerTextSize(getActivity());
mScrambleTextSize = PrefUtils.getScrambleTextSize(getActivity());
mKeepScreenOn = PrefUtils.isKeepScreenOnEnabled(getActivity());
mSignEnabled = PrefUtils.isSignEnabled(getActivity());
mMonospaceScrambleFontEnabled = PrefUtils.isMonospaceScrambleFontEnabled(getActivity());
}
void setTimerTextToPrefSize() {
if (!mTimerText.getText().equals(getString(R.string.ready))) {
if (mTimerText != null && mTimerText2 != null) {
if (mTwoRowTimeEnabled) {
mTimerText.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTimerTextSize);
} else {
mTimerText.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTimerTextSize * 0.7F);
}
mTimerText2.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTimerTextSize / 2);
}
}
}
public void showScrambleImage(boolean enable) {
if (enable) {
mScrambleImage.setVisibility(View.VISIBLE);
} else {
mScrambleImage.setVisibility(View.GONE);
}
mScrambleImageDisplay = enable;
}
private void setTextColorPrimary() {
int[] textColorAttr = new int[]{android.R.attr.textColor};
TypedArray a = getActivity().obtainStyledAttributes(new TypedValue().data, textColorAttr);
int color = a.getColor(0, -1);
a.recycle();
setTextColor(color);
}
void setTextColor(int color) {
mTimerText.setTextColor(color);
mTimerText2.setTextColor(color);
}
/**
* Sets the timer text to last solve's time; if there are no solves,
* set to ready. Updates the timer text's size.
*/
void invalidateTimerText() {
PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.flatMapObservable(session -> session.getLastSolve(getActivity()))
.observeOn(AndroidSchedulers.mainThread())
.defaultIfEmpty(null)
.subscribe(new Subscriber<Solve>() {
@Override
public void onCompleted() {
}
@Override
public void onNext(Solve solve) {
if (solve != null) {
setTimerTextFromSolve(solve);
} else {
setTimerText(new String[]{getString(R.string.ready), ""});
mTimerText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 100);
}
}
@Override
public void onError(Throwable e) {
}
});
setTimerTextToPrefSize();
}
void setTimerTextFromSolve(Solve solve) {
setTimerText(solve.getTimeStringArray(mMillisecondsEnabled));
setTimerTextToPrefSize();
}
public void enableMenuItems(boolean enable) {
ActivityCallback callback = getActivityCallback();
callback.enableMenuItems(enable);
}
private ActivityCallback getActivityCallback() {
ActivityCallback callback;
try {
callback = (ActivityCallback) getActivity();
} catch (ClassCastException e) {
throw new ClassCastException(
getActivity().toString() + " must implement " +
"ActivityCallback");
}
return callback;
}
void toggleScrambleImage() {
if (mScrambleImageDisplay) {
mScrambleImageDisplay = false;
mScrambleImage.setVisibility(View.GONE);
} else {
if (!mRetainedFragment.isScrambling()) {
mScrambleImageDisplay = true;
mScrambleImage.setVisibility(View.VISIBLE);
mScrambleImage.setOnClickListener(v -> {
mScrambleImageDisplay = false;
mScrambleImage.setVisibility(View.GONE);
});
}
}
}
/**
* @return whether
* {@link CurrentSessionTimerFragment#onTimerTouchUp()}
* will be triggered when touch is released
*/
private synchronized boolean onTimerTouchDown() {
boolean scrambling = mRetainedFragment.isScrambling();
//Currently Timing: User stopping timer
if (mTiming) {
PuzzleType.getCurrent(getActivity())
.flatMap(puzzleType -> puzzleType.getCurrentSessionDeferred(getActivity()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(session -> {
try {
Solve s = null;
Session.SolveBuilder builder = session.newSolve(getActivity())
.setScramble(mRetainedFragment.getCurrentScrambleAndSvg().getScramble())
.setRawTime(System.nanoTime() - mTimingStartTimestamp);
if (mInspectionEnabled && mLateStartPenalty) {
builder.setPenalty(Solve.PENALTY_PLUSTWO);
}
s = builder.build();
setTimerTextFromSolve(s);
} catch (CouchbaseLiteException | IOException e) {
e.printStackTrace();
}
playLastBarEnterAnimation();
playEnterAnimations();
getActivityCallback().lockDrawerAndViewPager(false);
resetTimer();
if (scrambling) {
setScrambleText(getString(R.string.scrambling));
}
mRetainedFragment.postSetScrambleViewsToCurrent();
});
//TODO: Blind mode
/*if (!mBldMode) {
} else {
s = new BldSolve(mRetainedFragment.getCurrentScrambleAndSvg().getScramble(),
System.nanoTime() - mTimingStartTimestamp,
mInspectionStopTimestamp - mInspectionStartTimestamp);
}*/
//Add the solve to the current session with the
// current scramble/scramble image and time
//g.onTimingFinished(s);
return false;
}
if (mBldMode) {
return onTimerBldTouchDown(scrambling);
}
if (mHoldToStartEnabled &&
((!mInspectionEnabled && !scrambling) || mInspecting)) {
//If hold to start is on, start the hold timer
//If inspection is enabled, only start hold timer when inspecting
//Go to section 2
startHoldTimer();
return true;
} else if (mInspecting) {
//If inspecting and hold to start is off, start regular timer
//Go to section 3
setTextColor(mGreen500);
return true;
}
//If inspection is on and haven't started yet: section 1
//If hold to start and inspection are both off: section 3
if (!scrambling) {
setTextColor(mGreen500);
return true;
}
return false;
}
private synchronized boolean onTimerBldTouchDown(boolean scrambling) {
//If inspecting: section 3
//If not inspecting yet and not scrambling: section 1
setTextColor(mGreen500);
return mInspecting || !scrambling;
}
private synchronized void onTimerTouchUp() {
if ((mInspectionEnabled || mBldMode) && !mInspecting) {
//Section 1
//If inspection is on (or BLD) and not inspecting
startInspection();
playExitAnimations();
} else if (!mBldMode && mHoldToStartEnabled) {
//Section 2
//Hold to start is on (may be inspecting)
if (mHoldTiming &&
(System.nanoTime() - mHoldTimerStartTimestamp >=
HOLD_TIME)) {
//User held long enough for timer to turn
// green and lifted: start timing
stopInspection();
stopHoldTimer();
startTiming();
if (!mBldMode && !mInspectionEnabled) {
//If hold timer was started but not in
// inspection, generate next scramble
mRetainedFragment.generateNextScramble();
}
} else {
//User started hold timer but lifted before
// the timer is green: stop hold timer
stopHoldTimer();
}
} else {
//Section 3
//Hold to start is off, start timing
if (mInspecting) {
stopInspection();
} else {
playExitAnimations();
}
startTiming();
if (!mBldMode) {
mRetainedFragment.generateNextScramble();
}
}
}
private void playExitAnimations() {
Utils.lockOrientation(getActivity());
playScrambleExitAnimation();
playStatsExitAnimation();
getActivityCallback().playToolbarExitAnimation();
}
public void playEnterAnimations() {
Utils.unlockOrientation(getActivity());
playScrambleEnterAnimation();
playStatsEnterAnimation();
getActivityCallback().playToolbarEnterAnimation();
}
private void playDynamicStatusBarEnterAnimation() {
mDynamicStatusBarVisible = true;
ObjectAnimator enter = ObjectAnimator.ofFloat(mDynamicStatusBarFrame, View.TRANSLATION_Y, 0f);
enter.setDuration(125);
enter.setInterpolator(new LinearOutSlowInInterpolator());
AnimatorSet dynamicStatusBarAnimatorSet = new AnimatorSet();
dynamicStatusBarAnimatorSet.play(enter);
dynamicStatusBarAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (mDynamicStatusBarVisible) {
mTimeBarRecycler.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
dynamicStatusBarAnimatorSet.start();
}
private void playDynamicStatusBarExitAnimation() {
mDynamicStatusBarVisible = false;
ObjectAnimator exit = ObjectAnimator.ofFloat(mDynamicStatusBarFrame, View.TRANSLATION_Y,
mDynamicStatusBarFrame.getHeight());
exit.setDuration(125);
exit.setInterpolator(new FastOutLinearInInterpolator());
AnimatorSet dynamicStatusBarAnimatorSet = new AnimatorSet();
dynamicStatusBarAnimatorSet.play(exit);
dynamicStatusBarAnimatorSet.start();
mTimeBarRecycler.setVisibility(View.VISIBLE);
}
private void playLastBarEnterAnimation() {
if (mLastBarAnimationSet != null) {
mLastBarAnimationSet.cancel();
}
mLastDeleteButton.setEnabled(true);
mLastDnfButton.setEnabled(true);
mLastPlusTwoButton.setEnabled(true);
ObjectAnimator enter = ObjectAnimator.ofFloat(mLastBarLinearLayout, View.TRANSLATION_Y,
-mLastBarLinearLayout.getHeight());
ObjectAnimator exit = ObjectAnimator.ofFloat(mLastBarLinearLayout, View.TRANSLATION_Y, 0f);
enter.setDuration(125);
exit.setDuration(125);
exit.setStartDelay(2000);
enter.setInterpolator(new LinearOutSlowInInterpolator());
exit.setInterpolator(new FastOutLinearInInterpolator());
mLastBarAnimationSet = new AnimatorSet();
mLastBarAnimationSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mLastBarLinearLayout.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mLastBarLinearLayout.getTranslationY() == 0f) {
mLastBarLinearLayout.setVisibility(View.GONE);
}
}
});
mLastBarAnimationSet.playSequentially(enter, exit);
mLastBarAnimationSet.start();
}
void playLastBarExitAnimation() {
if (mLastBarAnimationSet != null) {
mLastBarAnimationSet.cancel();
}
mLastDeleteButton.setEnabled(false);
mLastDnfButton.setEnabled(false);
mLastPlusTwoButton.setEnabled(false);
ObjectAnimator exit = ObjectAnimator.ofFloat(mLastBarLinearLayout, View.TRANSLATION_Y, 0f);
exit.setDuration(125);
exit.setInterpolator(new FastOutLinearInInterpolator());
mLastBarAnimationSet = new AnimatorSet();
mLastBarAnimationSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mLastBarLinearLayout.getTranslationY() == 0f) {
mLastBarLinearLayout.setVisibility(View.GONE);
}
}
});
mLastBarAnimationSet.play(exit);
mLastBarAnimationSet.start();
}
void playScrambleExitAnimation() {
if (mScrambleAnimator != null) {
mScrambleAnimator.cancel();
}
mScrambleAnimator = ObjectAnimator.ofFloat(mScrambleText, View.TRANSLATION_Y,
-mScrambleText.getHeight());
mScrambleAnimator.setDuration(300);
mScrambleAnimator.setInterpolator(new FastOutSlowInInterpolator());
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
mScrambleAnimator.addUpdateListener(animation -> mScrambleTextShadow.setTranslationY((int) (float) animation.getAnimatedValue()));
}
mScrambleAnimator.start();
invalidateScrambleShadow(true);
}
void playScrambleEnterAnimation() {
if (mScrambleAnimator != null) {
mScrambleAnimator.cancel();
}
mScrambleAnimator = ObjectAnimator.ofFloat(mScrambleText, View.TRANSLATION_Y, 0f);
mScrambleAnimator.setDuration(300);
mScrambleAnimator.setInterpolator(new FastOutSlowInInterpolator());
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
mScrambleAnimator.addUpdateListener(animation -> mScrambleTextShadow.setTranslationY((int) (float) animation.getAnimatedValue()));
}
mScrambleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
invalidateScrambleShadow(false);
}
});
mScrambleAnimator.start();
}
void playStatsExitAnimation() {
ObjectAnimator exit = ObjectAnimator.ofFloat(mStatsText, View.ALPHA, 0f);
exit.setDuration(300);
exit.setInterpolator(new FastOutLinearInInterpolator());
ObjectAnimator exit3 = ObjectAnimator.ofFloat(mStatsSolvesText, View.ALPHA, 0f);
exit3.setDuration(300);
AnimatorSet scrambleAnimatorSet = new AnimatorSet();
scrambleAnimatorSet.play(exit).with(exit3);
scrambleAnimatorSet.start();
}
void playStatsEnterAnimation() {
ObjectAnimator enter = ObjectAnimator.ofFloat(mStatsText, View.ALPHA, 1f);
enter.setDuration(300);
enter.setInterpolator(new LinearOutSlowInInterpolator());
ObjectAnimator enter3 = ObjectAnimator.ofFloat(mStatsSolvesText, View.ALPHA, 1f);
enter3.setDuration(300);
AnimatorSet scrambleAnimatorSet = new AnimatorSet();
scrambleAnimatorSet.play(enter).with(enter3);
scrambleAnimatorSet.start();
}
void startHoldTimer() {
playLastBarExitAnimation();
mHoldTiming = true;
mHoldTimerStartTimestamp = System.nanoTime();
setTextColor(mRed500);
mUiHandler.postDelayed(holdTimerRunnable, 550);
}
public void stopHoldTimer() {
mHoldTiming = false;
mHoldTimerStartTimestamp = 0;
mUiHandler.removeCallbacks(holdTimerRunnable);
setTextColorPrimary();
}
/**
* Start inspection; Start Generating Next Scramble
*/
void startInspection() {
playLastBarExitAnimation();
playDynamicStatusBarEnterAnimation();
mDynamicStatusBarText.setText(R.string.inspecting);
mInspectionStartTimestamp = System.nanoTime();
mInspecting = true;
if (mBldMode) {
mUiHandler.post(timerRunnable);
} else {
mUiHandler.post(inspectionRunnable);
}
mRetainedFragment.generateNextScramble();
enableMenuItems(false);
showScrambleImage(false);
getActivityCallback().lockDrawerAndViewPager(true);
setTextColorPrimary();
}
void stopInspection() {
mInspectionStopTimestamp = System.nanoTime();
mInspecting = false;
mUiHandler.removeCallbacks(inspectionRunnable);
}
/**
* Start timing; does not start generating next scramble
*/
void startTiming() {
playLastBarExitAnimation();
playDynamicStatusBarEnterAnimation();
mTimingStartTimestamp = System.nanoTime();
mInspecting = false;
mTiming = true;
if (!mBldMode) mUiHandler.post(timerRunnable);
enableMenuItems(false);
showScrambleImage(false);
mDynamicStatusBarText.setText(R.string.timing);
getActivityCallback().lockDrawerAndViewPager(true);
setTextColorPrimary();
}
public void resetGenerateScramble() {
mRetainedFragment.resetScramblerThread();
mRetainedFragment.generateNextScramble();
mRetainedFragment.postSetScrambleViewsToCurrent();
}
public void resetTimer() {
mUiHandler.removeCallbacksAndMessages(null);
mHoldTiming = false;
mTiming = false;
mLateStartPenalty = false;
mHoldTimerStartTimestamp = 0;
mInspectionStartTimestamp = 0;
mTimingStartTimestamp = 0;
mInspecting = false;
setTextColorPrimary();
playDynamicStatusBarExitAnimation();
}
public Handler getUiHandler() {
return mUiHandler;
}
public interface ActivityCallback {
void lockDrawerAndViewPager(boolean lock);
void playToolbarEnterAnimation();
void playToolbarExitAnimation();
CurrentSessionTimerRetainedFragment getTimerRetainedFragment();
void enableMenuItems(boolean enable);
FrameLayout getContentFrameLayout();
}
}