├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── ForTheWatch.iml ├── ForTheWatchSource.iml ├── README.md ├── README_ENGLISH.md ├── TimerView.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── pheynix │ │ └── forthewatch │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pheynix │ │ └── forthewatch │ │ ├── MainActivity.java │ │ ├── Model.java │ │ └── MyTimer.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_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 │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── forthewatch ├── .gitignore ├── build.gradle ├── forthewatch.iml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── pheynix │ │ └── forthewatch │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pheynix │ │ └── forthewatch │ │ ├── Model.java │ │ └── MyTimer.java │ └── res │ └── values │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── read_me ├── screen_record.gif └── screen_shot.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | TimerView -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ForTheWatch.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ForTheWatchSource.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimerView 2 | 3 | [ENGLISH README](README_ENGLISH.md) 4 | 5 | 6 | ## 重要的事情 7 |
8 | 由于使用了MaskFilter,辉光效果必须[关闭硬件加速](http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/)!! 9 | 10 | 由于使用了MakskFilter,辉光效果必须[关闭硬件加速](http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/)!! 11 | 12 | 由于使用了MakskFilter,辉光效果必须[关闭硬件加速](http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/)!! 13 | 14 | 15 | 16 | 17 | ## 简介 18 |
19 | 一个倒计时的View, 20 | 21 | 可以实现最大6小时的倒计时,具体看图~ 22 | 23 | 24 | 25 | 26 | ## 展示 27 |
28 | 控件长这样,下方的button不属于这个控件 29 | 30 | ![屏幕截图](/read_me/screen_shot.png) 31 | 32 | 33 |

34 | 35 | 36 | 37 | 👇👇👇👇👇👇👇👇👇👇这儿有个gif图,等一会或者刷新~👇👇👇👇👇👇👇👇👇👇 38 | 39 | 40 | ![gif图,加载好慢...](/read_me/screen_record.gif) 41 | 42 | 👆👆👆👆👆👆👆👆👆👆这儿有个gif图,等一会或者刷新~👆👆👆👆👆👆👆👆👆👆 43 | 44 | 45 | 46 | ## 使用 47 | 48 | ### 方法1: 49 | 50 | 添加以下代码到build.gradle(Project:xxxxx): 51 | 52 | ``` 53 | repositories { 54 | maven { url "https://jitpack.io" } 55 | } 56 | 57 | ``` 58 | 59 | 添加以下代码到build.gradle(Module:xxxxx): 60 | 61 | ``` 62 | dependencies { 63 | compile 'com.github.pheynix:TimerView:660400fb64' 64 | } 65 | ``` 66 | 67 | ### 方法2: 68 | 69 | 找到[MyTimer.java](/app/src/main/java/com/pheynix/forthewatch/MyTimer.java),复制到你的项目里面,就可以使用或者修改了~ 70 | 71 | 72 | 73 | 74 | ## 更新 75 | 76 | 77 | * 20150716 增加listener 78 | * 20150717 整合@wsk900906 fork的版本,增加计时功能,新增listener 79 | * 20150719 增加lib方便使用 80 | 81 | ---- 82 | 83 | 84 | 如果对这么控件有啥问题或建议,尽情提issues或者[发邮件](mailto:pheynixdu@gmail.com)给我pheynixdu@gmail.com 85 | 86 | 87 |


