1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.emergency.widgets.countdown; 18 19 20 import android.animation.TimeInterpolator; 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.PorterDuff; 26 import android.graphics.RectF; 27 import android.view.SurfaceHolder; 28 import android.view.animation.DecelerateInterpolator; 29 30 import androidx.annotation.GuardedBy; 31 32 import com.android.emergency.action.R; 33 34 import java.time.Duration; 35 import java.time.Instant; 36 37 /** 38 * Thread that handles the looping "pending" animation with optional count down text. 39 */ 40 final class LoopingAnimationThread extends Thread { 41 42 private final SurfaceHolder mSurfaceHolder; 43 private final Context mContext; 44 private final Paint mBackgroundPaint; 45 private final Paint mLoopPaint; 46 private final Paint mLoopHeadPaint; 47 private final TimeInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); 48 private final Duration mLoopInterval; 49 private final Duration mLoopTrailDelay; 50 51 private volatile boolean mIsDrawing = false; 52 53 @GuardedBy("this") 54 private RectF mLoopBounds; 55 56 @GuardedBy("this") 57 private float mLoopRadius; 58 59 @GuardedBy("this") 60 private float mLoopHeadRadius; 61 62 @GuardedBy("this") 63 private final CountDownRenderer mCountDownRenderer; 64 65 @GuardedBy("this") 66 private int mTotalDiameter; 67 68 /** 69 * Constructor for looping animation thread. 70 * 71 * @param surfaceHolder Surface holder from surface view. 72 * @param context The context from the view. 73 */ LoopingAnimationThread( SurfaceHolder surfaceHolder, Context context)74 LoopingAnimationThread( 75 SurfaceHolder surfaceHolder, 76 Context context) { 77 super(LoopingAnimationThread.class.getSimpleName()); 78 this.mSurfaceHolder = surfaceHolder; 79 mContext = context; 80 mBackgroundPaint = new Paint(); 81 mBackgroundPaint.setColor(Color.BLACK); 82 mBackgroundPaint.setStyle(Paint.Style.FILL); 83 mBackgroundPaint.setAntiAlias(true); 84 mLoopPaint = new Paint(); 85 mLoopPaint.setColor(Color.WHITE); 86 mLoopPaint.setStyle(Paint.Style.STROKE); 87 mLoopPaint.setAntiAlias(true); 88 mLoopHeadPaint = new Paint(); 89 mLoopHeadPaint.setColor(Color.WHITE); 90 mLoopHeadPaint.setStyle(Paint.Style.FILL); 91 mLoopHeadPaint.setAntiAlias(true); 92 93 mCountDownRenderer = new CountDownRenderer(context); 94 95 mLoopInterval = Duration.ofMillis( 96 mContext.getResources().getInteger(R.integer.count_down_view_loop_interval_millis)); 97 mLoopTrailDelay = Duration.ofMillis( 98 mContext.getResources().getInteger(R.integer.count_down_view_loop_delay_millis)); 99 } 100 101 @Override run()102 public void run() { 103 mIsDrawing = true; 104 Instant now = Instant.ofEpochMilli(System.currentTimeMillis()); 105 updateSize( 106 mSurfaceHolder.getSurfaceFrame().width(), 107 mSurfaceHolder.getSurfaceFrame().height()); 108 while (mIsDrawing) { 109 if (isInterrupted()) { 110 mIsDrawing = false; 111 return; 112 } 113 Canvas canvas = null; 114 try { 115 synchronized (mSurfaceHolder) { 116 canvas = mSurfaceHolder.lockCanvas(null); 117 draw(canvas, now); 118 } 119 } finally { 120 // Make sure we don't leave the Surface in an inconsistent state. 121 if (canvas != null) { 122 mSurfaceHolder.unlockCanvasAndPost(canvas); 123 } 124 } 125 } 126 } 127 128 /** 129 * Reveals the count down if animation started, otherwise count down will show when animation 130 * starts. 131 */ showCountDown()132 synchronized void showCountDown() { 133 mCountDownRenderer.show(); 134 } 135 136 /** Sets the count down left duration to be drawn. */ setCountDownLeft(Duration timeLeft)137 synchronized void setCountDownLeft(Duration timeLeft) { 138 if (timeLeft.isNegative() || timeLeft.isZero()) { 139 mCountDownRenderer.setCountDownLeft(Duration.ZERO); 140 } else { 141 mCountDownRenderer.setCountDownLeft(timeLeft); 142 } 143 } 144 145 /** 146 * Draw frame. 147 * 148 * @param canvas Canvas to draw on. 149 * @param startTime start time of animation. 150 */ draw(Canvas canvas, Instant startTime)151 private synchronized void draw(Canvas canvas, Instant startTime) { 152 if (!mIsDrawing) { 153 // It is possible to lose the canvas because surface got destroyed here. 154 return; 155 } 156 // Clear background. 157 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 158 // Draw background. 159 canvas.drawCircle(mLoopBounds.centerX(), mLoopBounds.centerY(), mLoopRadius, 160 mBackgroundPaint); 161 162 // Calculate each end of the loop's elapsed time for calculating what arc to draw. 163 164 long progressedMillis = 165 Duration.between( 166 startTime, Instant.ofEpochMilli(System.currentTimeMillis())).toMillis(); 167 long loopHeadT = progressedMillis % mLoopInterval.toMillis(); 168 long loopTailT = loopHeadT - mLoopTrailDelay.toMillis(); 169 170 // 0 means track start, 1 means track end. 171 172 Duration loopTrackInterval = mLoopInterval.minus(mLoopTrailDelay); 173 float loopTailTrackRatio = 174 loopTailT <= 0f ? 0f : loopTailT / (float) loopTrackInterval.toMillis(); 175 float loopHeadTrackRatio = 176 loopHeadT <= loopTrackInterval.toMillis() 177 ? loopHeadT / (float) loopTrackInterval.toMillis() 178 : 1f; 179 // Interpolate and convert track completion ratio to degrees. From a clockwise perspective, 180 // tail is starting angle and head is the ending angle so we intentionally swap terminology 181 // here. 182 float interpolatedSweepAngleStart = 183 mDecelerateInterpolator.getInterpolation(loopTailTrackRatio) * 360f; 184 float interpolatedSweepAngleEnd = mDecelerateInterpolator.getInterpolation( 185 loopHeadTrackRatio) * 360f; 186 float finalSweepAngleStart = interpolatedSweepAngleStart - 90f; 187 float finalSweepAngleEnd = interpolatedSweepAngleEnd - 90f; 188 canvas.drawArc( 189 mLoopBounds, 190 finalSweepAngleStart, 191 finalSweepAngleEnd - finalSweepAngleStart, 192 false, 193 mLoopPaint); 194 float leadingDotX = 195 (float) Math.cos(Math.toRadians(finalSweepAngleEnd)) * mLoopRadius 196 + mLoopBounds.centerX(); 197 float leadingDotY = 198 (float) Math.sin(Math.toRadians(finalSweepAngleEnd)) * mLoopRadius 199 + mLoopBounds.centerY(); 200 canvas.drawCircle(leadingDotX, leadingDotY, mLoopHeadRadius, mLoopHeadPaint); 201 if (mCountDownRenderer.isRevealed()) { 202 mCountDownRenderer.draw(canvas); 203 } 204 } 205 206 /** Update size of loop based on new width and height. */ updateSize(int w, int h)207 synchronized void updateSize(int w, int h) { 208 float lookTrackDiameterToBoundsRatio = 209 mContext.getResources().getFloat( 210 R.dimen.count_down_view_loop_track_diameter_to_bounds_ratio); 211 float lookHeadDiameterToBoundsRatio = 212 mContext.getResources().getFloat( 213 R.dimen.count_down_view_loop_head_diameter_to_bounds_ratio); 214 float lookStrokeWidthToBoundsRatio = 215 mContext.getResources().getFloat( 216 R.dimen.count_down_view_loop_stoke_width_to_bounds_ratio); 217 218 mTotalDiameter = Math.min(w, h); 219 // Use ratios to calculate loop/track radius. 220 mLoopRadius = mTotalDiameter * lookTrackDiameterToBoundsRatio * 0.5f; 221 // Use ratios to calculate loop head radius. 222 mLoopHeadRadius = mTotalDiameter * lookHeadDiameterToBoundsRatio * 0.5f; 223 // Use ratios to calculate loop stroke width. 224 mLoopPaint.setStrokeWidth(mTotalDiameter * lookStrokeWidthToBoundsRatio); 225 mLoopBounds = new RectF(0, 0, mLoopRadius * 2, mLoopRadius * 2); 226 float updatedSizeCenterOffsetX = (w - (mLoopRadius + mLoopHeadRadius) * 2) * 0.5f; 227 float updatedSizeCenterOffsetY = (h - (mLoopRadius + mLoopHeadRadius) * 2) * 0.5f; 228 mLoopBounds.offset( 229 updatedSizeCenterOffsetX + mLoopHeadRadius, 230 updatedSizeCenterOffsetY + mLoopHeadRadius); 231 // Update bounds for count down text. 232 mCountDownRenderer.updateBounds(mLoopBounds, mTotalDiameter); 233 } 234 235 /** Stop animation from drawing. */ stopDrawing()236 void stopDrawing() { 237 mIsDrawing = false; 238 } 239 } 240