/* * Copyright (C) 2013 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.google.android.glass.sample.stopwatch; import android.content.Context; import android.media.AudioManager; import android.media.SoundPool; import android.os.Handler; import android.os.SystemClock; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.FrameLayout; import android.widget.TextView; import java.util.concurrent.TimeUnit; /** * Animated countdown going from {@code mTimeSeconds} to 0. * * The current animation for each second is as follow: * 1. From 0 to 500ms, move the TextView from {@code MAX_TRANSLATION_Y} to 0 and its alpha from * {@code 0} to {@code ALPHA_DELIMITER}. * 2. From 500ms to 1000ms, update the TextView's alpha from {@code ALPHA_DELIMITER} to {@code 1}. * At each second change, update the TextView text. */ public class CountDownView extends FrameLayout { /** * Interface to listen for changes in the countdown. */ public interface Listener { /** * Notified of a tick, indicating a layout change. */ public void onTick(long millisUntilFinish); /** * Notified when the countdown is finished. */ public void onFinish(); } /** Time delimiter specifying when the second component is fully shown. */ public static final float ANIMATION_DURATION_IN_MILLIS = 850.0f; private static final long DELAY_MILLIS = 40; private static final int SOUND_PRIORITY = 1; private static final int MAX_STREAMS = 1; // Constants visible for testing. static final int MAX_TRANSLATION_Y = 30; static final float ALPHA_DELIMITER = 0.95f; static final long SEC_TO_MILLIS = TimeUnit.SECONDS.toMillis(1); // Sounds ID visible for testing. final int mFinishSoundId; final int mCountDownSoundId; private final TextView mSecondsView; private final SoundPool mSoundPool; private final Handler mHandler = new Handler(); private final Runnable mUpdateViewRunnable = new Runnable() { @Override public void run() { if (!updateView()) { postDelayed(mUpdateViewRunnable, DELAY_MILLIS); } } }; private long mTimeSeconds; private long mCurrentTimeSeconds; private long mStopTimeInFuture; private Listener mListener; private boolean mStarted; public CountDownView(Context context) { this(context, null, 0); } public CountDownView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CountDownView(Context context, AttributeSet attrs, int style) { super(context, attrs, style); LayoutInflater.from(context).inflate(R.layout.card_countdown, this); mSecondsView = (TextView) findViewById(R.id.seconds); mSoundPool = new SoundPool(MAX_STREAMS, AudioManager.STREAM_MUSIC, 0); mFinishSoundId = mSoundPool.load(context, R.raw.start, SOUND_PRIORITY); mCountDownSoundId = mSoundPool.load(context, R.raw.countdown_bip, SOUND_PRIORITY); } public void setCountDown(long timeSeconds) { mTimeSeconds = timeSeconds; } public long getCountDown() { return mTimeSeconds; } /** * Sets a {@link Listener}. */ public void setListener(Listener listener) { mListener = listener; } /** * Returns the set {@link Listener}. */ public Listener getListener() { return mListener; } @Override public boolean postDelayed(Runnable action, long delayMillis) { return mHandler.postDelayed(action, delayMillis); } /** * Starts the countdown animation if not yet started. */ public void start() { if (!mStarted) { mCurrentTimeSeconds = 0; mStopTimeInFuture = TimeUnit.SECONDS.toMillis(mTimeSeconds) + getElapsedRealtime(); mStarted = true; postDelayed(mUpdateViewRunnable, 0); } } /** * Returns {@link SystemClock.elapsedRealtime}, overridable for testing. */ protected long getElapsedRealtime() { return SystemClock.elapsedRealtime(); } /** * Plays the provided {@code soundId}, overridable for testing. */ protected void playSound(int soundId) { mSoundPool.play(soundId, 1 /* leftVolume */, 1 /* rightVolume */, SOUND_PRIORITY, 0 /* loop */, 1 /* rate */); } /** * Updates the view to reflect the current state of animation, visible for testing. * * @return whether or not the count down is finished. */ boolean updateView() { long millisLeft = mStopTimeInFuture - getElapsedRealtime(); long currentTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(millisLeft); boolean countDownDone = millisLeft <= 0; if (countDownDone) { mStarted = false; if (mListener != null) { mListener.onFinish(); } playSound(mFinishSoundId); } else { updateView(millisLeft); if (mListener != null) { mListener.onTick(millisLeft); } if (mCurrentTimeSeconds != currentTimeSeconds) { playSound(mCountDownSoundId); mCurrentTimeSeconds = currentTimeSeconds; } } return countDownDone; } /** * Updates the view to reflect the current state of animation, visible for testing. * * @params millisUntilFinish milliseconds until the countdown is done */ void updateView(long millisUntilFinish) { long currentTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(millisUntilFinish) + 1; long frame = SEC_TO_MILLIS - (millisUntilFinish % SEC_TO_MILLIS); mSecondsView.setText(Long.toString(currentTimeSeconds)); if (frame <= ANIMATION_DURATION_IN_MILLIS) { float factor = frame / ANIMATION_DURATION_IN_MILLIS; mSecondsView.setAlpha(factor * ALPHA_DELIMITER); mSecondsView.setTranslationY(MAX_TRANSLATION_Y * (1 - factor)); } else { float factor = (frame - ANIMATION_DURATION_IN_MILLIS) / ANIMATION_DURATION_IN_MILLIS; mSecondsView.setAlpha(ALPHA_DELIMITER + factor * (1 - ALPHA_DELIMITER)); } } }