/* * Copyright (C) 2016 CaMnter [email protected] * * 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.camnter.easycountdowntextureview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.TextureView; import java.lang.ref.WeakReference; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** * Description:EasyCountDownTextureView * Created by:CaMnter * Time:2016-03-16 13:45 */ public class EasyCountDownTextureView extends TextureView implements TextureView.SurfaceTextureListener { private static final String TAG = EasyCountDownTextureView.class.getSimpleName(); private static final String LESS_THAN_TEN_FORMAT = "%02d"; private static final String COLON = ":"; private static final int DEFAULT_COLOR_BACKGROUND = Color.BLACK; private static final int DEFAULT_COLOR_COLON = Color.BLACK; private static final int DEFAULT_COLOR_TIME = Color.WHITE; private static final int DEFAULT_COLOR_RECT_BORDER = Color.BLACK; private DisplayMetrics metrics; private static final int COUNT_DOWN_INTERVAL = 1000; private static final long ONE_SECOND = 1000L; private static final long ONE_MINUTE = 60 * ONE_SECOND; private static final long ONE_HOUR = 60 * ONE_MINUTE; private static final long ONE_DAY = 24 * ONE_HOUR; private volatile long millisInFuture = 0L; /************** * Default dp * **************/ private static final float DEFAULT_BACKGROUND_PAINT_WIDTH = 0.01f; private static final float DEFAULT_COLON_PAINT_STROKE = 0.66f; private static final float DEFAULT_TIME_PAINT_STROKE = 0.77f; private static final float DEFAULT_ROUND_RECT_RADIUS = 2.66f; private static final float DEFAULT_RECT_WIDTH = 18.0f; private static final float DEFAULT_RECT_HEIGHT = 17.0f; private static final float DEFAULT_RECT_SPACING = 6.0f; private static final float DEFAULT_TIME_TEXT_SIZE = 13.0f; private static final float DEFAULT_COLON_TEXT_SIZE = 13.0f; // 66dp private static final float DEFAULT_VIEW_WIDTH = DEFAULT_RECT_WIDTH * 3 + DEFAULT_RECT_SPACING * 2; // 17dp private static final float DEFAULT_VIEW_HEIGHT = DEFAULT_RECT_HEIGHT; /************** * Default px * **************/ private float rectWidth; private float rectHeight; private float rectSpacing; private float rectRadius; private boolean drawRectBorder = false; private float paddingLeft; private float paddingTop; private float paddingRight; private float paddingBottom; private float firstTranslateX; private float firstTranslateColonX; private float secondTranslateX; private float secondTranslateColonX; private int timeHour; private int timeMinute; private int timeSecond; private int viewWidth; private int viewHeight; private float defaultWrapContentWidth; private float defaultWrapContentHeight; private EasyThread easyThread; private final Locale locale = Locale.getDefault(); private final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); private Paint colonPaint; private Paint rectBorderPaint; private Paint timePaint; // for draw time private float timePaintBaseLine; // for draw colon private float timePaintBaseLineFixed; private Paint backgroundPaint; private RectF backgroundRectF; private volatile long lastRecordTime = 0L; private volatile boolean runningState = false; private boolean autoResume = true; private long pauseTime = 0L; private EasyCountDownListener easyCountDownListener; private static class MainHandler extends Handler { private static final int WHAT_COUNT_DOWN_COMPLETED = 0x26; private final WeakReference<EasyCountDownListener> listenerReference; MainHandler(@NonNull final EasyCountDownListener easyCountDownListener) { super(Looper.getMainLooper()); this.listenerReference = new WeakReference<>(easyCountDownListener); } /** * Handle system messages here. */ @Override public void dispatchMessage(Message msg) { switch (msg.what) { case WHAT_COUNT_DOWN_COMPLETED: final EasyCountDownListener easyCountDownListener = this.listenerReference.get(); if (easyCountDownListener == null) return; easyCountDownListener.onCountDownCompleted(); break; } } } private MainHandler mainHandler; public EasyCountDownTextureView(Context context) { super(context); this.init(context, null); } public EasyCountDownTextureView(Context context, AttributeSet attrs) { super(context, attrs); this.init(context, attrs); } public EasyCountDownTextureView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.init(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public EasyCountDownTextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); this.init(context, attrs); } private void init(Context context, AttributeSet attrs) { this.metrics = this.getResources().getDisplayMetrics(); this.defaultWrapContentWidth = this.dp2px(DEFAULT_VIEW_WIDTH); this.defaultWrapContentHeight = this.dp2px(DEFAULT_VIEW_HEIGHT); this.setSurfaceTextureListener(this); this.setOpaque(false); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.EasyCountDownTextureView); this.timeHour = typedArray.getInteger(R.styleable.EasyCountDownTextureView_easyCountHour, 0); this.timeMinute = typedArray.getInteger( R.styleable.EasyCountDownTextureView_easyCountMinute, 0); this.timeSecond = typedArray.getInteger( R.styleable.EasyCountDownTextureView_easyCountSecond, 0); this.initTimePaint(typedArray); this.initColonPaint(typedArray); this.initRectBorderPaint(typedArray); this.initBackgroundPaint(typedArray); this.rectWidth = typedArray.getDimension( R.styleable.EasyCountDownTextureView_easyCountRectWidth, this.dp2px(DEFAULT_RECT_WIDTH)); this.rectHeight = typedArray.getDimension( R.styleable.EasyCountDownTextureView_easyCountRectHeight, this.dp2px(DEFAULT_RECT_HEIGHT)); this.rectSpacing = typedArray.getDimension( R.styleable.EasyCountDownTextureView_easyCountRectSpacing, this.dp2px(DEFAULT_RECT_SPACING)); this.refitBackgroundAttribute(); final Paint.FontMetricsInt timePaintFontMetrics = this.timePaint.getFontMetricsInt(); this.timePaintBaseLine = (this.backgroundRectF.bottom + this.backgroundRectF.top - timePaintFontMetrics.bottom - timePaintFontMetrics.top) / 2; // for colon this.timePaintBaseLineFixed = this.timePaintBaseLine / 40 * 37; this.rectRadius = typedArray.getDimension( R.styleable.EasyCountDownTextureView_easyCountRectRadius, this.dp2px(DEFAULT_ROUND_RECT_RADIUS)); typedArray.recycle(); this.updateTime(); } private void initColonPaint(@NonNull final TypedArray typedArray) { this.colonPaint = new Paint(); this.colonPaint.setAntiAlias(true); this.colonPaint.setColor( typedArray.getColor(R.styleable.EasyCountDownTextureView_easyCountColonColor, DEFAULT_COLOR_COLON)); this.colonPaint.setTextSize( typedArray.getDimension(R.styleable.EasyCountDownTextureView_easyCountColonSize, this.dp2px(DEFAULT_TIME_TEXT_SIZE))); this.colonPaint.setStrokeWidth( typedArray.getDimension(R.styleable.EasyCountDownTextureView_easyCountColonStroke, this.dp2px(DEFAULT_COLON_PAINT_STROKE))); this.colonPaint.setStyle(Paint.Style.FILL_AND_STROKE); this.colonPaint.setTextAlign(Paint.Align.CENTER); this.colonPaint.setStrokeCap(Paint.Cap.ROUND); } private void initTimePaint(@NonNull final TypedArray typedArray) { this.timePaint = new Paint(); this.timePaint.setAntiAlias(true); this.timePaint.setColor( typedArray.getColor(R.styleable.EasyCountDownTextureView_easyCountTimeColor, DEFAULT_COLOR_TIME)); this.timePaint.setTextSize( typedArray.getDimension(R.styleable.EasyCountDownTextureView_easyCountTimeSize, this.dp2px(DEFAULT_COLON_TEXT_SIZE))); this.timePaint.setStrokeWidth( typedArray.getDimension(R.styleable.EasyCountDownTextureView_easyCountTimeStroke, this.dp2px(DEFAULT_TIME_PAINT_STROKE))); this.timePaint.setStyle(Paint.Style.FILL_AND_STROKE); this.timePaint.setTextAlign(Paint.Align.CENTER); this.timePaint.setStrokeCap(Paint.Cap.ROUND); } private void initRectBorderPaint(@NonNull final TypedArray typedArray) { final float rectBorderStroke = typedArray.getDimension( R.styleable.EasyCountDownTextureView_easyCountRectBorderStroke, Float.MIN_VALUE); this.checkRectBorder(rectBorderStroke); if (!this.drawRectBorder) return; this.rectBorderPaint = new Paint(); this.rectBorderPaint.setAntiAlias(true); this.rectBorderPaint.setColor( typedArray.getColor(R.styleable.EasyCountDownTextureView_easyCountRectBorderColor, DEFAULT_COLOR_RECT_BORDER)); this.rectBorderPaint.setStrokeWidth(rectBorderStroke); this.rectBorderPaint.setStyle(Paint.Style.STROKE); this.rectBorderPaint.setTextAlign(Paint.Align.CENTER); this.rectBorderPaint.setStrokeCap(Paint.Cap.SQUARE); } private void initBackgroundPaint(@NonNull final TypedArray typedArray) { this.backgroundPaint = new Paint(); this.backgroundPaint.setAntiAlias(true); this.backgroundPaint.setColor( typedArray.getColor(R.styleable.EasyCountDownTextureView_easyCountBackgroundColor, DEFAULT_COLOR_BACKGROUND)); this.backgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE); this.backgroundPaint.setStrokeWidth(this.dp2px(DEFAULT_BACKGROUND_PAINT_WIDTH)); this.backgroundPaint.setTextAlign(Paint.Align.CENTER); this.backgroundPaint.setStrokeCap(Paint.Cap.ROUND); } private void checkRectBorder(final float rectBorder) { this.drawRectBorder = rectBorder != Float.MIN_VALUE; } private void updateTime() { this.millisInFuture = this.timeHour * ONE_HOUR + this.timeMinute * ONE_MINUTE + this.timeSecond * ONE_SECOND; this.setTime(this.millisInFuture); } private void refitBackgroundAttribute() { this.paddingLeft = this.getPaddingLeft(); this.paddingTop = this.getPaddingTop(); this.paddingRight = this.getPaddingRight(); this.paddingBottom = this.getPaddingBottom(); this.firstTranslateX = this.rectWidth + this.rectSpacing + paddingLeft; this.secondTranslateX = this.rectWidth * 2 + this.rectSpacing * 2 + paddingLeft; this.firstTranslateColonX = this.firstTranslateX - this.rectSpacing / 2; this.secondTranslateColonX = this.secondTranslateX - this.rectSpacing / 2; this.backgroundRectF = new RectF(0, 0, this.rectWidth, this.rectHeight); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); this.viewWidth = MeasureSpec.getSize(widthMeasureSpec); this.viewHeight = MeasureSpec.getSize(heightMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); float resultWidth; float resultHeight; switch (widthMode) { // wrap_content case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: resultWidth = this.defaultWrapContentWidth; break; // match_parent case MeasureSpec.EXACTLY: default: resultWidth = Math.max(this.viewWidth, this.defaultWrapContentWidth); break; } switch (heightMode) { // wrap_content case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: resultHeight = this.defaultWrapContentHeight; break; // match_parent case MeasureSpec.EXACTLY: default: resultHeight = Math.max(this.viewHeight, this.defaultWrapContentHeight); break; } resultWidth += (this.paddingLeft + this.paddingRight); resultHeight += (this.paddingTop + this.paddingBottom); this.setMeasuredDimension((int) resultWidth, (int) resultHeight); } @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); this.viewWidth = width; this.viewHeight = height; this.refitBackgroundAttribute(); this.invalidate(); } public void setTimeHour(final int timeHour) { this.timeHour = timeHour; this.updateTime(); } public void setTimeMinute(final int timeMinute) { this.timeMinute = timeMinute; this.updateTime(); } public void setTimeSecond(final int timeSecond) { this.timeSecond = timeSecond; this.updateTime(); } public void setRectWidth(final float rectWidthDp) { this.rectWidth = this.dp2px(rectWidthDp); this.refitBackgroundAttribute(); } public void setRectHeight(final float rectHeightDp) { this.rectHeight = this.dp2px(rectHeightDp); this.refitBackgroundAttribute(); } public void setRectSpacing(final float rectSpacingDp) { this.rectSpacing = this.dp2px(rectSpacingDp); this.refitBackgroundAttribute(); } public void setAutoResume(final boolean autoResume) { this.autoResume = autoResume; } public void setEasyCountDownListener( @NonNull final EasyCountDownListener easyCountDownListener) { this.easyCountDownListener = easyCountDownListener; this.mainHandler = new MainHandler(easyCountDownListener); } public boolean isRunningState() { return this.runningState; } public float getRectWidth() { return this.rectWidth; } public float getRectHeight() { return this.rectHeight; } public float getRectSpacing() { return this.rectSpacing; } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { Log.i(TAG, "[onSurfaceTextureAvailable]"); this.startAndRestoreTime(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { Log.i(TAG, "[onSurfaceTextureDestroyed]"); this.stopAndRecordTime(); return true; } public void stopAndRecordTime() { if (this.autoResume) { this.pauseTime = SystemClock.elapsedRealtime(); } this.stop(); } public void startAndRestoreTime() { if (this.pauseTime > 0) { this.millisInFuture -= (SystemClock.elapsedRealtime() - this.pauseTime); this.pauseTime = 0; } this.start(); } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { // Nothing to do } public void start() { if (this.runningState) return; this.drawZeroZeroZero(); if (millisInFuture > 0) { this.easyThread = new EasyThread(); this.easyThread.startThread(); this.easyThread.start(); this.runningState = true; if (this.easyCountDownListener != null) { this.easyCountDownListener.onCountDownStart(); } } else { if (this.easyCountDownListener != null) { this.easyCountDownListener.onCountDownTimeError(); } this.runningState = false; } } public void stop() { if (!this.runningState) return; if (this.easyThread != null) { this.easyThread.interrupt(); this.easyThread = null; } if (this.easyCountDownListener != null) { this.easyCountDownListener.onCountDownStop(this.millisInFuture); } this.runningState = false; } /** * Start count down by date * * @param date date */ public void setTime(@NonNull final Date date) { this.millisInFuture = date.getTime(); } /** * Start count down by timeMillis * * @param timeMillis timeMillis */ public void setTime(final long timeMillis) { this.millisInFuture = timeMillis; this.calendar.setTimeInMillis(this.millisInFuture); } private void drawZeroZeroZero() { Canvas canvas = null; try { canvas = EasyCountDownTextureView.this.lockCanvas(); if (canvas == null) return; this.drawTimeAndBackground(canvas, String.format(locale, LESS_THAN_TEN_FORMAT, 0), String.format(locale, LESS_THAN_TEN_FORMAT, 0), String.format(locale, LESS_THAN_TEN_FORMAT, 0)); unlockCanvasAndPost(canvas); } catch (Exception e) { e.printStackTrace(); unlockCanvasAndPost(canvas); } } private class EasyThread extends Thread { private volatile boolean running = false; private volatile boolean completed = false; EasyThread() { this.running = true; } final void startThread() { this.completed = false; this.running = true; } final void stopThread() { this.completed = true; this.running = false; } private int checkCalendarHour(final long millisInFuture, int calendarHour) { final int days = (int) (millisInFuture / ONE_DAY); if (days >= 1) { calendarHour += days * 24; } return calendarHour; } @Override public void run() { while (!this.completed) { while (this.running) { Canvas canvas = null; try { synchronized (this) { lastRecordTime = SystemClock.elapsedRealtime(); canvas = EasyCountDownTextureView.this.lockCanvas(); if (canvas == null) continue; timeHour = calendar.get(Calendar.HOUR_OF_DAY); timeMinute = calendar.get(Calendar.MINUTE); timeSecond = calendar.get(Calendar.SECOND); drawTimeAndBackground(canvas, String.format(locale, LESS_THAN_TEN_FORMAT, this.checkCalendarHour(millisInFuture, timeHour)), String.format(locale, LESS_THAN_TEN_FORMAT, timeMinute), String.format(locale, LESS_THAN_TEN_FORMAT, timeSecond)); final long pastTime = SystemClock.elapsedRealtime() - lastRecordTime; if (pastTime < COUNT_DOWN_INTERVAL) { this.wait(COUNT_DOWN_INTERVAL - pastTime); } // refresh time millisInFuture -= 1000; if (millisInFuture < 0) { this.completed = true; this.running = false; // refresh runningState runningState = false; if (mainHandler != null) { mainHandler.sendEmptyMessageDelayed( MainHandler.WHAT_COUNT_DOWN_COMPLETED, 1000); } calendar.setTimeInMillis(0); } else { calendar.setTimeInMillis(millisInFuture); } } } catch (InterruptedException interruptedException) { final long intervalTime = SystemClock.elapsedRealtime() - lastRecordTime; Log.i(TAG, "[run]\t\t\t thread interrupted\t\t\t interval time: " + intervalTime, interruptedException); millisInFuture -= intervalTime; calendar.setTimeInMillis(millisInFuture); this.stopThread(); } catch (Exception e) { e.printStackTrace(); } finally { try { unlockCanvasAndPost(canvas); } catch (Exception e) { e.printStackTrace(); } } } } } } private void drawTimeAndBackground(@NonNull final Canvas canvas, @NonNull final String hour, @NonNull final String minute, @NonNull final String second) { // background canvas.save(); canvas.translate(paddingLeft, paddingTop); canvas.drawRoundRect(backgroundRectF, rectRadius, rectRadius, backgroundPaint); // border this.drawRectBorder(canvas, backgroundRectF, rectRadius, rectBorderPaint); canvas.drawText(hour, backgroundRectF.centerX(), timePaintBaseLine, timePaint); canvas.restore(); // colon canvas.save(); canvas.translate(firstTranslateColonX, paddingTop); canvas.drawText(COLON, 0, timePaintBaseLineFixed, colonPaint); canvas.restore(); // background canvas.save(); canvas.translate(firstTranslateX, paddingTop); canvas.drawRoundRect(backgroundRectF, rectRadius, rectRadius, backgroundPaint); // border this.drawRectBorder(canvas, backgroundRectF, rectRadius, rectBorderPaint); canvas.drawText(minute, backgroundRectF.centerX(), timePaintBaseLine, timePaint); canvas.restore(); // colon canvas.save(); canvas.translate(secondTranslateColonX, paddingTop); canvas.drawText(COLON, 0, timePaintBaseLineFixed, colonPaint); canvas.restore(); // background canvas.save(); canvas.translate(secondTranslateX, paddingTop); canvas.drawRoundRect(backgroundRectF, rectRadius, rectRadius, backgroundPaint); // border this.drawRectBorder(canvas, backgroundRectF, rectRadius, rectBorderPaint); canvas.drawText(second, backgroundRectF.centerX(), timePaintBaseLine, timePaint); canvas.restore(); } private void drawRectBorder(@NonNull final Canvas canvas, @NonNull final RectF rect, final float rectRadius, @Nullable Paint paint) { if (paint == null) return; if (rectRadius > 0) { paint.setStrokeCap(Paint.Cap.ROUND); canvas.drawRoundRect(rect, rectRadius, rectRadius, paint); } else { paint.setStrokeCap(Paint.Cap.SQUARE); canvas.drawRect(rect, paint); } } /** * Dp to px * * @param dp dp * @return px */ private float dp2px(final float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.metrics); } public interface EasyCountDownListener { /** * When count down start */ void onCountDownStart(); /** * When count down time error */ void onCountDownTimeError(); /** * When count down stop * * @param millisInFuture millisInFuture */ void onCountDownStop(long millisInFuture); /** * When count down completed */ void onCountDownCompleted(); } }