| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.deskclock.timer; |
| |
| import static android.view.View.ALPHA; |
| import static android.view.View.GONE; |
| import static android.view.View.INVISIBLE; |
| import static android.view.View.TRANSLATION_Y; |
| import static android.view.View.VISIBLE; |
| import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.widget.ImageView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.recyclerview.widget.GridLayoutManager; |
| import androidx.recyclerview.widget.LinearLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.deskclock.AnimatorUtils; |
| import com.android.deskclock.DeskClock; |
| import com.android.deskclock.DeskClockFragment; |
| import com.android.deskclock.R; |
| import com.android.deskclock.Utils; |
| import com.android.deskclock.data.DataModel; |
| import com.android.deskclock.data.Timer; |
| import com.android.deskclock.data.TimerListener; |
| import com.android.deskclock.events.Events; |
| import com.android.deskclock.uidata.UiDataModel; |
| |
| import java.io.Serializable; |
| |
| /** |
| * Displays a vertical list of timers in all states. |
| */ |
| public final class TimerFragment extends DeskClockFragment { |
| |
| private static final String EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"; |
| |
| private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input"; |
| |
| /** Scheduled to update the timers while at least one is running. */ |
| private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable(); |
| |
| /** Updates the FABs in response to timers being added or removed. */ |
| private final TimerListener mTimerWatcher = new TimerWatcher(); |
| |
| private TimerSetupView mCreateTimerView; |
| private TimerAdapter mAdapter; |
| private View mTimersView; |
| private View mCurrentView; |
| private RecyclerView mRecyclerView; |
| private TimerClickHandler mTimerClickHandler; |
| |
| private Serializable mTimerSetupState; |
| |
| /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */ |
| private boolean mCreatingTimer; |
| |
| /** |
| * @return an Intent that selects the timers tab with the setup screen for a new timer in place. |
| */ |
| public static Intent createTimerSetupIntent(Context context) { |
| return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true); |
| } |
| |
| /** The public no-arg constructor required by all fragments. */ |
| public TimerFragment() { |
| super(TIMERS); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| final View view = inflater.inflate(R.layout.timer_fragment, container, false); |
| |
| mTimerClickHandler = new TimerClickHandler(this); |
| mAdapter = new TimerAdapter(mTimerClickHandler); |
| mRecyclerView = view.findViewById(R.id.recycler_view); |
| mRecyclerView.setAdapter(mAdapter); |
| mRecyclerView.setLayoutManager(getLayoutManager(view.getContext())); |
| |
| mTimersView = view.findViewById(R.id.timer_view); |
| mCreateTimerView = view.findViewById(R.id.timer_setup); |
| mCreateTimerView.setFabContainer(this); |
| |
| DataModel.getDataModel().addTimerListener(mAdapter); |
| DataModel.getDataModel().addTimerListener(mTimerWatcher); |
| |
| // If timer setup state is present, retrieve it to be later honored. |
| if (savedInstanceState != null) { |
| mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE); |
| } |
| |
| return view; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| |
| boolean createTimer = false; |
| int showTimerId = -1; |
| |
| // Examine the intent of the parent activity to determine which view to display. |
| final Intent intent = getActivity() != null ? getActivity().getIntent() : null; |
| if (intent != null) { |
| // These extras are single-use; remove them after honoring them. |
| createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false); |
| intent.removeExtra(EXTRA_TIMER_SETUP); |
| |
| showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1); |
| intent.removeExtra(TimerService.EXTRA_TIMER_ID); |
| } |
| |
| // Choose the view to display in this fragment. |
| if (showTimerId != -1) { |
| // A specific timer must be shown; show the list of timers. |
| showTimersView(FAB_AND_BUTTONS_IMMEDIATE); |
| } else if (!hasTimers() || createTimer || mTimerSetupState != null) { |
| // No timers exist, a timer is being created, or the last view was timer setup; |
| // show the timer setup view. |
| showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE); |
| |
| if (mTimerSetupState != null) { |
| mCreateTimerView.setState(mTimerSetupState); |
| mTimerSetupState = null; |
| } |
| } else { |
| // Otherwise, default to showing the list of timers. |
| showTimersView(FAB_AND_BUTTONS_IMMEDIATE); |
| } |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| |
| // Stop updating the timers when this fragment is no longer visible. |
| stopUpdatingTime(); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| super.onDestroyView(); |
| |
| DataModel.getDataModel().removeTimerListener(mAdapter); |
| DataModel.getDataModel().removeTimerListener(mTimerWatcher); |
| } |
| |
| @Override |
| public void onSaveInstanceState(@NonNull Bundle outState) { |
| super.onSaveInstanceState(outState); |
| |
| // If the timer creation view is visible, store the input for later restoration. |
| if (mCurrentView == mCreateTimerView) { |
| mTimerSetupState = mCreateTimerView.getState(); |
| outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState); |
| } |
| } |
| |
| @Override |
| public int getFabTargetVisibility() { |
| final Timer timer = getTimer(); |
| if (mCurrentView == mTimersView) { |
| return VISIBLE; |
| } else if (mCurrentView == mCreateTimerView) { |
| if (mCreateTimerView.hasValidInput() || timer != null) { |
| return VISIBLE; |
| } else { |
| return INVISIBLE; |
| } |
| } |
| |
| return INVISIBLE; |
| } |
| |
| private void updateFab(@NonNull ImageView fab) { |
| if (mCurrentView == mTimersView) { |
| fab.setImageResource(R.drawable.ic_add_24dp); |
| fab.setContentDescription(fab.getResources().getString(R.string.timer_add_timer)); |
| fab.setVisibility(VISIBLE); |
| } else if (mCurrentView == mCreateTimerView) { |
| if (mCreateTimerView.hasValidInput()) { |
| fab.setImageResource(R.drawable.ic_pause_play); |
| fab.setContentDescription(fab.getResources().getString(R.string.timer_start)); |
| fab.setVisibility(VISIBLE); |
| } else { |
| fab.setContentDescription(null); |
| fab.setVisibility(INVISIBLE); |
| } |
| } |
| } |
| |
| @Override |
| public void onUpdateFab(@NonNull ImageView fab) { |
| updateFab(fab); |
| } |
| |
| @Override |
| public void onMorphFab(@NonNull ImageView fab) { |
| // Update the fab's drawable to match the current timer state. |
| updateFab(fab); |
| // Animate the drawable. |
| AnimatorUtils.startDrawableAnimation(fab); |
| } |
| |
| @Override |
| public void onUpdateFabButtons(@NonNull ImageView left, @NonNull ImageView right) { |
| final Context context = left.getContext(); |
| final Drawable icDelete = Utils.getVectorDrawable(context, R.drawable.ic_delete); |
| if (mCurrentView == mTimersView) { |
| left.setVisibility(INVISIBLE); |
| right.setVisibility(INVISIBLE); |
| } else if (mCurrentView == mCreateTimerView) { |
| left.setClickable(true); |
| left.setImageDrawable(icDelete); |
| left.setContentDescription(left.getResources().getString(R.string.timer_cancel)); |
| // If no timers yet exist, the user is forced to create the first one. |
| left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE); |
| |
| right.setVisibility(INVISIBLE); |
| } |
| } |
| |
| @Override |
| public void onFabClick(@NonNull ImageView fab) { |
| if (mCurrentView == mTimersView) { |
| animateToView(mCreateTimerView, null, true); |
| } else if (mCurrentView == mCreateTimerView) { |
| mCreatingTimer = true; |
| try { |
| // Create the new timer. |
| final long timerLength = mCreateTimerView.getTimeInMillis(); |
| final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false); |
| Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock); |
| |
| // Start the new timer. |
| DataModel.getDataModel().startTimer(timer); |
| Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock); |
| } finally { |
| mCreatingTimer = false; |
| } |
| |
| // Return to the list of timers. |
| animateToView(mTimersView, null, true); |
| } |
| } |
| |
| @Override |
| public void onLeftButtonClick(@NonNull ImageView left) { |
| if (mCurrentView == mTimersView) { |
| // Clicking the "delete" button. |
| final Timer timer = getTimer(); |
| if (timer == null) { |
| return; |
| } |
| |
| DataModel.getDataModel().removeTimer(timer); |
| Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock); |
| |
| if (mAdapter.getItemCount() <= 1) { |
| animateToView(mCreateTimerView, timer, false); |
| } |
| |
| left.announceForAccessibility(getActivity().getString(R.string.timer_deleted)); |
| } else if (mCurrentView == mCreateTimerView) { |
| // Clicking the "cancel" button on the timer creation page returns to the timers list. |
| mCreateTimerView.reset(); |
| |
| animateToView(mTimersView, null, false); |
| |
| left.announceForAccessibility(getActivity().getString(R.string.timer_canceled)); |
| } |
| } |
| |
| @Override |
| public void onRightButtonClick(@NonNull ImageView right) { |
| if (mCurrentView != mCreateTimerView) { |
| animateToView(mCreateTimerView, null, true); |
| } |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (mCurrentView == mCreateTimerView) { |
| return mCreateTimerView.onKeyDown(keyCode, event); |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| /** |
| * Display the view that creates a new timer. |
| */ |
| private void showCreateTimerView(int updateTypes) { |
| // Stop animating the timers. |
| stopUpdatingTime(); |
| |
| // Show the creation view; hide the timer view. |
| mTimersView.setVisibility(GONE); |
| mCreateTimerView.setVisibility(VISIBLE); |
| |
| // Record the fact that the create view is visible. |
| mCurrentView = mCreateTimerView; |
| |
| // Update the fab and buttons. |
| updateFab(updateTypes); |
| } |
| |
| /** |
| * Display the view that lists all existing timers. |
| */ |
| private void showTimersView(int updateTypes) { |
| // Clear any defunct timer creation state; the next timer creation starts fresh. |
| mTimerSetupState = null; |
| |
| // Show the timer view; hide the creation view. |
| mTimersView.setVisibility(VISIBLE); |
| mCreateTimerView.setVisibility(GONE); |
| |
| // Record the fact that the create view is visible. |
| mCurrentView = mTimersView; |
| |
| // Update the fab and buttons. |
| updateFab(updateTypes); |
| |
| // Start animating the timers. |
| startUpdatingTime(); |
| } |
| |
| /** |
| * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView} |
| * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer |
| * should be removed |
| * @param animateDown {@code true} if the views should animate upwards, otherwise downwards |
| */ |
| private void animateToView(final View toView, final Timer timerToRemove, |
| final boolean animateDown) { |
| if (mCurrentView == toView) { |
| return; |
| } |
| |
| final boolean toTimers = toView == mTimersView; |
| if (toTimers) { |
| mTimersView.setVisibility(VISIBLE); |
| } else { |
| mCreateTimerView.setVisibility(VISIBLE); |
| } |
| // Avoid double-taps by enabling/disabling the set of buttons active on the new view. |
| updateFab(BUTTONS_DISABLE); |
| |
| final long animationDuration = UiDataModel.getUiDataModel().getMediumAnimationDuration(); |
| |
| final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver(); |
| viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| if (viewTreeObserver.isAlive()) { |
| viewTreeObserver.removeOnPreDrawListener(this); |
| } |
| |
| final View view = mTimersView.findViewById(R.id.timer_time); |
| final float distanceY = view != null ? view.getHeight() + view.getY() : 0; |
| final float translationDistance = animateDown ? distanceY : -distanceY; |
| |
| toView.setTranslationY(-translationDistance); |
| mCurrentView.setTranslationY(0f); |
| toView.setAlpha(0f); |
| mCurrentView.setAlpha(1f); |
| |
| final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView, |
| TRANSLATION_Y, translationDistance); |
| final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f); |
| final AnimatorSet translationAnimatorSet = new AnimatorSet(); |
| translationAnimatorSet.playTogether(translateCurrent, translateNew); |
| translationAnimatorSet.setDuration(animationDuration); |
| translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN); |
| |
| final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f); |
| fadeOutAnimator.setDuration(animationDuration / 2); |
| fadeOutAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| super.onAnimationStart(animation); |
| |
| // The fade-out animation and fab-shrinking animation should run together. |
| updateFab(FAB_AND_BUTTONS_SHRINK); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| if (toTimers) { |
| showTimersView(FAB_AND_BUTTONS_EXPAND); |
| |
| // Reset the state of the create view. |
| mCreateTimerView.reset(); |
| } else { |
| showCreateTimerView(FAB_AND_BUTTONS_EXPAND); |
| } |
| |
| if (timerToRemove != null) { |
| DataModel.getDataModel().removeTimer(timerToRemove); |
| Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock); |
| } |
| |
| // Update the fab and button states now that the correct view is visible and |
| // before the animation to expand the fab and buttons starts. |
| updateFab(FAB_AND_BUTTONS_IMMEDIATE); |
| } |
| }); |
| |
| final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f); |
| fadeInAnimator.setDuration(animationDuration / 2); |
| fadeInAnimator.setStartDelay(animationDuration / 2); |
| |
| final AnimatorSet animatorSet = new AnimatorSet(); |
| animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet); |
| animatorSet.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| mTimersView.setTranslationY(0f); |
| mCreateTimerView.setTranslationY(0f); |
| mTimersView.setAlpha(1f); |
| mCreateTimerView.setAlpha(1f); |
| } |
| }); |
| animatorSet.start(); |
| |
| return true; |
| } |
| }); |
| } |
| |
| private boolean hasTimers() { |
| return mAdapter.getItemCount() > 0; |
| } |
| |
| private Timer getTimer() { |
| if (mAdapter == null) { |
| TimerAdapter adapter = new TimerAdapter(mTimerClickHandler); |
| return adapter.getItemCount() == 0 ? null : adapter.getTimer(0); |
| } |
| |
| if (mRecyclerView == null) { |
| return null; |
| } |
| |
| return mAdapter.getItemCount() == 0 ? null : mAdapter.getTimer(0); |
| } |
| |
| private void startUpdatingTime() { |
| // Ensure only one copy of the runnable is ever scheduled by first stopping updates. |
| stopUpdatingTime(); |
| mRecyclerView.post(mTimeUpdateRunnable); |
| } |
| |
| private void stopUpdatingTime() { |
| mRecyclerView.removeCallbacks(mTimeUpdateRunnable); |
| } |
| |
| private RecyclerView.LayoutManager getLayoutManager(Context context) { |
| Resources res = context.getResources(); |
| boolean isTablet = res.getBoolean(R.bool.rotateAlarmAlert); |
| if (isTablet) { |
| int columnCount = res.getInteger(R.integer.timers_column_count); |
| return new GridLayoutManager(context, columnCount); |
| } |
| |
| int orientation = res.getConfiguration().orientation; |
| boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE; |
| return new LinearLayoutManager(context, isLandscape |
| ? LinearLayoutManager.HORIZONTAL : LinearLayoutManager.VERTICAL, false); |
| } |
| |
| /** |
| * Periodically refreshes the state of each timer. |
| */ |
| private class TimeUpdateRunnable implements Runnable { |
| @Override |
| public void run() { |
| final long startTime = SystemClock.elapsedRealtime(); |
| // If no timers require continuous updates, avoid scheduling the next update. |
| if (!mAdapter.updateTime()) { |
| return; |
| } |
| final long endTime = SystemClock.elapsedRealtime(); |
| |
| // Try to maintain a consistent period of time between redraws. |
| final long delay = Math.max(0, startTime + 20 - endTime); |
| mTimersView.postDelayed(this, delay); |
| } |
| } |
| |
| /** |
| * Update the fab in response to the visible timer changing. |
| */ |
| private class TimerWatcher implements TimerListener { |
| @Override |
| public void timerAdded(Timer timer) { |
| // If the timer is being created via this fragment avoid adjusting the fab. |
| // Timer setup view is about to be animated away in response to this timer creation. |
| // Changes to the fab immediately preceding that animation are jarring. |
| if (!mCreatingTimer) { |
| updateFab(FAB_AND_BUTTONS_IMMEDIATE); |
| } |
| } |
| |
| @Override |
| public void timerUpdated(Timer before, Timer after) { |
| // If the timer started, animate the timers. |
| if (before.isReset() && !after.isReset()) { |
| startUpdatingTime(); |
| } |
| } |
| |
| @Override |
| public void timerRemoved(Timer timer) { |
| updateFab(FAB_AND_BUTTONS_IMMEDIATE); |
| |
| if (mCurrentView == mTimersView && mAdapter.getItemCount() == 0) { |
| animateToView(mCreateTimerView, null, false); |
| } |
| } |
| } |
| } |