88 | 其实我是真的忙... 89 | 90 | 一个人写了好久代码,想找群小伙伴了 91 | 92 | 深圳/广州找工作ing... 93 | -------------------------------------------------------------------------------- /README_ENGLISH.md: -------------------------------------------------------------------------------- 1 | # TimerView 2 | 3 | [中文README](README.md) 4 | 5 | 6 | ## Something important 7 |
8 | 9 | If you need the glow effect,please [DISABLE](http://developer.android.com/guide/topics/graphics/hardware-accel.html) hardware accelerated 10 | 11 | If you need the glow effect,please [DISABLE](http://developer.android.com/guide/topics/graphics/hardware-accel.html) hardware accelerated 12 | 13 | If you need the glow effect,please [DISABLE](http://developer.android.com/guide/topics/graphics/hardware-accel.html) hardware accelerated 14 | 15 | 16 | 17 | 18 | ## Summary 19 |
20 | a timer view, 21 | 22 | look at the picture below; 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## Screen shot 30 |
31 | It looks like this,buttons doesn't belong to this view 32 | 33 | ![screen shot](/read_me/screen_shot.png) 34 | 35 | 36 |

37 | 38 | 39 | 40 | 👇👇👇👇👇👇👇👇👇👇here is a gif picture,wait or refresh👇👇👇👇👇👇👇👇👇👇 41 | 42 | 43 | ![gif](/read_me/screen_record.gif) 44 | 45 | 👆👆👆👆👆👆👆👆👆👆here is a gif picture,wait or refresh👆👆👆👆👆👆👆👆👆👆 46 | 47 | 48 | ## 使用 49 | 50 | ### Method 1: 51 | 52 | Add the following to your build.gradle(Project:xxxxx): 53 | 54 | ``` 55 | repositories { 56 | maven { url "https://jitpack.io" } 57 | } 58 | ``` 59 | 60 | Add the following to your build.gradle(Module:xxxxx): 61 | 62 | ``` 63 | dependencies { 64 | compile 'com.github.pheynix:TimerView:660400fb64' 65 | } 66 | ``` 67 | 68 | ### Method 2: 69 | 70 | Locate [MyTimer.java](/app/src/main/java/com/pheynix/forthewatch/MyTimer.java),copy it,modify it,use it,or just let it lay on you hard disk forever~ 71 | 72 | 73 | 74 | ## Change Log 75 | 76 | 77 | * 20150716 add listener,add English remark 78 | * 20150717 merge fork version from @wsk900906 , add timing function,add listener 79 | * 20150719 add lib 80 | 81 | 82 | ---- 83 | 84 | If you have problem using this view or want to give me some advice on improving it,feel free to issues or [email me](mailto:pheynixdu@gmail.com),pheynixdu@gmail.com. 85 | 86 | 87 | 88 |


89 | I AM SO SORRY FOR MY POOR ENGLISH... 90 | -------------------------------------------------------------------------------- /TimerView.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.pheynix.forthewatch" 9 | minSdkVersion 16 10 | targetSdkVersion 22 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:22.2.0' 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 /Users/pheynix/Documents/coding/sdk/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/pheynix/forthewatch/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.pheynix.forthewatch; 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 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/pheynix/forthewatch/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.pheynix.forthewatch; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.widget.Button; 8 | 9 | public class MainActivity extends AppCompatActivity implements View.OnClickListener, MyTimer.OnTimeChangeListener, MyTimer.OnSecondChangListener,MyTimer.OnMinChangListener,MyTimer.OnHourChangListener { 10 | 11 | MyTimer timer; 12 | Button btn_start,btn_stop,btn_reset; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | 19 | timer = (MyTimer) findViewById(R.id.timer); 20 | timer.setOnTimeChangeListener(this); 21 | timer.setSecondChangListener(this); 22 | timer.setMinChangListener(this); 23 | timer.setHourChangListener(this); 24 | timer.setModel(Model.Timer); 25 | timer.setStartTime(1,30,30); 26 | btn_start = (Button) findViewById(R.id.btn_start); 27 | btn_stop = (Button) findViewById(R.id.btn_stop); 28 | btn_reset = (Button) findViewById(R.id.btn_reset); 29 | btn_start.setOnClickListener(this); 30 | btn_stop.setOnClickListener(this); 31 | btn_reset.setOnClickListener(this); 32 | } 33 | 34 | @Override 35 | public void onClick(View v) { 36 | switch (v.getId()){ 37 | case R.id.btn_start: 38 | timer.start(); 39 | break; 40 | case R.id.btn_stop: 41 | timer.stop(); 42 | break; 43 | case R.id.btn_reset: 44 | timer.reset(); 45 | break; 46 | } 47 | } 48 | 49 | @Override 50 | public void onTimerStart(long timeStart) { 51 | Log.e("pheynix","onTimerStart "+timeStart); 52 | } 53 | 54 | @Override 55 | public void onTimeChange(long timeStart, long timeRemain) { 56 | Log.e("pheynix","onTimeChange timeStart "+timeStart); 57 | Log.e("pheynix","onTimeChange timeRemain "+timeRemain); 58 | } 59 | 60 | @Override 61 | public void onTimeStop(long timeStart, long timeRemain) { 62 | Log.e("pheynix","onTimeStop timeRemain "+timeStart); 63 | Log.e("pheynix","onTimeStop timeRemain "+timeRemain); 64 | } 65 | 66 | @Override 67 | public void onSecondChange(int second) { 68 | Log.e("swifty","second change to "+second); 69 | } 70 | 71 | @Override 72 | public void onHourChange(int hour) { 73 | Log.e("swifty","hour change to "+hour); 74 | } 75 | 76 | @Override 77 | public void onMinChange(int minute) { 78 | Log.e("swifty", "minute change to "+minute); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/pheynix/forthewatch/Model.java: -------------------------------------------------------------------------------- 1 | package com.pheynix.forthewatch; 2 | 3 | /** 4 | * Created by Swifty.Wang on 2015/7/16. 5 | */ 6 | public enum Model { 7 | Timer, 8 | StopWatch; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/pheynix/forthewatch/MyTimer.java: -------------------------------------------------------------------------------- 1 | package com.pheynix.forthewatch; 2 | 3 | import android.content.Context; 4 | import android.graphics.BlurMaskFilter; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.Rect; 8 | import android.graphics.RectF; 9 | import android.os.Handler; 10 | import android.os.Message; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | 16 | import java.util.Calendar; 17 | import java.util.Timer; 18 | import java.util.TimerTask; 19 | 20 | 21 | /** 22 | * Created by pheynix on 7/12/15. 23 | * Sorry for my English... 24 | * 25 | *Disable hardware accelerate if you need the glow effect,See: http://developer.android.com/guide/topics/graphics/hardware-accel.html 26 | *Disable hardware accelerate if you need the glow effect,See: http://developer.android.com/guide/topics/graphics/hardware-accel.html 27 | *Disable hardware accelerate if you need the glow effect,See: http://developer.android.com/guide/topics/graphics/hardware-accel.html 28 | *如果需要辉光效果,请务必关闭硬件加速,参考: http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/ 29 | *如果需要辉光效果,请务必关闭硬件加速,参考: http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/ 30 | *如果需要辉光效果,请务必关闭硬件加速,参考: http://blog.chenming.info/blog/2012/09/18/android-hardware-accel/ 31 | */ 32 | public class MyTimer extends View { 33 | 34 | //use this in Log,saving lots of time; 35 | //在打Log的时候使用,节约时间 36 | private static final String tag = "pheynix"; 37 | private static final String msg = ">>>>>>>>>>>>>>>>>>>>>> LOOK AT ME <<<<<<<<<<<<<<<<<<<<<<<<"; 38 | 39 | 40 | //flags 41 | private boolean isInitialized = false; 42 | private boolean isStarted = false;// =true if countdown begin //倒计时开始的时候为true 43 | private boolean isInDragButton; 44 | private int whichDragButton;//1==hour 2==minute 3==second 45 | 46 | //store time,calculate result when countdown pause/stop 47 | //保存时间,倒计时结束时计算时间差 48 | private Calendar timeStart; 49 | private Calendar timeRemain; 50 | 51 | //dimension and coordinate 52 | //尺寸和坐标 53 | private float viewWidth; 54 | private float viewHeight; 55 | private float circleRadiusHour; 56 | private float circleRadiusMinute; 57 | private float circleRadiusSecond; 58 | private float circleRadiusDragButton; 59 | private float currentDegreeHour; 60 | private float currentDegreeMinute; 61 | private float currentDegreeSecond; 62 | private float centerXHour; 63 | private float centerYHour; 64 | private float centerXMinute; 65 | private float centerYMinute; 66 | private float centerXSecond; 67 | private float centerYSecond; 68 | private float strokeWidth; 69 | private String displayNumberHour; 70 | private String displayNumberMinute; 71 | private String displayNumberSecond; 72 | 73 | private float[] dragButtonHourPosition; 74 | private float[] dragButtonMinutePosition; 75 | private float[] dragButtonSecondPosition; 76 | 77 | private float[] defaultDragButtonHourPosition; 78 | private float[] defaultDragButtonMinutePosition; 79 | private float[] defaultDragButtonSecondPosition; 80 | 81 | //paint 82 | private Paint paintCircleBackground; 83 | private Paint paintDragButton; 84 | private Paint paintHour; 85 | private Paint paintMinute; 86 | private Paint paintSecond; 87 | private Paint paintNumber; 88 | private Paint paintGlowEffect; 89 | 90 | //color 91 | private static final int colorDefault = 0xFFD6D6D6; 92 | private static final int colorHour = 0xFF9AD13C; 93 | private static final int colorMinute = 0xFFA55F7C; 94 | private static final int colorSecond = 0xFF00BCD4; 95 | 96 | private static final int DEFAULT_VIEW_WIDTH = 720; 97 | private static final int DEFAULT_VIEW_HEIGHT = 720; 98 | 99 | private OnTimeChangeListener timeChangeListener; 100 | private OnMinChangListener minChangListener; 101 | private OnHourChangListener hourChangListener; 102 | private OnSecondChangListener secondChangListener; 103 | private OnInitialFinishListener initialFinishListener; 104 | //add model by Swifty default timer 105 | private Model model = Model.Timer; 106 | private boolean maxTime; 107 | 108 | //initialize every thing 109 | //初始化 110 | private void initialize(Canvas canvas) { 111 | Log.e("aa", "init"); 112 | timeRemain = Calendar.getInstance(); 113 | timeStart = Calendar.getInstance(); 114 | timeStart.clear(); 115 | timeRemain.clear(); 116 | 117 | viewWidth = canvas.getWidth(); 118 | viewHeight = canvas.getHeight(); 119 | 120 | //use different dimension in high resolution device 121 | //保证高分辨率屏幕有比较好的显示效果 122 | if (viewWidth > 720) { 123 | strokeWidth = 30; 124 | circleRadiusDragButton = 50; 125 | } else { 126 | strokeWidth = 15; 127 | circleRadiusDragButton = 25; 128 | } 129 | 130 | circleRadiusHour = viewWidth / 6; 131 | circleRadiusMinute = viewWidth / 3; 132 | circleRadiusSecond = viewWidth / 6; 133 | currentDegreeHour = 0; 134 | currentDegreeMinute = 0; 135 | currentDegreeSecond = 0; 136 | displayNumberHour = "0"; 137 | displayNumberMinute = "0"; 138 | displayNumberSecond = "0"; 139 | centerXHour = viewWidth / 6 + strokeWidth/2 + circleRadiusDragButton/2; 140 | centerYHour = viewHeight / 6 + strokeWidth/2 + circleRadiusDragButton/2; 141 | centerXMinute = viewWidth / 2; 142 | centerYMinute = 2*viewHeight / 3 - strokeWidth; 143 | centerXSecond = 5 * viewWidth / 6 - strokeWidth/2 - circleRadiusDragButton/2; 144 | centerYSecond = viewHeight / 6 + strokeWidth/2 + circleRadiusDragButton/2; 145 | 146 | defaultDragButtonHourPosition = new float[]{centerXHour, centerYHour - circleRadiusHour}; 147 | defaultDragButtonMinutePosition = new float[]{centerXMinute, centerYMinute - circleRadiusMinute}; 148 | defaultDragButtonSecondPosition = new float[]{centerXSecond, centerYSecond - circleRadiusSecond}; 149 | dragButtonHourPosition = defaultDragButtonHourPosition; 150 | dragButtonMinutePosition = defaultDragButtonMinutePosition; 151 | dragButtonSecondPosition = defaultDragButtonSecondPosition; 152 | 153 | paintCircleBackground = new Paint(); 154 | paintDragButton = new Paint(); 155 | paintHour = new Paint(); 156 | paintMinute = new Paint(); 157 | paintSecond = new Paint(); 158 | paintNumber = new Paint(); 159 | paintGlowEffect = new Paint(); 160 | 161 | paintCircleBackground.setColor(colorDefault); 162 | paintCircleBackground.setStrokeWidth(strokeWidth); 163 | paintCircleBackground.setStyle(Paint.Style.STROKE); 164 | paintCircleBackground.setAntiAlias(true); 165 | paintDragButton.setStrokeWidth(5); 166 | paintDragButton.setStyle(Paint.Style.FILL); 167 | paintDragButton.setAntiAlias(true); 168 | paintHour.setColor(colorHour); 169 | paintHour.setStrokeWidth(strokeWidth); 170 | paintHour.setStyle(Paint.Style.STROKE); 171 | paintHour.setAntiAlias(true); 172 | paintMinute.setColor(colorMinute); 173 | paintMinute.setStrokeWidth(strokeWidth); 174 | paintMinute.setStyle(Paint.Style.STROKE); 175 | paintMinute.setAntiAlias(true); 176 | paintSecond.setColor(colorSecond); 177 | paintSecond.setStrokeWidth(strokeWidth); 178 | paintSecond.setStyle(Paint.Style.STROKE); 179 | paintSecond.setAntiAlias(true); 180 | paintNumber.setStrokeWidth(2); 181 | paintNumber.setStyle(Paint.Style.FILL); 182 | paintNumber.setAntiAlias(true); 183 | 184 | //draw glow effect on the end of arc,glow-effect == dragButton 185 | //用于绘制圆弧尽头的辉光效果,辉光区域就是dragButton的区域 186 | paintGlowEffect.setMaskFilter(new BlurMaskFilter(2 * strokeWidth / 3, BlurMaskFilter.Blur.NORMAL)); 187 | paintGlowEffect.setStrokeWidth(strokeWidth); 188 | paintGlowEffect.setAntiAlias(true); 189 | paintGlowEffect.setStyle(Paint.Style.FILL); 190 | 191 | //完成初始化回调 192 | if (initialFinishListener != null) { 193 | initialFinishListener.onInitialFinishListener(); 194 | } 195 | } 196 | 197 | public MyTimer(Context context) { 198 | super(context); 199 | } 200 | 201 | //use this view in .xml file will invoke this constructor 202 | //在.xml中使用此控件时调用此构造函数 203 | public MyTimer(Context context, AttributeSet attrs) { 204 | super(context, attrs); 205 | } 206 | 207 | public MyTimer(Context context, AttributeSet attrs, int defStyleAttr) { 208 | super(context, attrs, defStyleAttr); 209 | } 210 | 211 | 212 | @Override 213 | protected void onDraw(Canvas canvas) { 214 | super.onDraw(canvas); 215 | 216 | //initialize dimension and coordinate just for once 217 | //初始化尺寸,只会调用一次 218 | if (!isInitialized) { 219 | initialize(canvas); 220 | isInitialized = true; 221 | } 222 | 223 | //arc and number depending on degree,update before drawing 224 | //角度决定圆弧长度和数字,每次重绘前先更新角度 225 | if (isStarted) { 226 | updateDegree(); 227 | } 228 | 229 | 230 | //draw background circle 231 | //画背景的圆圈 232 | canvas.drawCircle(centerXHour, centerYHour, circleRadiusHour, paintCircleBackground); 233 | canvas.drawCircle(centerXMinute, centerYMinute, circleRadiusMinute, paintCircleBackground); 234 | canvas.drawCircle(centerXSecond, centerYSecond, circleRadiusSecond, paintCircleBackground); 235 | 236 | //draw arc 237 | //画弧形 238 | RectF rectFHour = new RectF(centerXHour - circleRadiusHour, centerYHour - circleRadiusHour 239 | , centerXHour + circleRadiusHour, centerYHour + circleRadiusHour); 240 | RectF rectFMinute = new RectF(centerXMinute - circleRadiusMinute, centerYMinute - circleRadiusMinute 241 | , centerXMinute + circleRadiusMinute, centerYMinute + circleRadiusMinute); 242 | RectF rectFSecond = new RectF(centerXSecond - circleRadiusSecond, centerYSecond - circleRadiusSecond 243 | , centerXSecond + circleRadiusSecond, centerYSecond + circleRadiusSecond); 244 | 245 | canvas.drawArc(rectFHour, -90, currentDegreeHour, false, paintHour); 246 | canvas.drawArc(rectFMinute, -90, currentDegreeMinute, false, paintMinute); 247 | canvas.drawArc(rectFSecond, -90, currentDegreeSecond, false, paintSecond); 248 | 249 | 250 | //draw glow effect 251 | //画辉光效果 252 | paintDragButton.setColor(colorHour); 253 | canvas.drawCircle(dragButtonHourPosition[0], dragButtonHourPosition[1], strokeWidth / 2, paintDragButton); 254 | paintGlowEffect.setColor(colorHour); 255 | canvas.drawCircle(dragButtonHourPosition[0], dragButtonHourPosition[1], strokeWidth, paintGlowEffect); 256 | 257 | paintDragButton.setColor(colorMinute); 258 | canvas.drawCircle(dragButtonMinutePosition[0], dragButtonMinutePosition[1], strokeWidth / 2, paintDragButton); 259 | paintGlowEffect.setColor(colorMinute); 260 | canvas.drawCircle(dragButtonMinutePosition[0], dragButtonMinutePosition[1], strokeWidth, paintGlowEffect); 261 | 262 | paintDragButton.setColor(colorSecond); 263 | canvas.drawCircle(dragButtonSecondPosition[0], dragButtonSecondPosition[1], strokeWidth / 2, paintDragButton); 264 | paintGlowEffect.setColor(colorSecond); 265 | canvas.drawCircle(dragButtonSecondPosition[0], dragButtonSecondPosition[1], strokeWidth, paintGlowEffect); 266 | 267 | 268 | //draw letter "H""M""S",point(0,0) of text area is on the bottom-left of this area! 269 | //画"H""M""S"这三个字母,文字区域的(0,0)在左下角! 270 | getDisplayNumber(); 271 | Rect rect = new Rect(); 272 | 273 | paintNumber.setTextSize(70); 274 | paintNumber.setColor(colorHour); 275 | paintNumber.getTextBounds(displayNumberHour, 0, displayNumberHour.length(), rect); 276 | canvas.drawText(displayNumberHour, centerXHour - rect.width() / 2, centerYHour + rect.height() / 2, paintNumber); 277 | paintNumber.setTextSize(25); 278 | canvas.drawText("H", centerXHour + 30, centerYHour + 25, paintNumber); 279 | 280 | paintNumber.setTextSize(70); 281 | paintNumber.setColor(colorMinute); 282 | paintNumber.getTextBounds(displayNumberMinute, 0, displayNumberMinute.length(), rect); 283 | canvas.drawText(displayNumberMinute, centerXMinute - rect.width() / 2, centerYMinute + rect.height() / 2, paintNumber); 284 | paintNumber.setTextSize(25); 285 | canvas.drawText("M", centerXMinute + 50, centerYMinute + 25, paintNumber); 286 | 287 | paintNumber.setTextSize(70); 288 | paintNumber.setColor(colorSecond); 289 | paintNumber.getTextBounds(displayNumberSecond, 0, displayNumberSecond.length(), rect); 290 | canvas.drawText(displayNumberSecond, centerXSecond - rect.width() / 2, centerYSecond + rect.height() / 2, paintNumber); 291 | paintNumber.setTextSize(25); 292 | canvas.drawText("S", centerXSecond + 50, centerYSecond + 25, paintNumber); 293 | } 294 | 295 | 296 | //handle touch event 297 | // 298 | @Override 299 | public boolean onTouchEvent(MotionEvent event) { 300 | super.onTouchEvent(event); 301 | 302 | switch (event.getAction()) { 303 | //whether touch in the drag button or not 304 | //判断点击是否在dragButton内 305 | case MotionEvent.ACTION_DOWN: 306 | isInDragButton(event.getX(), event.getY()); 307 | break; 308 | 309 | //update coordination of dragButton 310 | //更新dragButton的位置 311 | case MotionEvent.ACTION_MOVE: 312 | if (!isStarted) { 313 | if (isInDragButton) { 314 | switch (whichDragButton) { 315 | case 1: 316 | currentDegreeHour = getDegree(event.getX(), event.getY(), centerXHour, centerYHour); 317 | updateTime(1); 318 | updateDragButtonPosition(1); 319 | invalidate(); 320 | break; 321 | case 2: 322 | currentDegreeMinute = getDegree(event.getX(), event.getY(), centerXMinute, centerYMinute); 323 | updateTime(2); 324 | updateDragButtonPosition(2); 325 | invalidate(); 326 | break; 327 | case 3: 328 | currentDegreeSecond = getDegree(event.getX(), event.getY(), centerXSecond, centerYSecond); 329 | updateTime(3); 330 | updateDragButtonPosition(3); 331 | invalidate(); 332 | break; 333 | } 334 | } 335 | } 336 | break; 337 | 338 | case MotionEvent.ACTION_UP: 339 | isInDragButton = false; 340 | break; 341 | } 342 | 343 | return true; 344 | } 345 | 346 | 347 | @Override 348 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 349 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 350 | 351 | int width = getDimension(DEFAULT_VIEW_WIDTH, widthMeasureSpec); 352 | int height = getDimension(width, heightMeasureSpec); 353 | 354 | viewWidth = width; 355 | viewHeight = height; 356 | 357 | setMeasuredDimension(width, height); 358 | } 359 | 360 | private int getDimension(int defaultDimension,int measureSpec){ 361 | 362 | int result; 363 | 364 | switch (MeasureSpec.getMode(measureSpec)){ 365 | case MeasureSpec.EXACTLY: 366 | result = MeasureSpec.getSize(measureSpec); 367 | break; 368 | case MeasureSpec.AT_MOST: 369 | result = Math.min(defaultDimension,MeasureSpec.getSize(measureSpec)); 370 | 371 | break; 372 | default: 373 | result = defaultDimension; 374 | break; 375 | } 376 | return result; 377 | } 378 | 379 | 380 | 381 | //update degree,depend on the path user dragging on the screen 382 | //根据用户在屏幕划过的轨迹更新角度 383 | private float getDegree(float eventX, float eventY, float centerX, float centerY) { 384 | 385 | // http://stackoverflow.com/questions/7926816/calculate-angle-of-touched-point-and-rotate-it-in-android 386 | // Math has defeated me once again.So sad... 387 | // 卧槽... 388 | double tx = eventX - centerX; 389 | double ty = eventY - centerY; 390 | double t_length = Math.sqrt(tx * tx + ty * ty); 391 | double a = Math.acos(ty / t_length); 392 | float degree = 180 - (float) Math.toDegrees(a); 393 | 394 | if (centerX > eventX) { 395 | degree = 180 + (float) Math.toDegrees(a); 396 | } 397 | 398 | return degree; 399 | } 400 | 401 | private void getDisplayNumber() { 402 | if (Integer.valueOf(displayNumberHour) != timeRemain.get(Calendar.HOUR_OF_DAY)) { 403 | displayNumberHour = timeRemain.get(Calendar.HOUR_OF_DAY) + ""; 404 | if (hourChangListener != null) { 405 | hourChangListener.onHourChange(timeRemain.get(Calendar.HOUR_OF_DAY)); 406 | } 407 | } 408 | if (Integer.valueOf(displayNumberMinute) != timeRemain.get(Calendar.MINUTE)) { 409 | displayNumberMinute = timeRemain.get(Calendar.MINUTE) + ""; 410 | if (minChangListener != null) { 411 | minChangListener.onMinChange(timeRemain.get(Calendar.MINUTE)); 412 | } 413 | } 414 | if (Integer.valueOf(displayNumberSecond) != timeRemain.get(Calendar.SECOND)) { 415 | displayNumberSecond = timeRemain.get(Calendar.SECOND) + ""; 416 | if (secondChangListener != null) { 417 | secondChangListener.onSecondChange(timeRemain.get(Calendar.SECOND)); 418 | } 419 | } 420 | 421 | } 422 | 423 | 424 | private void isInDragButton(float eventX, float eventY) { 425 | 426 | if (circleRadiusDragButton > Math.sqrt(Math.pow(eventX - dragButtonHourPosition[0], 2) 427 | + Math.pow(eventY - dragButtonHourPosition[1], 2))) { 428 | //在dragButtonHour中 429 | isInDragButton = true; 430 | whichDragButton = 1; 431 | } else if (circleRadiusDragButton > Math.sqrt(Math.pow(eventX - dragButtonMinutePosition[0], 2) 432 | + Math.pow(eventY - dragButtonMinutePosition[1], 2))) { 433 | //在dragButtonMinute中 434 | isInDragButton = true; 435 | whichDragButton = 2; 436 | } else if (circleRadiusDragButton > Math.sqrt(Math.pow(eventX - dragButtonSecondPosition[0], 2) 437 | + Math.pow(eventY - dragButtonSecondPosition[1], 2))) { 438 | //在dragButtonSecond中 439 | isInDragButton = true; 440 | whichDragButton = 3; 441 | } else { 442 | //不在 443 | isInDragButton = false; 444 | whichDragButton = 0; 445 | } 446 | } 447 | 448 | 449 | //degree depending on timeRemain 450 | //角度由剩余时间决定 451 | private void updateDegree() { 452 | 453 | currentDegreeHour = (float) ((timeRemain.get(Calendar.HOUR_OF_DAY) * 60 + timeRemain.get(Calendar.MINUTE)) / (6.0 * 60)) * 360; 454 | currentDegreeMinute = (float) ((timeRemain.get(Calendar.MINUTE) * 60 + timeRemain.get(Calendar.SECOND)) / (60.0 * 60)) * 360; 455 | currentDegreeSecond = (float) ((timeRemain.get(Calendar.SECOND) * 1000 + timeRemain.get(Calendar.MILLISECOND)) / (60.0 * 1000)) * 360; 456 | 457 | updateDragButtonPosition(0); 458 | 459 | } 460 | 461 | //update drag button position(glow effect area) 462 | //更新拖动按钮中心点(辉光效果区域) 463 | private void updateDragButtonPosition(int flag) { 464 | 465 | switch (flag) { 466 | case 0: 467 | dragButtonHourPosition[0] = (float) (centerXHour + circleRadiusHour * (Math.sin(Math.toRadians(currentDegreeHour)))); 468 | dragButtonHourPosition[1] = (float) (centerYHour - circleRadiusHour * (Math.cos(Math.toRadians(currentDegreeHour)))); 469 | 470 | dragButtonMinutePosition[0] = (float) (centerXMinute + circleRadiusMinute * Math.sin(Math.toRadians(currentDegreeMinute))); 471 | dragButtonMinutePosition[1] = (float) (centerYMinute - circleRadiusMinute * Math.cos(Math.toRadians(currentDegreeMinute))); 472 | 473 | dragButtonSecondPosition[0] = (float) (centerXSecond + circleRadiusSecond * Math.sin(Math.toRadians(currentDegreeSecond))); 474 | dragButtonSecondPosition[1] = (float) (centerYSecond - circleRadiusSecond * Math.cos(Math.toRadians(currentDegreeSecond))); 475 | break; 476 | case 1: 477 | dragButtonHourPosition[0] = (float) (centerXHour + circleRadiusHour * (Math.sin(Math.toRadians(currentDegreeHour)))); 478 | dragButtonHourPosition[1] = (float) (centerYHour - circleRadiusHour * (Math.cos(Math.toRadians(currentDegreeHour)))); 479 | break; 480 | case 2: 481 | dragButtonMinutePosition[0] = (float) (centerXMinute + circleRadiusMinute * Math.sin(Math.toRadians(currentDegreeMinute))); 482 | dragButtonMinutePosition[1] = (float) (centerYMinute - circleRadiusMinute * Math.cos(Math.toRadians(currentDegreeMinute))); 483 | break; 484 | case 3: 485 | dragButtonSecondPosition[0] = (float) (centerXSecond + circleRadiusSecond * Math.sin(Math.toRadians(currentDegreeSecond))); 486 | dragButtonSecondPosition[1] = (float) (centerYSecond - circleRadiusSecond * Math.cos(Math.toRadians(currentDegreeSecond))); 487 | break; 488 | } 489 | 490 | } 491 | 492 | 493 | //get the time from currentDegree and store it in timeStart and timeRemain 494 | //从当前的角度获取时间,保存到timeStart和timeRemain 495 | private void updateTime(int flag) { 496 | 497 | switch (flag) { 498 | case 0: 499 | timeStart.set(Calendar.HOUR_OF_DAY, (int) Math.floor(6 * currentDegreeHour / 360)); 500 | timeRemain.set(Calendar.HOUR_OF_DAY, (int) Math.floor(6 * currentDegreeHour / 360)); 501 | 502 | timeStart.set(Calendar.MINUTE, (int) Math.floor(60 * currentDegreeMinute / 360)); 503 | timeRemain.set(Calendar.MINUTE, (int) Math.floor(60 * currentDegreeMinute / 360)); 504 | 505 | timeStart.set(Calendar.SECOND, (int) Math.floor(60 * currentDegreeSecond / 360)); 506 | timeRemain.set(Calendar.SECOND, (int) Math.floor(60 * currentDegreeSecond / 360)); 507 | break; 508 | case 1: 509 | timeStart.set(Calendar.HOUR_OF_DAY, (int) Math.floor(6 * currentDegreeHour / 360)); 510 | timeRemain.set(Calendar.HOUR_OF_DAY, (int) Math.floor(6 * currentDegreeHour / 360)); 511 | break; 512 | case 2: 513 | timeStart.set(Calendar.MINUTE, (int) Math.floor(60 * currentDegreeMinute / 360)); 514 | timeRemain.set(Calendar.MINUTE, (int) Math.floor(60 * currentDegreeMinute / 360)); 515 | break; 516 | case 3: 517 | timeStart.set(Calendar.SECOND, (int) Math.floor(60 * currentDegreeSecond / 360)); 518 | timeRemain.set(Calendar.SECOND, (int) Math.floor(60 * currentDegreeSecond / 360)); 519 | break; 520 | } 521 | 522 | } 523 | 524 | 525 | //common Timer-TimerTask-Handler countdown solution 526 | //常见的Timer-TimerTask-Handler倒计时模式 527 | private Handler mHandler = new Handler() { 528 | @Override 529 | public void handleMessage(Message msg) { 530 | super.handleMessage(msg); 531 | 532 | switch (msg.what) { 533 | //countdown running 534 | //可以倒计时 535 | case 1: 536 | 537 | timeRemain.add(Calendar.MILLISECOND, -100); 538 | 539 | if (timeChangeListener != null) { 540 | timeChangeListener.onTimeChange(timeStart.getTimeInMillis(), timeRemain.getTimeInMillis()); 541 | } 542 | 543 | invalidate(); 544 | 545 | break; 546 | //countdown stop 547 | //时间为空,停止倒计时,提示用户 548 | case 2: 549 | isStarted = false; 550 | timerTask.cancel(); 551 | break; 552 | 553 | //StopWatch running 554 | case 11: 555 | timeRemain.add(Calendar.MILLISECOND, 100); 556 | if (timeChangeListener != null) { 557 | timeChangeListener.onTimeChange(timeStart.getTimeInMillis(), timeRemain.getTimeInMillis()); 558 | } 559 | invalidate(); 560 | break; 561 | //StopWatch stop 562 | //到达MAX TIME 563 | case 12: 564 | isStarted = false; 565 | timerTask.cancel(); 566 | break; 567 | } 568 | } 569 | }; 570 | 571 | Timer timer = new Timer(true); 572 | TimerTask timerTask; 573 | 574 | 575 | public boolean start() { 576 | if (model == Model.Timer) { 577 | if (!isTimeEmpty() && !isStarted) { 578 | 579 | timerTask = new TimerTask() { 580 | @Override 581 | public void run() { 582 | if (!isTimeEmpty()) { 583 | Message message = new Message(); 584 | message.what = 1; 585 | mHandler.sendMessage(message); 586 | } else { 587 | Message message = new Message(); 588 | message.what = 2; 589 | mHandler.sendMessage(message); 590 | } 591 | 592 | } 593 | }; 594 | 595 | timer.schedule(timerTask, 1000, 100); 596 | isStarted = true; 597 | 598 | if (timeChangeListener != null) { 599 | timeChangeListener.onTimerStart(timeStart.getTimeInMillis()); 600 | } 601 | } 602 | } else if (model == Model.StopWatch) { 603 | if (!isMaxTime() && !isStarted) { 604 | timerTask = new TimerTask() { 605 | @Override 606 | public void run() { 607 | if (!isMaxTime()) { 608 | Message message = new Message(); 609 | message.what = 11; 610 | mHandler.sendMessage(message); 611 | } else { 612 | Message message = new Message(); 613 | message.what = 12; 614 | mHandler.sendMessage(message); 615 | } 616 | } 617 | }; 618 | 619 | timer.schedule(timerTask, 1000, 100); 620 | isStarted = true; 621 | 622 | if (timeChangeListener != null) { 623 | timeChangeListener.onTimerStart(timeStart.getTimeInMillis()); 624 | } 625 | } 626 | } 627 | return isStarted; 628 | } 629 | 630 | private boolean isTimeEmpty() { 631 | if (timeRemain.get(Calendar.HOUR_OF_DAY) != 0 632 | || timeRemain.get(Calendar.MINUTE) != 0 633 | || timeRemain.get(Calendar.SECOND) != 0 634 | || timeRemain.get(Calendar.MILLISECOND) != 0) { 635 | return false; 636 | } else { 637 | return true; 638 | } 639 | } 640 | 641 | 642 | public long stop() { 643 | timerTask.cancel(); 644 | isStarted = false; 645 | 646 | if (timeChangeListener != null) { 647 | timeChangeListener.onTimeStop(timeStart.getTimeInMillis(), timeRemain.getTimeInMillis()); 648 | } 649 | 650 | return timeStart.getTimeInMillis() - timeRemain.getTimeInMillis(); 651 | } 652 | 653 | 654 | public Calendar getTimeStart() { 655 | return timeStart; 656 | } 657 | 658 | 659 | public Calendar getTimeRemaid() { 660 | return timeRemain; 661 | } 662 | 663 | 664 | public long getTimePass() { 665 | return timeStart.getTimeInMillis() - timeRemain.getTimeInMillis(); 666 | } 667 | 668 | 669 | public void setOnTimeChangeListener(OnTimeChangeListener listener) { 670 | if (listener != null) { 671 | timeChangeListener = listener; 672 | } 673 | } 674 | 675 | public void setMinChangListener(OnMinChangListener minChangListener) { 676 | this.minChangListener = minChangListener; 677 | } 678 | 679 | public void setSecondChangListener(OnSecondChangListener secondChangListener) { 680 | this.secondChangListener = secondChangListener; 681 | } 682 | 683 | public void setHourChangListener(OnHourChangListener hourChangListener) { 684 | this.hourChangListener = hourChangListener; 685 | } 686 | 687 | public void reset() { 688 | //先停止计时 689 | stop(); 690 | //初始化calendar 691 | isInitialized = false; 692 | invalidate(); 693 | } 694 | 695 | public boolean isMaxTime() { 696 | if (timeRemain.get(Calendar.HOUR_OF_DAY) == 5 697 | && timeRemain.get(Calendar.MINUTE) == 59 698 | && timeRemain.get(Calendar.SECOND) == 59) { 699 | return true; 700 | } else { 701 | return false; 702 | } 703 | } 704 | 705 | 706 | //listener 707 | public interface OnTimeChangeListener { 708 | public void onTimerStart(long timeStart); 709 | 710 | public void onTimeChange(long timeStart, long timeRemain); 711 | 712 | public void onTimeStop(long timeStart, long timeRemain); 713 | } 714 | 715 | public interface OnMinChangListener { 716 | public void onMinChange(int minute); 717 | } 718 | 719 | public interface OnHourChangListener { 720 | public void onHourChange(int hour); 721 | } 722 | 723 | public interface OnInitialFinishListener { 724 | public void onInitialFinishListener(); 725 | } 726 | 727 | public interface OnSecondChangListener { 728 | public void onSecondChange(int second); 729 | } 730 | 731 | public void setModel(Model model) { 732 | this.model = model; 733 | } 734 | 735 | /** 736 | * set default time 737 | * 738 | * @param h max 5 739 | * @param m max 59 740 | * @param s max 59 741 | */ 742 | public void setStartTime(final int h, final int m, final int s) throws NumberFormatException { 743 | initialFinishListener = new OnInitialFinishListener() { 744 | @Override 745 | public void onInitialFinishListener() { 746 | if (h > 5 || m > 59 || s > 69 || h < 0 || m < 0 | s < 0) { 747 | throw new NumberFormatException("hour must in [0-5], minute and second must in [0-59]"); 748 | } 749 | timeRemain.set(Calendar.HOUR_OF_DAY, h); 750 | timeRemain.set(Calendar.MINUTE, m); 751 | timeRemain.set(Calendar.SECOND, s); 752 | timeStart.set(Calendar.HOUR_OF_DAY, h); 753 | timeStart.set(Calendar.MINUTE, m); 754 | timeStart.set(Calendar.SECOND, s); 755 | updateDegree(); 756 | invalidate(); 757 | } 758 | }; 759 | } 760 | 761 | } 762 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 18 | 19 |