├── .gitignore ├── LICENSE ├── README.md ├── Screenshot.png ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── roo │ │ └── clockanimation │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── roo │ │ │ └── clockanimation │ │ │ ├── ClockDrawable.java │ │ │ ├── MainActivity.java │ │ │ ├── MyApp.java │ │ │ ├── PlusMinusButton.java │ │ │ └── PlusMinusLayout.java │ └── res │ │ ├── drawable │ │ └── ic_reload.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── content_main.xml │ │ ├── plus_minus_button.xml │ │ └── plus_minus_date_time_layout.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 │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── roo │ └── clockanimation │ └── ExampleUnitTest.java ├── build.gradle ├── demo.mp4 ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | *.jks 41 | app/libs/ 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClockDrawableAnimation 2 | 3 | Android animated clock Drawable. 4 | 5 | ### Video demo [here](demo.mp4) 6 | 7 | 8 | 9 | ### Screenshot 10 | 11 | [![ScreenShot](Screenshot.png)](demo.mp4) 12 | 13 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evelyne24/ClockDrawableAnimation/aa62e959ab9b0dc3d2b730cd5b53ce5b1b1f426d/Screenshot.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "23.0.3" 6 | defaultConfig { 7 | applicationId "roo.clockanimation" 8 | minSdkVersion 16 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | vectorDrawables.useSupportLibrary = true 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 26 | exclude group: 'com.android.support', module: 'support-annotations' 27 | }) 28 | compile 'com.android.support:appcompat-v7:24.0.0' 29 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha2' 30 | compile 'com.android.support:design:24.0.0' 31 | compile 'net.danlew:android.joda:2.9.4.1' 32 | testCompile 'junit:junit:4.12' 33 | } 34 | -------------------------------------------------------------------------------- /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/evelina/Library/Android/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/roo/clockanimation/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("roo.clockanimation", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/roo/clockanimation/ClockDrawable.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.animation.ValueAnimator.AnimatorUpdateListener; 7 | import android.content.res.Resources; 8 | import android.graphics.Canvas; 9 | import android.graphics.ColorFilter; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.Path.Direction; 13 | import android.graphics.PixelFormat; 14 | import android.graphics.Rect; 15 | import android.graphics.drawable.Animatable; 16 | import android.graphics.drawable.Drawable; 17 | import android.support.annotation.ColorRes; 18 | import android.view.animation.AccelerateDecelerateInterpolator; 19 | 20 | import org.joda.time.LocalDateTime; 21 | 22 | import static android.graphics.Paint.ANTI_ALIAS_FLAG; 23 | import static android.graphics.Paint.Cap.ROUND; 24 | import static android.graphics.Paint.Style.FILL; 25 | import static android.graphics.Paint.Style.STROKE; 26 | import static android.util.Log.d; 27 | import static org.joda.time.Minutes.minutesBetween; 28 | 29 | /** 30 | * Created by evelina on 15/07/2016. 31 | */ 32 | 33 | public class ClockDrawable extends Drawable implements Animatable { 34 | 35 | private final static int ANIMATION_DURATION = 500; 36 | 37 | private static final @ColorRes int FACE_COLOR = android.R.color.white; 38 | private static final @ColorRes int RIM_COLOR = R.color.colorAccent; 39 | 40 | private final Paint facePaint; 41 | private final Paint rimPaint; 42 | private final ValueAnimator minAnimator; 43 | private final ValueAnimator hourAnimator; 44 | 45 | private float rimRadius; 46 | private float faceRadius; 47 | private float screwRadius; 48 | 49 | private final Path hourHandPath; 50 | private final Path minuteHandPath; 51 | 52 | private float remainingHourRotation = 0f; 53 | private float remainingMinRotation = 0f; 54 | 55 | private float targetHourRotation = 0f; 56 | private float targetMinRotation = 0f; 57 | 58 | private float currentHourRotation = 0f; 59 | private float currentMinRotation; 60 | 61 | private boolean hourAnimInterrupted; 62 | private boolean minAnimInterrupted; 63 | 64 | private LocalDateTime previousTime; 65 | 66 | private boolean animateDays = true; 67 | 68 | public ClockDrawable(Resources resources) { 69 | facePaint = new Paint(ANTI_ALIAS_FLAG); 70 | facePaint.setColor(resources.getColor(FACE_COLOR)); 71 | facePaint.setStyle(FILL); 72 | 73 | rimPaint = new Paint(ANTI_ALIAS_FLAG); 74 | rimPaint.setColor(resources.getColor(RIM_COLOR)); 75 | rimPaint.setStyle(STROKE); 76 | rimPaint.setStrokeCap(ROUND); 77 | rimPaint.setStrokeWidth(resources.getDimension(R.dimen.clock_stroke_width)); 78 | 79 | hourHandPath = new Path(); 80 | minuteHandPath = new Path(); 81 | 82 | hourAnimator = ValueAnimator.ofFloat(0, 0); 83 | hourAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 84 | hourAnimator.setDuration(ANIMATION_DURATION); 85 | hourAnimator.addUpdateListener(new AnimatorUpdateListener() { 86 | @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { 87 | float fraction = (float) valueAnimator.getAnimatedValue(); 88 | //d("ANIM", "Hfraction = " + fraction + ", remaining hour rotation = " + remainingHourRotation); 89 | remainingHourRotation = targetHourRotation - fraction; 90 | currentHourRotation = fraction; 91 | invalidateSelf(); 92 | } 93 | }); 94 | hourAnimator.addListener(new AnimatorListenerAdapter() { 95 | @Override public void onAnimationEnd(Animator animation) { 96 | if (!hourAnimInterrupted) { 97 | remainingHourRotation = 0f; 98 | } 99 | //i("ANIM", "END! remaining hour rotation = " + remainingHourRotation); 100 | } 101 | }); 102 | 103 | 104 | minAnimator = ValueAnimator.ofFloat(0, 0); 105 | minAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 106 | minAnimator.setDuration(ANIMATION_DURATION); 107 | minAnimator.addUpdateListener(new AnimatorUpdateListener() { 108 | @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { 109 | float fraction = (float) valueAnimator.getAnimatedValue(); 110 | //d("ANIM", "Mfraction = " + fraction + ", remaining minute rotation = " + remainingMinRotation); 111 | remainingMinRotation = targetMinRotation - fraction; 112 | currentMinRotation = fraction; 113 | invalidateSelf(); 114 | } 115 | }); 116 | minAnimator.addListener(new AnimatorListenerAdapter() { 117 | @Override public void onAnimationEnd(Animator animation) { 118 | if (!minAnimInterrupted) { 119 | remainingMinRotation = 0f; 120 | } 121 | //i("ANIM", "END! remaining minute rotation = " + remainingMinRotation); 122 | } 123 | }); 124 | 125 | previousTime = LocalDateTime.now().withTime(0, 0, 0, 0); 126 | } 127 | 128 | @Override 129 | protected void onBoundsChange(Rect bounds) { 130 | super.onBoundsChange(bounds); 131 | 132 | rimRadius = Math.min(bounds.width(), bounds.height()) / 2f - rimPaint.getStrokeWidth(); 133 | faceRadius = rimRadius - rimPaint.getStrokeWidth(); 134 | screwRadius = rimPaint.getStrokeWidth() * 2; 135 | float hourHandLength = (float) (0.5 * faceRadius); 136 | float minuteHandLength = (float) (0.7 * faceRadius); 137 | float top = bounds.centerY() - screwRadius; 138 | 139 | hourHandPath.reset(); 140 | hourHandPath.moveTo(bounds.centerX(), bounds.centerY()); 141 | hourHandPath.addRect(bounds.centerX(), top, bounds.centerX(), top - hourHandLength, Direction.CCW); 142 | hourHandPath.close(); 143 | 144 | minuteHandPath.reset(); 145 | minuteHandPath.moveTo(bounds.centerX(), bounds.centerY()); 146 | minuteHandPath.addRect(bounds.centerX(), top, bounds.centerX(), top - minuteHandLength, Direction.CCW); 147 | minuteHandPath.close(); 148 | } 149 | 150 | @Override public void draw(Canvas canvas) { 151 | Rect bounds = getBounds(); 152 | 153 | // draw the outer rim of the clock 154 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), rimRadius, rimPaint); 155 | // draw the face of the clock 156 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), faceRadius, facePaint); 157 | // draw the little rim in the middle of the clock 158 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), screwRadius, rimPaint); 159 | 160 | int saveCount = canvas.save(); 161 | canvas.rotate(currentHourRotation, bounds.centerX(), bounds.centerY()); 162 | // draw hour hand 163 | canvas.drawPath(hourHandPath, rimPaint); 164 | canvas.restoreToCount(saveCount); 165 | 166 | saveCount = canvas.save(); 167 | canvas.rotate(currentMinRotation, bounds.centerX(), bounds.centerY()); 168 | // draw minute hand 169 | canvas.drawPath(minuteHandPath, rimPaint); 170 | canvas.restoreToCount(saveCount); 171 | } 172 | 173 | @Override public void setAlpha(int alpha) { 174 | rimPaint.setAlpha(alpha); 175 | facePaint.setAlpha(alpha); 176 | invalidateSelf(); 177 | } 178 | 179 | @Override public void setColorFilter(ColorFilter colorFilter) { 180 | rimPaint.setColorFilter(colorFilter); 181 | invalidateSelf(); 182 | } 183 | 184 | @Override public int getOpacity() { 185 | return PixelFormat.OPAQUE; 186 | } 187 | 188 | @Override public void start() { 189 | hourAnimInterrupted = false; 190 | minAnimInterrupted = false; 191 | hourAnimator.start(); 192 | minAnimator.start(); 193 | } 194 | 195 | public void setAnimateDays(boolean animateDays) { 196 | this.animateDays = animateDays; 197 | } 198 | 199 | public void start(LocalDateTime newTime) { 200 | int minDiff = getMinsBetween(previousTime, newTime); 201 | // 60min ... 360grade 202 | // minDif .. minDelta 203 | float minDeltaRotation = ((float) minDiff * 360f) / 60f; 204 | // 720min ... 360grade = 12h ... 360grade 205 | // minDif ... hourDelta 206 | float hourDeltaRotation = ((float) minDiff * 360f) / 720f; 207 | 208 | remainingMinRotation += minDeltaRotation; 209 | remainingHourRotation += hourDeltaRotation; 210 | 211 | d("ANIM", "current hour rotation = " + currentHourRotation + ", current min rotation = " + currentMinRotation); 212 | 213 | if (isRunning()) { 214 | stop(); 215 | } 216 | 217 | targetHourRotation = currentHourRotation + remainingHourRotation; 218 | hourAnimator.setFloatValues(currentHourRotation, targetHourRotation); 219 | 220 | targetMinRotation = currentMinRotation + remainingMinRotation; 221 | minAnimator.setFloatValues(currentMinRotation, targetMinRotation); 222 | 223 | start(); 224 | 225 | previousTime = newTime; 226 | } 227 | 228 | @Override public void stop() { 229 | hourAnimInterrupted = true; 230 | minAnimInterrupted = true; 231 | hourAnimator.cancel(); 232 | minAnimator.cancel(); 233 | } 234 | 235 | @Override public boolean isRunning() { 236 | return hourAnimator.isRunning() || minAnimator.isRunning(); 237 | } 238 | 239 | private int getMinsBetween(LocalDateTime t1, LocalDateTime t2) { 240 | if(animateDays) { 241 | return minutesBetween(t1, t2).getMinutes(); 242 | } 243 | return minutesBetween(t1, t2.withDate(t1.getYear(), t1.getMonthOfYear(), t1.getDayOfMonth())).getMinutes(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /app/src/main/java/roo/clockanimation/MainActivity.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.Toolbar; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.view.View; 9 | import android.view.View.OnClickListener; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import org.joda.time.DateTimeZone; 14 | import org.joda.time.LocalDateTime; 15 | import org.joda.time.format.DateTimeFormat; 16 | import org.joda.time.format.DateTimeFormatter; 17 | 18 | import java.util.Locale; 19 | 20 | import roo.clockanimation.PlusMinusLayout.OnChangeListener; 21 | 22 | public class MainActivity extends AppCompatActivity { 23 | 24 | private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("dd MMM HH:mm") 25 | .withLocale(Locale.getDefault()) 26 | .withZone(DateTimeZone.getDefault()); 27 | 28 | private final LocalDateTime now = LocalDateTime.now().withTime(0, 0, 0, 0); 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 35 | 36 | final ClockDrawable clockDrawable = new ClockDrawable(getResources()); 37 | clockDrawable.setAnimateDays(false); 38 | ImageView imageView = (ImageView) findViewById(R.id.image); 39 | imageView.setImageDrawable(clockDrawable); 40 | 41 | final TextView dateTimeView = (TextView) findViewById(R.id.dateTime); 42 | dateTimeView.setText(DTF.print(now)); 43 | 44 | final PlusMinusLayout plusMinusLayout = (PlusMinusLayout) findViewById(R.id.test); 45 | plusMinusLayout.setListener(new OnChangeListener() { 46 | @Override public void onChange(int days, int hours, int minutes) { 47 | LocalDateTime current = now.plusDays(days).plusHours(hours).plusMinutes(minutes); 48 | dateTimeView.setText(DTF.print(current)); 49 | clockDrawable.start(current); 50 | } 51 | }); 52 | 53 | final View reset = findViewById(R.id.reset); 54 | reset.setOnClickListener(new OnClickListener() { 55 | @Override public void onClick(View view) { 56 | plusMinusLayout.reset(); 57 | dateTimeView.setText(DTF.print(now)); 58 | } 59 | }); 60 | 61 | } 62 | 63 | @Override 64 | public boolean onCreateOptionsMenu(Menu menu) { 65 | // Inflate the menu; this adds items to the action bar if it is present. 66 | getMenuInflater().inflate(R.menu.menu_main, menu); 67 | return true; 68 | } 69 | 70 | @Override 71 | public boolean onOptionsItemSelected(MenuItem item) { 72 | // Handle action bar item clicks here. The action bar will 73 | // automatically handle clicks on the Home/Up button, so long 74 | // as you specify a parent activity in AndroidManifest.xml. 75 | int id = item.getItemId(); 76 | 77 | //noinspection SimplifiableIfStatement 78 | if (id == R.id.action_settings) { 79 | return true; 80 | } 81 | 82 | return super.onOptionsItemSelected(item); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/roo/clockanimation/MyApp.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.app.Application; 4 | 5 | import net.danlew.android.joda.JodaTimeAndroid; 6 | 7 | public class MyApp extends Application { 8 | @Override 9 | public void onCreate() { 10 | super.onCreate(); 11 | JodaTimeAndroid.init(this); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/roo/clockanimation/PlusMinusButton.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.content.Context; 4 | import android.os.Build.VERSION_CODES; 5 | import android.support.annotation.RequiresApi; 6 | import android.util.AttributeSet; 7 | import android.view.Gravity; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.View.OnClickListener; 11 | import android.widget.LinearLayout; 12 | 13 | /** 14 | * Created by evelina on 15/07/2016. 15 | */ 16 | 17 | public class PlusMinusButton extends LinearLayout implements OnClickListener { 18 | 19 | public interface OnPlusMinusClickListener { 20 | void onPlusClicked(View view); 21 | 22 | void onMinusClicked(View view); 23 | } 24 | 25 | private View plusButton; 26 | private View minusButton; 27 | private OnPlusMinusClickListener listener; 28 | 29 | public PlusMinusButton(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | init(context, attrs); 32 | } 33 | 34 | public PlusMinusButton(Context context, AttributeSet attrs, int defStyleAttr) { 35 | super(context, attrs, defStyleAttr); 36 | init(context, attrs); 37 | } 38 | 39 | @RequiresApi(api = VERSION_CODES.LOLLIPOP) 40 | public PlusMinusButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 41 | super(context, attrs, defStyleAttr, defStyleRes); 42 | init(context, attrs); 43 | } 44 | 45 | private void init(Context context, AttributeSet attributeSet) { 46 | setOrientation(VERTICAL); 47 | setGravity(Gravity.CENTER); 48 | LayoutInflater.from(context).inflate(R.layout.plus_minus_button, this, true); 49 | setBackgroundResource(R.drawable.abc_btn_colored_material); 50 | plusButton = findViewById(R.id.plus); 51 | minusButton = findViewById(R.id.minus); 52 | 53 | plusButton.setOnClickListener(this); 54 | minusButton.setOnClickListener(this); 55 | } 56 | 57 | public void setListener(OnPlusMinusClickListener listener) { 58 | this.listener = listener; 59 | } 60 | 61 | 62 | @Override public void onClick(View view) { 63 | if (listener != null) { 64 | if (view == plusButton) { 65 | listener.onPlusClicked(this); 66 | } else if (view == minusButton) { 67 | listener.onMinusClicked(this); 68 | } 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/roo/clockanimation/PlusMinusLayout.java: -------------------------------------------------------------------------------- 1 | package roo.clockanimation; 2 | 3 | import android.content.Context; 4 | import android.os.Build.VERSION_CODES; 5 | import android.support.annotation.RequiresApi; 6 | import android.text.TextUtils; 7 | import android.util.AttributeSet; 8 | import android.view.Gravity; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.EditText; 12 | import android.widget.LinearLayout; 13 | 14 | import roo.clockanimation.PlusMinusButton.OnPlusMinusClickListener; 15 | 16 | import static android.view.LayoutInflater.from; 17 | import static java.lang.Integer.parseInt; 18 | 19 | /** 20 | * Created by evelina on 15/07/2016. 21 | */ 22 | 23 | public class PlusMinusLayout extends LinearLayout implements OnPlusMinusClickListener { 24 | 25 | public interface OnChangeListener { 26 | void onChange(int days, int hours, int minutes); 27 | } 28 | 29 | private EditText dayText; 30 | private EditText hourText; 31 | private EditText minuteText; 32 | 33 | private PlusMinusButton plusMinusDays; 34 | private PlusMinusButton plusMinusHours; 35 | private PlusMinusButton plusMinusMinutes; 36 | 37 | private OnChangeListener listener; 38 | 39 | public PlusMinusLayout(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | init(context, attrs); 42 | } 43 | 44 | public PlusMinusLayout(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | init(context, attrs); 47 | } 48 | 49 | @RequiresApi(api = VERSION_CODES.LOLLIPOP) 50 | public PlusMinusLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 51 | super(context, attrs, defStyleAttr, defStyleRes); 52 | init(context, attrs); 53 | } 54 | 55 | private void init(Context context, AttributeSet attributeSet) { 56 | setOrientation(HORIZONTAL); 57 | setGravity(Gravity.CENTER); 58 | from(context).inflate(R.layout.plus_minus_date_time_layout, this, true); 59 | setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 60 | setFocusable(true); 61 | setFocusableInTouchMode(true); 62 | requestFocus(); 63 | 64 | dayText = (EditText) findViewById(R.id.day); 65 | hourText = (EditText) findViewById(R.id.hour); 66 | minuteText = (EditText) findViewById(R.id.minute); 67 | 68 | init(dayText); 69 | init(hourText); 70 | init(minuteText); 71 | 72 | plusMinusDays = (PlusMinusButton) findViewById(R.id.dayPlusMinus); 73 | plusMinusHours = (PlusMinusButton) findViewById(R.id.hourPlusMinus); 74 | plusMinusMinutes = (PlusMinusButton) findViewById(R.id.minPlusMinus); 75 | 76 | plusMinusDays.setListener(this); 77 | plusMinusHours.setListener(this); 78 | plusMinusMinutes.setListener(this); 79 | } 80 | 81 | @Override public void onPlusClicked(View view) { 82 | if (view == plusMinusDays) { 83 | increase(dayText); 84 | } else if (view == plusMinusHours) { 85 | increase(hourText); 86 | } else if (view == plusMinusMinutes) { 87 | increase(minuteText); 88 | } 89 | } 90 | 91 | @Override public void onMinusClicked(View view) { 92 | if (view == plusMinusDays) { 93 | decrease(dayText); 94 | } else if (view == plusMinusHours) { 95 | decrease(hourText); 96 | } else if (view == plusMinusMinutes) { 97 | decrease(minuteText); 98 | } 99 | } 100 | 101 | public int getDays() { 102 | return parseValue(dayText); 103 | } 104 | 105 | public int getHours() { 106 | return parseValue(hourText); 107 | } 108 | 109 | public int getMinutes() { 110 | return parseValue(minuteText); 111 | } 112 | 113 | public void setListener(OnChangeListener listener) { 114 | this.listener = listener; 115 | } 116 | 117 | public void reset() { 118 | reset(dayText); 119 | reset(hourText); 120 | reset(minuteText); 121 | notifyChangeListener(); 122 | } 123 | 124 | private void init(EditText editText) { 125 | setText(editText, "0"); 126 | editText.setOnFocusChangeListener(new CustomFocusListener(editText)); 127 | } 128 | 129 | private void reset(EditText editText) { 130 | editText.setOnFocusChangeListener(null); 131 | init(editText); 132 | } 133 | 134 | private void notifyChangeListener() { 135 | if (listener != null) { 136 | listener.onChange(getDays(), getHours(), getMinutes()); 137 | } 138 | } 139 | 140 | private void increase(EditText editText) { 141 | setText(editText, change(editText, 1)); 142 | notifyChangeListener(); 143 | } 144 | 145 | private void decrease(EditText editText) { 146 | setText(editText, change(editText, -1)); 147 | notifyChangeListener(); 148 | } 149 | 150 | private static String change(EditText editText, int value) { 151 | return String.valueOf(parseValue(editText) + value); 152 | } 153 | 154 | private static int parseValue(EditText editText) { 155 | return isEmpty(editText) ? 0 : parseInt(getText(editText)); 156 | } 157 | 158 | private static String getText(EditText editText) { 159 | return editText.getText().toString(); 160 | } 161 | 162 | private static boolean isEmpty(EditText editText) { 163 | return TextUtils.isEmpty(getText(editText)); 164 | } 165 | 166 | private static boolean isZero(EditText editText) { 167 | return "0".equals(getText(editText)); 168 | } 169 | 170 | private static void setText(EditText editText, String text) { 171 | editText.setText(text); 172 | editText.setSelection(getText(editText).length()); 173 | } 174 | 175 | 176 | private class CustomFocusListener implements OnFocusChangeListener { 177 | private final EditText editText; 178 | 179 | public CustomFocusListener(EditText editText) { 180 | this.editText = editText; 181 | } 182 | 183 | @Override public void onFocusChange(View view, boolean hasFocus) { 184 | if (editText != view) { 185 | return; 186 | } 187 | 188 | if (hasFocus && isZero(editText)) { 189 | setText(editText, ""); 190 | 191 | } else if (!hasFocus) { 192 | if (isEmpty(editText)) { 193 | setText(editText, "0"); 194 | } 195 | notifyChangeListener(); 196 | } 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reload.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 22 | 23 | 31 | 32 | 41 | 42 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/plus_minus_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |