| /* |
| * 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; |
| } |
| } |