├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── maxwellforest │ │ └── blogtimer │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── maxwellforest │ │ └── blogtimer │ │ ├── MainActivity.java │ │ └── TimerView.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Android 2 | /local.properties 3 | 4 | # Gradle 5 | .gradle 6 | /build 7 | 8 | # IDE 9 | .idea 10 | *.iml 11 | 12 | # OS-specific 13 | .DS_Store 14 | 15 | # Other 16 | .java_pid*.hprof 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maxwell Forest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog_android_timer 2 | Sample code for an Android countdown timer 3 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.maxwellforest.blogtimer" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:23.0.1' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/maxwellforest/blogtimer/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.maxwellforest.blogtimer; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/maxwellforest/blogtimer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.maxwellforest.blogtimer; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | /** 9 | * Main container. 10 | * 11 | * @author Mike Gouline 12 | */ 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | private static final int TIMER_LENGTH = 30; 16 | 17 | private TimerView mTimerView; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | 24 | mTimerView = (TimerView) findViewById(R.id.timer); 25 | 26 | final Button timerStartButton = (Button) findViewById(R.id.btn_timer_start); 27 | timerStartButton.setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View v) { 30 | mTimerView.start(TIMER_LENGTH); 31 | } 32 | }); 33 | } 34 | 35 | @Override 36 | protected void onPause() { 37 | mTimerView.stop(); 38 | 39 | super.onPause(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/maxwellforest/blogtimer/TimerView.java: -------------------------------------------------------------------------------- 1 | package com.maxwellforest.blogtimer; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.PorterDuff; 11 | import android.graphics.PorterDuffXfermode; 12 | import android.graphics.RectF; 13 | import android.util.AttributeSet; 14 | import android.view.View; 15 | import android.view.animation.LinearInterpolator; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | 19 | /** 20 | * Countdown timer view. 21 | * 22 | * @author Mike Gouline 23 | */ 24 | public class TimerView extends View { 25 | 26 | private static final int ARC_START_ANGLE = 270; // 12 o'clock 27 | 28 | private static final float THICKNESS_SCALE = 0.03f; 29 | 30 | private Bitmap mBitmap; 31 | private Canvas mCanvas; 32 | 33 | private RectF mCircleOuterBounds; 34 | private RectF mCircleInnerBounds; 35 | 36 | private Paint mCirclePaint; 37 | private Paint mEraserPaint; 38 | 39 | private float mCircleSweepAngle; 40 | 41 | private ValueAnimator mTimerAnimator; 42 | 43 | public TimerView(Context context) { 44 | this(context, null); 45 | } 46 | 47 | public TimerView(Context context, AttributeSet attrs) { 48 | this(context, attrs, 0); 49 | } 50 | 51 | public TimerView(Context context, AttributeSet attrs, int defStyleAttr) { 52 | super(context, attrs, defStyleAttr); 53 | 54 | int circleColor = Color.RED; 55 | 56 | if (attrs != null) { 57 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TimerView); 58 | if (ta != null) { 59 | circleColor = ta.getColor(R.styleable.TimerView_circleColor, circleColor); 60 | ta.recycle(); 61 | } 62 | } 63 | 64 | mCirclePaint = new Paint(); 65 | mCirclePaint.setAntiAlias(true); 66 | mCirclePaint.setColor(circleColor); 67 | 68 | mEraserPaint = new Paint(); 69 | mEraserPaint.setAntiAlias(true); 70 | mEraserPaint.setColor(Color.TRANSPARENT); 71 | mEraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 72 | } 73 | 74 | @SuppressWarnings("SuspiciousNameCombination") 75 | @Override 76 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 77 | super.onMeasure(widthMeasureSpec, widthMeasureSpec); // Trick to make the view square 78 | } 79 | 80 | @Override 81 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 82 | if (w != oldw || h != oldh) { 83 | mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 84 | mBitmap.eraseColor(Color.TRANSPARENT); 85 | mCanvas = new Canvas(mBitmap); 86 | } 87 | 88 | super.onSizeChanged(w, h, oldw, oldh); 89 | updateBounds(); 90 | } 91 | 92 | @Override 93 | protected void onDraw(Canvas canvas) { 94 | mCanvas.drawColor(0, PorterDuff.Mode.CLEAR); 95 | 96 | if (mCircleSweepAngle > 0f) { 97 | mCanvas.drawArc(mCircleOuterBounds, ARC_START_ANGLE, mCircleSweepAngle, true, mCirclePaint); 98 | mCanvas.drawOval(mCircleInnerBounds, mEraserPaint); 99 | } 100 | 101 | canvas.drawBitmap(mBitmap, 0, 0, null); 102 | } 103 | 104 | public void start(int secs) { 105 | stop(); 106 | 107 | mTimerAnimator = ValueAnimator.ofFloat(0f, 1f); 108 | mTimerAnimator.setDuration(TimeUnit.SECONDS.toMillis(secs)); 109 | mTimerAnimator.setInterpolator(new LinearInterpolator()); 110 | mTimerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 111 | @Override 112 | public void onAnimationUpdate(ValueAnimator animation) { 113 | drawProgress((float) animation.getAnimatedValue()); 114 | } 115 | }); 116 | mTimerAnimator.start(); 117 | } 118 | 119 | public void stop() { 120 | if (mTimerAnimator != null && mTimerAnimator.isRunning()) { 121 | mTimerAnimator.cancel(); 122 | mTimerAnimator = null; 123 | 124 | drawProgress(0f); 125 | } 126 | } 127 | 128 | private void drawProgress(float progress) { 129 | mCircleSweepAngle = 360 * progress; 130 | 131 | invalidate(); 132 | } 133 | 134 | private void updateBounds() { 135 | final float thickness = getWidth() * THICKNESS_SCALE; 136 | 137 | mCircleOuterBounds = new RectF(0, 0, getWidth(), getHeight()); 138 | mCircleInnerBounds = new RectF( 139 | mCircleOuterBounds.left + thickness, 140 | mCircleOuterBounds.top + thickness, 141 | mCircleOuterBounds.right - thickness, 142 | mCircleOuterBounds.bottom - thickness); 143 | 144 | invalidate(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 |