blob: 47cc8e55dbcab29e0bd09d3c05d81dac3c117ff7 [file] [log] [blame]
/*
* Copyright (C) 2020 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.emergency.widgets.countdown;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.view.SurfaceHolder;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.GuardedBy;
import com.android.emergency.action.R;
import java.time.Duration;
import java.time.Instant;
/**
* Thread that handles the looping "pending" animation with optional count down text.
*/
final class LoopingAnimationThread extends Thread {
private final SurfaceHolder mSurfaceHolder;
private final Context mContext;
private final Paint mBackgroundPaint;
private final Paint mLoopPaint;
private final Paint mLoopHeadPaint;
private final TimeInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
private final Duration mLoopInterval;
private final Duration mLoopTrailDelay;
private volatile boolean mIsDrawing = false;
@GuardedBy("this")
private RectF mLoopBounds;
@GuardedBy("this")
private float mLoopRadius;
@GuardedBy("this")
private float mLoopHeadRadius;
@GuardedBy("this")
private final CountDownRenderer mCountDownRenderer;
@GuardedBy("this")
private int mTotalDiameter;
/**
* Constructor for looping animation thread.
*
* @param surfaceHolder Surface holder from surface view.
* @param context The context from the view.
*/
LoopingAnimationThread(
SurfaceHolder surfaceHolder,
Context context) {
super(LoopingAnimationThread.class.getSimpleName());
this.mSurfaceHolder = surfaceHolder;
mContext = context;
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(Color.BLACK);
mBackgroundPaint.setStyle(Paint.Style.FILL);
mBackgroundPaint.setAntiAlias(true);
mLoopPaint = new Paint();
mLoopPaint.setColor(Color.WHITE);
mLoopPaint.setStyle(Paint.Style.STROKE);
mLoopPaint.setAntiAlias(true);
mLoopHeadPaint = new Paint();
mLoopHeadPaint.setColor(Color.WHITE);
mLoopHeadPaint.setStyle(Paint.Style.FILL);
mLoopHeadPaint.setAntiAlias(true);
mCountDownRenderer = new CountDownRenderer(context);
mLoopInterval = Duration.ofMillis(
mContext.getResources().getInteger(R.integer.count_down_view_loop_interval_millis));
mLoopTrailDelay = Duration.ofMillis(
mContext.getResources().getInteger(R.integer.count_down_view_loop_delay_millis));
}
@Override
public void run() {
mIsDrawing = true;
Instant now = Instant.ofEpochMilli(System.currentTimeMillis());
updateSize(
mSurfaceHolder.getSurfaceFrame().width(),
mSurfaceHolder.getSurfaceFrame().height());
while (mIsDrawing) {
if (isInterrupted()) {
mIsDrawing = false;
return;
}
Canvas canvas = null;
try {
synchronized (mSurfaceHolder) {
canvas = mSurfaceHolder.lockCanvas(null);
draw(canvas, now);
}
} finally {
// Make sure we don't leave the Surface in an inconsistent state.
if (canvas != null) {
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}
/**
* Reveals the count down if animation started, otherwise count down will show when animation
* starts.
*/
synchronized void showCountDown() {
mCountDownRenderer.show();
}
/** Sets the count down left duration to be drawn. */
synchronized void setCountDownLeft(Duration timeLeft) {
if (timeLeft.isNegative() || timeLeft.isZero()) {
mCountDownRenderer.setCountDownLeft(Duration.ZERO);
} else {
mCountDownRenderer.setCountDownLeft(timeLeft);
}
}
/**
* Draw frame.
*
* @param canvas Canvas to draw on.
* @param startTime start time of animation.
*/
private synchronized void draw(Canvas canvas, Instant startTime) {
if (!mIsDrawing) {
// It is possible to lose the canvas because surface got destroyed here.
return;
}
// Clear background.
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
// Draw background.
canvas.drawCircle(mLoopBounds.centerX(), mLoopBounds.centerY(), mLoopRadius,
mBackgroundPaint);
// Calculate each end of the loop's elapsed time for calculating what arc to draw.
long progressedMillis =
Duration.between(
startTime, Instant.ofEpochMilli(System.currentTimeMillis())).toMillis();
long loopHeadT = progressedMillis % mLoopInterval.toMillis();
long loopTailT = loopHeadT - mLoopTrailDelay.toMillis();
// 0 means track start, 1 means track end.
Duration loopTrackInterval = mLoopInterval.minus(mLoopTrailDelay);
float loopTailTrackRatio =
loopTailT <= 0f ? 0f : loopTailT / (float) loopTrackInterval.toMillis();
float loopHeadTrackRatio =
loopHeadT <= loopTrackInterval.toMillis()
? loopHeadT / (float) loopTrackInterval.toMillis()
: 1f;
// Interpolate and convert track completion ratio to degrees. From a clockwise perspective,
// tail is starting angle and head is the ending angle so we intentionally swap terminology
// here.
float interpolatedSweepAngleStart =
mDecelerateInterpolator.getInterpolation(loopTailTrackRatio) * 360f;
float interpolatedSweepAngleEnd = mDecelerateInterpolator.getInterpolation(
loopHeadTrackRatio) * 360f;
float finalSweepAngleStart = interpolatedSweepAngleStart - 90f;
float finalSweepAngleEnd = interpolatedSweepAngleEnd - 90f;
canvas.drawArc(
mLoopBounds,
finalSweepAngleStart,
finalSweepAngleEnd - finalSweepAngleStart,
false,
mLoopPaint);
float leadingDotX =
(float) Math.cos(Math.toRadians(finalSweepAngleEnd)) * mLoopRadius
+ mLoopBounds.centerX();
float leadingDotY =
(float) Math.sin(Math.toRadians(finalSweepAngleEnd)) * mLoopRadius
+ mLoopBounds.centerY();
canvas.drawCircle(leadingDotX, leadingDotY, mLoopHeadRadius, mLoopHeadPaint);
if (mCountDownRenderer.isRevealed()) {
mCountDownRenderer.draw(canvas);
}
}
/** Update size of loop based on new width and height. */
synchronized void updateSize(int w, int h) {
float lookTrackDiameterToBoundsRatio =
mContext.getResources().getFloat(
R.dimen.count_down_view_loop_track_diameter_to_bounds_ratio);
float lookHeadDiameterToBoundsRatio =
mContext.getResources().getFloat(
R.dimen.count_down_view_loop_head_diameter_to_bounds_ratio);
float lookStrokeWidthToBoundsRatio =
mContext.getResources().getFloat(
R.dimen.count_down_view_loop_stoke_width_to_bounds_ratio);
mTotalDiameter = Math.min(w, h);
// Use ratios to calculate loop/track radius.
mLoopRadius = mTotalDiameter * lookTrackDiameterToBoundsRatio * 0.5f;
// Use ratios to calculate loop head radius.
mLoopHeadRadius = mTotalDiameter * lookHeadDiameterToBoundsRatio * 0.5f;
// Use ratios to calculate loop stroke width.
mLoopPaint.setStrokeWidth(mTotalDiameter * lookStrokeWidthToBoundsRatio);
mLoopBounds = new RectF(0, 0, mLoopRadius * 2, mLoopRadius * 2);
float updatedSizeCenterOffsetX = (w - (mLoopRadius + mLoopHeadRadius) * 2) * 0.5f;
float updatedSizeCenterOffsetY = (h - (mLoopRadius + mLoopHeadRadius) * 2) * 0.5f;
mLoopBounds.offset(
updatedSizeCenterOffsetX + mLoopHeadRadius,
updatedSizeCenterOffsetY + mLoopHeadRadius);
// Update bounds for count down text.
mCountDownRenderer.updateBounds(mLoopBounds, mTotalDiameter);
}
/** Stop animation from drawing. */
void stopDrawing() {
mIsDrawing = false;
}
}