Java tutorial
/* * Copyright (C) 2015 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.wizardsofm.deskclock.stopwatch; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.os.SystemClock; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.v4.graphics.ColorUtils; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SimpleItemAnimator; import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageButton; import android.widget.ImageView; import com.wizardsofm.deskclock.DeskClockFragment; import com.wizardsofm.deskclock.LogUtils; import com.wizardsofm.deskclock.Utils; import com.wizardsofm.deskclock.data.DataModel; import com.wizardsofm.deskclock.data.Lap; import com.wizardsofm.deskclock.data.Stopwatch; import com.wizardsofm.deskclock.data.StopwatchListener; import com.wizardsofm.deskclock.events.Events; import com.wizardsofm.deskclock.timer.CountingTimerView; import com.wizardsofm.deskclock.uidata.TabListener; import com.wizardsofm.deskclock.uidata.UiDataModel; import static android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM; import static android.view.View.GONE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.wizardsofm.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE; import static com.wizardsofm.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_MORPH; /** * Fragment that shows the stopwatch and recorded laps. */ public final class StopwatchFragment extends DeskClockFragment { /** Milliseconds between redraws. */ private static final int REDRAW_PERIOD = 25; /** Keep the screen on when this tab is selected. */ private final TabListener mTabWatcher = new TabWatcher(); /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */ private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable(); /** Updates the user interface in response to stopwatch changes. */ private final StopwatchListener mStopwatchWatcher = new StopwatchWatcher(); /** Draws a gradient over the bottom of the {@link #mLapsList} to reduce clash with the fab. */ private GradientItemDecoration mGradientItemDecoration; /** The data source for {@link #mLapsList}. */ private LapsAdapter mLapsAdapter; /** The layout manager for the {@link #mLapsAdapter}. */ private LinearLayoutManager mLapsLayoutManager; /** Draws the reference lap while the stopwatch is running. */ private StopwatchCircleView mTime; /** Displays the recorded lap times. */ private RecyclerView mLapsList; /** Displays the current stopwatch time. */ private CountingTimerView mTimeText; /** Number of laps the stopwatch is tracking. */ private int mLapCount; /** The public no-arg constructor required by all fragments. */ public StopwatchFragment() { super(UiDataModel.Tab.STOPWATCH); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { mLapsAdapter = new LapsAdapter(getActivity()); mLapsLayoutManager = new LinearLayoutManager(getActivity()); mGradientItemDecoration = new GradientItemDecoration(getActivity()); final View v = inflater.inflate(com.wizardsofm.deskclock.R.layout.stopwatch_fragment, container, false); mTime = (StopwatchCircleView) v.findViewById(com.wizardsofm.deskclock.R.id.stopwatch_time); mLapsList = (RecyclerView) v.findViewById(com.wizardsofm.deskclock.R.id.laps_list); ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false); mLapsList.setLayoutManager(mLapsLayoutManager); mLapsList.addItemDecoration(mGradientItemDecoration); // In landscape layouts, the laps list can reach the top of the screen and thus can cause // a drop shadow to appear. The same is not true for portrait landscapes. if (Utils.isLandscape(getActivity())) { final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher(); mLapsList.addOnLayoutChangeListener(scrollPositionWatcher); mLapsList.addOnScrollListener(scrollPositionWatcher); } mLapsList.setAdapter(mLapsAdapter); // Timer text serves as a virtual start/stop button. mTimeText = (CountingTimerView) v.findViewById(com.wizardsofm.deskclock.R.id.stopwatch_time_text); mTimeText.setShowBoundingCircle(mTime != null); mTimeText.setVirtualButtonEnabled(true); mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable()); DataModel.getDataModel().addStopwatchListener(mStopwatchWatcher); return v; } @Override public void onStart() { super.onStart(); // Conservatively assume the data in the adapter has changed while the fragment was paused. mLapCount = mLapsAdapter.getItemCount(); mLapsAdapter.notifyDataSetChanged(); // Synchronize the user interface with the data model. updateUI(FAB_AND_BUTTONS_IMMEDIATE); // Start watching for page changes away from this fragment. UiDataModel.getUiDataModel().addTabListener(mTabWatcher); } @Override public void onStop() { super.onStop(); // Stop all updates while the fragment is not visible. stopUpdatingTime(); mTimeText.blinkTimeStr(false); // Stop watching for page changes away from this fragment. UiDataModel.getUiDataModel().removeTabListener(mTabWatcher); // Release the wake lock if it is currently held. releaseWakeLock(); } @Override public void onDestroyView() { super.onDestroyView(); DataModel.getDataModel().removeStopwatchListener(mStopwatchWatcher); } @Override public void onFabClick(@NonNull ImageView fab) { toggleStopwatchState(); } @Override public void onLeftButtonClick(@NonNull ImageButton left) { switch (getStopwatch().getState()) { case RUNNING: doAddLap(); break; case PAUSED: doReset(); break; } } @Override public void onRightButtonClick(@NonNull ImageButton right) { doShare(); } @Override public void onUpdateFab(@NonNull ImageView fab) { if (getStopwatch().isRunning()) { fab.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_pause_white_24dp); fab.setContentDescription( fab.getResources().getString(com.wizardsofm.deskclock.R.string.sw_pause_button)); } else { fab.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_start_white_24dp); fab.setContentDescription( fab.getResources().getString(com.wizardsofm.deskclock.R.string.sw_start_button)); } fab.setVisibility(VISIBLE); } @Override public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) { right.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_share); right.setContentDescription( right.getResources().getString(com.wizardsofm.deskclock.R.string.sw_share_button)); switch (getStopwatch().getState()) { case RESET: left.setEnabled(false); left.setVisibility(INVISIBLE); right.setVisibility(INVISIBLE); break; case RUNNING: final boolean canRecordLaps = canRecordMoreLaps(); left.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_lap); left.setContentDescription( left.getResources().getString(com.wizardsofm.deskclock.R.string.sw_lap_button)); left.setEnabled(canRecordLaps); left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE); right.setVisibility(INVISIBLE); break; case PAUSED: left.setEnabled(true); left.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_reset); left.setContentDescription( left.getResources().getString(com.wizardsofm.deskclock.R.string.sw_reset_button)); left.setVisibility(VISIBLE); right.setVisibility(VISIBLE); break; } } @Override public void onMorphFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) { right.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_share); right.setContentDescription( right.getResources().getString(com.wizardsofm.deskclock.R.string.sw_share_button)); switch (getStopwatch().getState()) { case RESET: left.setEnabled(false); left.setVisibility(INVISIBLE); right.setVisibility(INVISIBLE); break; case RUNNING: { final boolean canRecordLaps = canRecordMoreLaps(); updateLapIcon(left); left.setContentDescription( left.getResources().getString(com.wizardsofm.deskclock.R.string.sw_lap_button)); left.setEnabled(canRecordLaps); left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE); right.setVisibility(INVISIBLE); final Drawable icon = left.getDrawable(); if (icon instanceof Animatable) { ((Animatable) icon).start(); } break; } case PAUSED: { left.setEnabled(true); updateResetIcon(left); left.setContentDescription( left.getResources().getString(com.wizardsofm.deskclock.R.string.sw_reset_button)); left.setVisibility(VISIBLE); right.setVisibility(VISIBLE); final Drawable icon = left.getDrawable(); if (icon instanceof Animatable) { ((Animatable) icon).start(); } break; } } } /** * @param color the newly installed app window color */ protected void onAppColorChanged(@ColorInt int color) { if (mGradientItemDecoration != null) { mGradientItemDecoration.updateGradientColors(color); } if (mLapsList != null) { mLapsList.invalidateItemDecorations(); } } private void updateLapIcon(ImageButton button) { if (Utils.isLMR1OrLater() && button.getVisibility() == VISIBLE) { final int newLapCount = mLapsAdapter.getItemCount(); if (newLapCount == mLapCount) { button.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_reset_lap_animation); } else { button.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_lap_animation); } } else { button.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_lap); } } private void updateResetIcon(ImageButton button) { if (Utils.isLMR1OrLater()) { button.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_lap_reset_animation); } else { button.setImageResource(com.wizardsofm.deskclock.R.drawable.ic_reset); } } /** * Start the stopwatch. */ private void doStart() { Events.sendStopwatchEvent(com.wizardsofm.deskclock.R.string.action_start, com.wizardsofm.deskclock.R.string.label_deskclock); DataModel.getDataModel().startStopwatch(); } /** * Pause the stopwatch. */ private void doPause() { Events.sendStopwatchEvent(com.wizardsofm.deskclock.R.string.action_pause, com.wizardsofm.deskclock.R.string.label_deskclock); DataModel.getDataModel().pauseStopwatch(); } /** * Reset the stopwatch. */ private void doReset() { Events.sendStopwatchEvent(com.wizardsofm.deskclock.R.string.action_reset, com.wizardsofm.deskclock.R.string.label_deskclock); DataModel.getDataModel().resetStopwatch(); } /** * Send stopwatch time and lap times to an external sharing application. */ private void doShare() { final String[] subjects = getResources().getStringArray(com.wizardsofm.deskclock.R.array.sw_share_strings); final String subject = subjects[(int) (Math.random() * subjects.length)]; final String text = mLapsAdapter.getShareText(); @SuppressLint("InlinedApi") @SuppressWarnings("deprecation") final Intent shareIntent = new Intent(Intent.ACTION_SEND) .addFlags(Utils.isLOrLater() ? Intent.FLAG_ACTIVITY_NEW_DOCUMENT : Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) .putExtra(Intent.EXTRA_SUBJECT, subject).putExtra(Intent.EXTRA_TEXT, text).setType("text/plain"); final Context context = getActivity(); final String title = context.getString(com.wizardsofm.deskclock.R.string.sw_share_button); final Intent shareChooserIntent = Intent.createChooser(shareIntent, title); try { context.startActivity(shareChooserIntent); } catch (ActivityNotFoundException anfe) { LogUtils.e("No compatible receiver is found"); } } /** * Record and add a new lap ending now. */ private void doAddLap() { Events.sendStopwatchEvent(com.wizardsofm.deskclock.R.string.action_lap, com.wizardsofm.deskclock.R.string.label_deskclock); // Record a new lap. final Lap lap = mLapsAdapter.addLap(); if (lap == null) { return; } // Update button states. updateFab(FAB_AND_BUTTONS_MORPH); if (lap.getLapNumber() == 1) { // Child views from prior lap sets hang around and blit to the screen when adding the // first lap of the subsequent lap set. Remove those superfluous children here manually // to ensure they aren't seen as the first lap is drawn. mLapsList.removeAllViewsInLayout(); if (mTime != null) { // Start animating the reference lap. mTime.update(); } // Recording the first lap transitions the UI to display the laps list. showOrHideLaps(false); } // Ensure the newly added lap is visible on screen. mLapsList.scrollToPosition(0); mLapCount = mLapsAdapter.getItemCount(); } /** * Show or hide the list of laps. */ private void showOrHideLaps(boolean clearLaps) { final ViewGroup sceneRoot = (ViewGroup) getView(); if (sceneRoot == null) { return; } TransitionManager.beginDelayedTransition(sceneRoot); if (clearLaps) { mLapsAdapter.clearLaps(); } final boolean lapsVisible = mLapsAdapter.getItemCount() > 0; mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE); if (Utils.isPortrait(getActivity())) { // When the lap list is visible, it includes the bottom padding. When it is absent the // appropriate bottom padding must be applied to the container. final Resources res = getResources(); final int bottom = lapsVisible ? 0 : res.getDimensionPixelSize(com.wizardsofm.deskclock.R.dimen.fab_height); final int top = sceneRoot.getPaddingTop(); final int left = sceneRoot.getPaddingLeft(); final int right = sceneRoot.getPaddingRight(); sceneRoot.setPadding(left, top, right, bottom); } } private void adjustWakeLock() { final boolean appInForeground = DataModel.getDataModel().isApplicationInForeground(); if (getStopwatch().isRunning() && isTabSelected() && appInForeground) { getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { releaseWakeLock(); } } private void releaseWakeLock() { getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } /** * Either pause or start the stopwatch based on its current state. */ private void toggleStopwatchState() { if (getStopwatch().isRunning()) { doPause(); } else { doStart(); } } private Stopwatch getStopwatch() { return DataModel.getDataModel().getStopwatch(); } private boolean canRecordMoreLaps() { return DataModel.getDataModel().canAddMoreLaps(); } /** * Post the first runnable to update times within the UI. It will reschedule itself as needed. */ private void startUpdatingTime() { // Ensure only one copy of the runnable is ever scheduled by first stopping updates. stopUpdatingTime(); mTimeText.post(mTimeUpdateRunnable); } /** * Remove the runnable that updates times within the UI. */ private void stopUpdatingTime() { mTimeText.removeCallbacks(mTimeUpdateRunnable); } /** * Update all time displays based on a single snapshot of the stopwatch progress. This includes * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in * the list of laps. */ private void updateTime() { // Compute the total time of the stopwatch. final Stopwatch stopwatch = getStopwatch(); final long totalTime = stopwatch.getTotalTime(); // Update the total time display. mTimeText.setTime(totalTime, true); // Update the current lap. final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0; if (!stopwatch.isReset() && currentLapIsVisible) { mLapsAdapter.updateCurrentLap(mLapsList, totalTime); } } /** * Synchronize the UI state with the model data. */ private void updateUI(UpdateType updateType) { adjustWakeLock(); // Draw the latest stopwatch and current lap times. updateTime(); if (mTime != null) { mTime.update(); } // Start updates if the stopwatch is running. final Stopwatch stopwatch = getStopwatch(); if (stopwatch.isRunning()) { startUpdatingTime(); } // Blink text iff the stopwatch is paused. mTimeText.blinkTimeStr(stopwatch.isPaused()); // Adjust the visibility of the list of laps. showOrHideLaps(stopwatch.isReset()); // Update button states. updateFab(updateType); } /** * This runnable periodically updates times throughout the UI. It stops these updates when the * stopwatch is no longer running. */ private final class TimeUpdateRunnable implements Runnable { @Override public void run() { final long startTime = SystemClock.elapsedRealtime(); updateTime(); if (getStopwatch().isRunning()) { // Try to maintain a consistent period of time between redraws. final long endTime = SystemClock.elapsedRealtime(); final long delay = Math.max(0, startTime + REDRAW_PERIOD - endTime); mTimeText.postDelayed(this, delay); } } } /** * Tapping the stopwatch text also toggles the stopwatch state, just like the fab. */ private final class ToggleStopwatchRunnable implements Runnable { @Override public void run() { toggleStopwatchState(); } } /** * Acquire or release the wake lock based on the tab state. */ private final class TabWatcher implements TabListener { @Override public void selectedTabChanged(UiDataModel.Tab oldSelectedTab, UiDataModel.Tab newSelectedTab) { adjustWakeLock(); } } /** * Update the user interface in response to a stopwatch change. */ private class StopwatchWatcher implements StopwatchListener { @Override public void stopwatchUpdated(Stopwatch before, Stopwatch after) { if (after.isReset()) { mLapCount = 0; } if (DataModel.getDataModel().isApplicationInForeground()) { updateUI(FAB_AND_BUTTONS_MORPH); } } @Override public void lapAdded(Lap lap) { } } /** * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls * the recyclerview or when the size/position of elements within the recyclerview changes. */ private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)); } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)); } } /** * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the * contrast between floating buttons and the laps list content. */ private static final class GradientItemDecoration extends RecyclerView.ItemDecoration { // 0% - 25% of gradient length -> opacity changes from 0% to 50% // 25% - 90% of gradient length -> opacity changes from 50% to 100% // 90% - 100% of gradient length -> opacity remains at 100% private static final int[] ALPHAS = { 0x00, // 0% 0x1A, // 10% 0x33, // 20% 0x4D, // 30% 0x66, // 40% 0x80, // 50% 0x89, // 53.8% 0x93, // 57.6% 0x9D, // 61.5% 0xA7, // 65.3% 0xB1, // 69.2% 0xBA, // 73.0% 0xC4, // 76.9% 0xCE, // 80.7% 0xD8, // 84.6% 0xE2, // 88.4% 0xEB, // 92.3% 0xF5, // 96.1% 0xFF, // 100% 0xFF, // 100% 0xFF, // 100% }; /** * A reusable array of control point colors that define the gradient. It is based on the * background color of the window and thus recomputed each time that color is changed. */ private final int[] mGradientColors = new int[ALPHAS.length]; /** The drawable that produces the tinting gradient effect of this decoration. */ private final GradientDrawable mGradient = new GradientDrawable(); /** The height of the gradient; sized relative to the fab height. */ private final int mGradientHeight; public GradientItemDecoration(Context context) { mGradient.setOrientation(TOP_BOTTOM); updateGradientColors(UiDataModel.getUiDataModel().getWindowBackgroundColor()); final Resources resources = context.getResources(); final float fabHeight = resources.getDimensionPixelSize(com.wizardsofm.deskclock.R.dimen.fab_height); mGradientHeight = Math.round(fabHeight * 1.2f); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); final int w = parent.getWidth(); final int h = parent.getHeight(); mGradient.setBounds(0, h - mGradientHeight, w, h); mGradient.draw(c); } /** * Given a {@code baseColor}, compute a gradient of tinted colors that define the fade * effect to apply to the bottom of the lap list. * * @param baseColor a base color to which the gradient tint should be applied */ public void updateGradientColors(@ColorInt int baseColor) { // Compute the tinted colors that form the gradient. for (int i = 0; i < mGradientColors.length; i++) { mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]); } // Set the gradient colors into the drawable. mGradient.setColors(mGradientColors); } } }