├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cn │ │ └── iwgang │ │ └── countuptimedemo │ │ ├── ListViewActivity.java │ │ └── MainActivity.java │ └── res │ ├── drawable │ └── sel_btn.xml │ ├── layout │ ├── activity_listview.xml │ ├── activity_main.xml │ └── list_item.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 ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cn │ │ └── iwgang │ │ └── countuptime │ │ ├── CountupView.java │ │ ├── CustomCountUpTimer.java │ │ └── LogUtils.java │ └── res │ └── values │ ├── attrs.xml │ ├── dimens.xml │ └── strings.xml ├── screenshot ├── androidCountUpTimeView.gif └── screenshot2.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | # files for the dex VM 5 | *.dex 6 | # Java class files 7 | *.class 8 | # Android generated files 9 | bin/ 10 | gen/ 11 | # Local configuration file (sdk path, etc) 12 | local.properties 13 | 14 | # Eclipse project files 15 | .classpath 16 | .project 17 | .settings 18 | .factorypath 19 | .apt_generated 20 | # Maven related 21 | target/ 22 | # Checkstyle 23 | .checkstyle 24 | # Android Lint 25 | lint.xml 26 | 27 | # IntelliJ 28 | .idea 29 | *.iml 30 | *.ipr 31 | *.iws 32 | classes 33 | gen-external-apklibs 34 | out/ 35 | 36 | # Gradle 37 | .gradle 38 | build 39 | */build/ 40 | */bin/ 41 | */gen/ 42 | */out/ 43 | # Misc. 44 | *.DS_Store 45 | *.jar.properties 46 | cache.properties 47 | cache.properties.lock 48 | # Crashlytics 49 | com_crashlytics_export_strings.xml 50 | 51 | .svn/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 iWgang 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CountupView 2 | Android 正计时控件,使用Canvas绘制,支持多种样式 3 | 4 | 修改自https://github.com/iwgang/CountdownView 5 | 支持秒计时和10毫秒计时 6 | 7 | SystemClock.elapsedRealtime()计算时间,即使放在listview或者recyclerview中被回收了始终也不会停止。 8 | 具体参考如下: 9 | ``` 10 | @Override protected void onDetachedFromWindow() { 11 | LogUtils.d("被回收",""); 12 | stop();//现在不会导致bug 13 | super.onDetachedFromWindow(); 14 | } 15 | 16 | @Override protected void onAttachedToWindow() { 17 | LogUtils.i("视图创建",""); 18 | if (null != mCustomCountUpTimer && mCustomCountUpTimer.isStarted()) { 19 | mCustomCountUpTimer.recycledstart(); 20 | LogUtils.i("时钟复用",""); 21 | } 22 | super.onAttachedToWindow(); 23 | } 24 | ``` 25 | 26 | ### screenshot 27 | ![](https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/master/screenshot/androidCountUpTimeView.gif) 28 | ![](https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/master/screenshot/screenshot2.png) 29 | 30 | ### code 31 | ``` 32 | CountupView mCvCountupView = (CountupView)findViewById(R.id.cv_CountupViewTest1); 33 | mCvCountupView.start(995550000); // 毫秒 34 | ``` 35 | 36 | ### layout 37 | ``` xml 38 | 57 | ``` 58 | 59 | ### customization 60 | 参数 | 类型 | 默认值 61 | --- | --- | --- 62 | isHideTimeBackground | boolean | true 63 | timeBgColor | color | #444444 64 | timeBgSize | dimension | timeSize + 2dp * 4 65 | timeBgRadius | dimension | 0 66 | isShowTimeBgDivisionLine | boolean | true 67 | timeBgDivisionLineColor | color | #30FFFFFF 68 | timeBgDivisionLineSize | dimension | 0.5dp 69 | timeTextSize | dimension | 12sp | 70 | timeTextColor | color | #000000 71 | isTimeTextBold | boolean | false 72 | isShowDay | boolean | 自动显示 (天 > 1 显示, = 0 隐藏) 73 | isShowHour | boolean | 自动显示 (小时 > 1 显示, = 0 隐藏) 74 | isShowMinute | boolean | true 75 | isShowSecond | boolean | true 76 | isShowMillisecond | boolean | false 77 | suffixTextSize | dimension | 12sp 78 | suffixTextColor | color | #000000 79 | isSuffixTextBold | boolean | false 80 | suffixGravity | 'top' or 'center' or 'bottom' | 'center' 81 | suffix | string | ':' 82 | suffixDay | string | null 83 | suffixHour | string | null 84 | suffixMinute | string | null 85 | suffixSecond | string | null 86 | suffixMillisecond | string | null 87 | suffixLRMargin | dimension | left 3dp right 3dp 88 | suffixDayLeftMargin | dimension | 0 89 | suffixDayRightMargin | dimension | 0 90 | suffixHourLeftMargin | dimension | 0 91 | suffixHourRightMargin | dimension | 0 92 | suffixMinuteLeftMargin | dimension | 0 93 | suffixMinuteRightMargin | dimension | 0 94 | suffixSecondLeftMargin | dimension | 0 95 | suffixSecondRightMargin | dimension | 0 96 | suffixMillisecondLeftMargin | dimension | 0 97 | 98 | 99 | 100 | ## 作者联系方式 101 | QQ:1196681436 102 | Weibo:http://www.weibo.com/u/1693069642 103 | 104 | 欢迎提出意见,提交代码。 105 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "cn.iwgang.countuptimedemo" 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.3.0' 25 | compile 'com.github.iwgang:familiarrecyclerview:1.3.0' 26 | compile project(':library') 27 | } 28 | -------------------------------------------------------------------------------- /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/apple/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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/cn/iwgang/countuptimedemo/ListViewActivity.java: -------------------------------------------------------------------------------- 1 | package cn.iwgang.countuptimedemo; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.util.SparseArray; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.BaseAdapter; 12 | import android.widget.ListView; 13 | import android.widget.TextView; 14 | 15 | import cn.iwgang.countuptime.CountupView; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Timer; 19 | import java.util.TimerTask; 20 | 21 | /* 22 | 此类模拟在ListView中使用倒计时 23 | */ 24 | public class ListViewActivity extends AppCompatActivity { 25 | private List mDataList; 26 | private MyListAdapter mMyAdapter; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_listview); 32 | 33 | initData(); 34 | 35 | ListView lvList = (ListView) findViewById(R.id.lv_list); 36 | lvList.setAdapter(mMyAdapter = new MyListAdapter(this, mDataList)); 37 | } 38 | 39 | private void initData() { 40 | mDataList = new ArrayList<>(); 41 | 42 | for (int i = 1; i < 20; i++) { 43 | mDataList.add(new ItemInfo(1000 + i, "ListView_测试标题_" + i, i * 20 * 1000)); 44 | } 45 | 46 | // 校对倒计时 47 | long curTime = System.currentTimeMillis(); 48 | for (ItemInfo itemInfo : mDataList) { 49 | itemInfo.setEndTime(curTime + itemInfo.getCountdown()); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onResume() { 55 | super.onResume(); 56 | if (null != mMyAdapter) { 57 | mMyAdapter.startRefreshTime(); 58 | } 59 | } 60 | 61 | @Override 62 | protected void onPause() { 63 | super.onPause(); 64 | if (null != mMyAdapter) { 65 | mMyAdapter.cancelRefreshTime(); 66 | } 67 | } 68 | 69 | @Override 70 | public void onDestroy() { 71 | super.onDestroy(); 72 | if (null != mMyAdapter) { 73 | mMyAdapter.cancelRefreshTime(); 74 | } 75 | } 76 | 77 | static class MyListAdapter extends BaseAdapter { 78 | private Context mContext; 79 | private List mDatas; 80 | private final SparseArray mCountdownVHList; 81 | private Handler mHandler = new Handler(); 82 | private Timer mTimer; 83 | private boolean isCancel = true; 84 | 85 | public MyListAdapter(Context context, List datas) { 86 | this.mContext = context; 87 | this.mDatas = datas; 88 | mCountdownVHList = new SparseArray<>(); 89 | startRefreshTime(); 90 | } 91 | 92 | public void startRefreshTime() { 93 | if (!isCancel) return; 94 | 95 | if (null != mTimer) { 96 | mTimer.cancel(); 97 | } 98 | 99 | isCancel = false; 100 | mTimer = new Timer(); 101 | mTimer.schedule(new TimerTask() { 102 | @Override 103 | public void run() { 104 | mHandler.post(mRefreshTimeRunnable); 105 | } 106 | }, 0, 10); 107 | } 108 | 109 | public void cancelRefreshTime() { 110 | isCancel = true; 111 | if (null != mTimer) { 112 | mTimer.cancel(); 113 | } 114 | mHandler.removeCallbacks(mRefreshTimeRunnable); 115 | } 116 | 117 | @Override 118 | public int getCount() { 119 | return mDatas.size(); 120 | } 121 | 122 | @Override 123 | public Object getItem(int position) { 124 | return mDatas.get(position); 125 | } 126 | 127 | @Override 128 | public long getItemId(int position) { 129 | return position; 130 | } 131 | 132 | @Override 133 | public View getView(int position, View convertView, ViewGroup parent) { 134 | MyViewHolder holder; 135 | if (convertView == null) { 136 | convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false); 137 | holder = new MyViewHolder(); 138 | holder.initView(convertView); 139 | convertView.setTag(holder); 140 | } else { 141 | holder = (MyViewHolder) convertView.getTag(); 142 | } 143 | 144 | ItemInfo curItemInfo = mDatas.get(position); 145 | holder.bindData(curItemInfo); 146 | 147 | // 处理倒计时 148 | if (curItemInfo.getCountdown() > 0) { 149 | synchronized (mCountdownVHList) { 150 | mCountdownVHList.put(curItemInfo.getId(), holder); 151 | } 152 | } 153 | 154 | return convertView; 155 | } 156 | 157 | private Runnable mRefreshTimeRunnable = new Runnable() { 158 | @Override 159 | public void run() { 160 | if (mCountdownVHList.size() == 0) return; 161 | 162 | synchronized (mCountdownVHList) { 163 | long currentTime = System.currentTimeMillis(); 164 | int key; 165 | for (int i = 0; i < mCountdownVHList.size(); i++) { 166 | key = mCountdownVHList.keyAt(i); 167 | MyViewHolder curMyViewHolder = mCountdownVHList.get(key); 168 | if (currentTime >= curMyViewHolder.getBean().getEndTime()) { 169 | // 倒计时结束 170 | curMyViewHolder.getBean().setCountdown(0); 171 | mCountdownVHList.remove(key); 172 | notifyDataSetChanged(); 173 | } else { 174 | curMyViewHolder.refreshTime(currentTime); 175 | } 176 | 177 | } 178 | } 179 | } 180 | }; 181 | 182 | static class MyViewHolder { 183 | private TextView mTvTitle; 184 | private CountupView mCvCountdownView; 185 | private ItemInfo mItemInfo; 186 | 187 | public void initView(View convertView) { 188 | mTvTitle = (TextView) convertView.findViewById(R.id.tv_title); 189 | mCvCountdownView = (CountupView) convertView.findViewById(R.id.cv_countdownView); 190 | } 191 | 192 | public void bindData(ItemInfo itemInfo) { 193 | mItemInfo = itemInfo; 194 | 195 | if (itemInfo.getCountdown() > 0) { 196 | refreshTime(System.currentTimeMillis()); 197 | } else { 198 | mCvCountdownView.allShowZero(); 199 | } 200 | 201 | mTvTitle.setText(itemInfo.getTitle()); 202 | } 203 | 204 | public void refreshTime(long curTimeMillis) { 205 | if (null == mItemInfo || mItemInfo.getCountdown() <= 0) return; 206 | 207 | mCvCountdownView.updateShow(mItemInfo.getEndTime() - curTimeMillis); 208 | } 209 | 210 | public ItemInfo getBean() { 211 | return mItemInfo; 212 | } 213 | } 214 | } 215 | 216 | static class ItemInfo { 217 | private int id; 218 | private String title; 219 | private long countdown; 220 | /* 221 | 根据服务器返回的countdown换算成手机对应的开奖时间 (毫秒) 222 | [正常情况最好由服务器返回countdown字段,然后客户端再校对成该手机对应的时间,不然误差很大] 223 | */ 224 | private long endTime; 225 | 226 | public ItemInfo(int id, String title, long countdown) { 227 | this.id = id; 228 | this.title = title; 229 | this.countdown = countdown; 230 | } 231 | 232 | public int getId() { 233 | return id; 234 | } 235 | 236 | public void setId(int id) { 237 | this.id = id; 238 | } 239 | 240 | public String getTitle() { 241 | return title; 242 | } 243 | 244 | public void setTitle(String title) { 245 | this.title = title; 246 | } 247 | 248 | public long getCountdown() { 249 | return countdown; 250 | } 251 | 252 | public void setCountdown(long countdown) { 253 | this.countdown = countdown; 254 | } 255 | 256 | public long getEndTime() { 257 | return endTime; 258 | } 259 | 260 | public void setEndTime(long endTime) { 261 | this.endTime = endTime; 262 | } 263 | } 264 | 265 | } 266 | 267 | 268 | -------------------------------------------------------------------------------- /app/src/main/java/cn/iwgang/countuptimedemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.iwgang.countuptimedemo; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | import cn.iwgang.countuptime.CountupView; 7 | 8 | public class MainActivity extends AppCompatActivity implements CountupView.OnCountdownEndListener { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_main); 14 | 15 | CountupView mCvCountupViewTest1 = (CountupView)findViewById(R.id.cv_CountupViewTest1); 16 | mCvCountupViewTest1.setTag("test1"); 17 | long time1 = (long)5 * 60 * 60 * 1000; 18 | mCvCountupViewTest1.start(time1); 19 | 20 | CountupView mCvCountupViewTest2 = (CountupView)findViewById(R.id.cv_CountupViewTest2); 21 | mCvCountupViewTest1.setTag("test2"); 22 | long time2 = (long)30 * 60 * 1000; 23 | mCvCountupViewTest2.start(time2); 24 | 25 | CountupView mCvCountupViewTest3 = (CountupView)findViewById(R.id.cv_CountupViewTest3); 26 | long time3 = (long)9 * 60 * 60 * 1000; 27 | mCvCountupViewTest3.start(time3); 28 | 29 | CountupView mCvCountupViewTest4 = (CountupView)findViewById(R.id.cv_CountupViewTest4); 30 | long time4 = (long)150 * 24 * 60 * 60 * 1000; 31 | mCvCountupViewTest4.start(time4); 32 | 33 | 34 | CountupView mCvCountupViewTest6 = (CountupView)findViewById(R.id.cv_CountupViewTest6); 35 | long time6 = (long)2 * 60 * 60 * 1000; 36 | mCvCountupViewTest6.start(time6); 37 | 38 | } 39 | 40 | @Override 41 | public void onEnd(CountupView cv) { 42 | Object tag = cv.getTag(); 43 | if (null != tag) { 44 | Log.i("wg", "tag = " + tag.toString()); 45 | } 46 | } 47 | } 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sel_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_listview.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 47 | 48 | 64 | 65 | 85 | 86 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CountupTimeView 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 11 17:52:19 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 10 9 | targetSdkVersion 21 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | } 24 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_GROUP_ID=com.github.iwgang 2 | POM_ARTIFACT_ID=countdownview 3 | POM_DESCRIPTION=Android Countdown View 4 | POM_URL=https://github.com/iwgang/CountdownView 5 | 6 | POM_SCM_URL=https://github.com/iwgang/CountdownView 7 | POM_SCM_CONNECTION=scm:git@github.com:iwgang/CountdownView.git 8 | POM_SCM_DEVELOPER_CONNECTION=scm:git@github.com:iwgang/CountdownView.git 9 | 10 | POM_LICENCE_NAME=The MIT License (MIT) 11 | POM_LICENCE_URL=http://opensource.org/licenses/MIT 12 | POM_LICENCE_DISTRIBUTION=repo 13 | 14 | POM_DEVELOPER_ID=iwgang 15 | POM_DEVELOPER_NAME=iWgang 16 | POM_DEVELOPER_EMAIL=iwgang@163.com 17 | 18 | 19 | VERSION_NAME=1.1 20 | -------------------------------------------------------------------------------- /library/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/apple/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 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/cn/iwgang/countuptime/CountupView.java: -------------------------------------------------------------------------------- 1 | package cn.iwgang.countuptime; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.graphics.RectF; 10 | import android.text.TextUtils; 11 | import android.util.AttributeSet; 12 | import android.view.View; 13 | 14 | /** 15 | * Countdown View 16 | * Created by iWgang on 15/9/16. 17 | * https://github.com/iwgang/CountdownView 18 | */ 19 | public class CountupView extends View { 20 | private static final String DEFAULT_SUFFIX = ":"; 21 | private static final float DEFAULT_SUFFIX_LR_MARGIN = 3; // dp 22 | private static final float DEFAULT_TIME_BG_DIVISION_LINE_SIZE = 0.5f; // dp 23 | 24 | private Context mContext; 25 | private int mDay, mHour, mMinute, mSecond, mMillisecond; 26 | private long mRemainTime; 27 | private OnCountdownEndListener mOnCountdownEndListener; 28 | private OnCountdownIntervalListener mOnCountdownIntervalListener; 29 | private CustomCountUpTimer mCustomCountUpTimer; 30 | 31 | private boolean isShowDay, isShowHour, isShowMinute, isShowSecond, isShowMillisecond; 32 | private boolean mHasSetIsShowDay, mHasSetIsShowHour; 33 | private boolean isHideTimeBackground; 34 | private boolean isShowTimeBgDivisionLine; 35 | private boolean isTimeTextBold; 36 | private boolean isSuffixTextBold; 37 | 38 | private Paint mTimeTextPaint; 39 | private Paint mSuffixTextPaint; 40 | private Paint mTimeTextBgPaint; 41 | private Paint mTimeTextBgDivisionLinePaint; 42 | 43 | private RectF mDayBgRectF, mHourBgRectF, mMinuteBgRectF, mSecondBgRectF, mMillisecondBgRectF; 44 | 45 | private float mTimeTextWidth; 46 | private float mTimeTextHeight; 47 | private float mTimeTextSize; 48 | private float mTimeBgSize; 49 | private int mTimeTextColor; 50 | private int mTimeBgColor; 51 | private float mTimeBgRadius; 52 | private int mTimeBgDivisionLineColor; 53 | private float mTimeBgDivisionLineSize; 54 | private float mTimeTextBaseY; 55 | private float mTimeBgDivisionLineYPos; 56 | 57 | private String mSuffix, mSuffixDay, mSuffixHour, mSuffixMinute, mSuffixSecond, mSuffixMillisecond; 58 | private int mSuffixTextColor; 59 | private float mSuffixTextSize; 60 | private float mSuffixDayTextWidth, mSuffixHourTextWidth, mSuffixMinuteTextWidth, 61 | mSuffixSecondTextWidth, mSuffixMillisecondTextWidth; 62 | private int mSuffixGravity; 63 | private float mSuffixLRMargin; 64 | private float mSuffixDayLeftMargin, mSuffixDayRightMargin; 65 | private float mSuffixHourLeftMargin, mSuffixHourRightMargin; 66 | private float mSuffixMinuteLeftMargin, mSuffixMinuteRightMargin; 67 | private float mSuffixSecondLeftMargin, mSuffixSecondRightMargin; 68 | private float mSuffixMillisecondLeftMargin; 69 | private float mSuffixDayTextBaseline, mSuffixHourTextBaseline, mSuffixMinuteTextBaseline, 70 | mSuffixSecondTextBaseline, mSuffixMillisecondTextBaseline; 71 | 72 | private float mTempSuffixDayLeftMargin, mTempSuffixDayRightMargin; 73 | private float mTempSuffixHourLeftMargin, mTempSuffixHourRightMargin; 74 | private float mTempSuffixMinuteLeftMargin, mTempSuffixMinuteRightMargin; 75 | private float mTempSuffixSecondLeftMargin, mTempSuffixSecondRightMargin; 76 | private float mTempSuffixMillisecondLeftMargin; 77 | private String mTempSuffixMinute, mTempSuffixSecond; 78 | 79 | private float mTimeTextBaseline; 80 | private float mLeftPaddingSize; 81 | private float mTopPaddingSize; 82 | private int mContentAllWidth; 83 | private int mContentAllHeight; 84 | private int mViewWidth; 85 | private int mViewHeight; 86 | private int mTimeTextBottom; 87 | 88 | private float mDayTimeTextWidth; 89 | private float mDayTimeBgWidth; 90 | private boolean isDayLargeNinetyNine; 91 | 92 | private long mInterval; 93 | private long mPreviouIntervalCallbackTime; 94 | private boolean hasCustomSomeSuffix = false; 95 | 96 | public CountupView(Context context) { 97 | this(context, null); 98 | } 99 | 100 | public CountupView(Context context, AttributeSet attrs) { 101 | this(context, attrs, 0); 102 | } 103 | 104 | public CountupView(Context context, AttributeSet attrs, int defStyleAttr) { 105 | super(context, attrs, defStyleAttr); 106 | this.mContext = context; 107 | 108 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CountupView); 109 | mTimeBgColor = ta.getColor(R.styleable.CountupView_timeBgColor, 0xFF444444); 110 | mTimeBgRadius = ta.getDimension(R.styleable.CountupView_timeBgRadius, 0); 111 | isShowTimeBgDivisionLine = 112 | ta.getBoolean(R.styleable.CountupView_isShowTimeBgDivisionLine, true); 113 | mTimeBgDivisionLineColor = 114 | ta.getColor(R.styleable.CountupView_timeBgDivisionLineColor, Color.parseColor("#30FFFFFF")); 115 | mTimeBgDivisionLineSize = ta.getDimension(R.styleable.CountupView_timeBgDivisionLineSize, 116 | dp2px(DEFAULT_TIME_BG_DIVISION_LINE_SIZE)); 117 | mTimeBgSize = ta.getDimension(R.styleable.CountupView_timeBgSize, 0); 118 | 119 | isTimeTextBold = ta.getBoolean(R.styleable.CountupView_isTimeTextBold, false); 120 | mTimeTextSize = ta.getDimension(R.styleable.CountupView_timeTextSize, sp2px(12)); 121 | mTimeTextColor = ta.getColor(R.styleable.CountupView_timeTextColor, 0xFF000000); 122 | isHideTimeBackground = ta.getBoolean(R.styleable.CountupView_isHideTimeBackground, true); 123 | isShowDay = ta.getBoolean(R.styleable.CountupView_isShowDay, false); 124 | isShowHour = ta.getBoolean(R.styleable.CountupView_isShowHour, false); 125 | isShowMinute = ta.getBoolean(R.styleable.CountupView_isShowMinute, true); 126 | isShowSecond = ta.getBoolean(R.styleable.CountupView_isShowSecond, true); 127 | isShowMillisecond = ta.getBoolean(R.styleable.CountupView_isShowMillisecond, false); 128 | 129 | mHasSetIsShowDay = ta.hasValue(R.styleable.CountupView_isShowDay); 130 | mHasSetIsShowHour = ta.hasValue(R.styleable.CountupView_isShowHour); 131 | 132 | isSuffixTextBold = ta.getBoolean(R.styleable.CountupView_isSuffixTextBold, false); 133 | mSuffixTextSize = ta.getDimension(R.styleable.CountupView_suffixTextSize, sp2px(12)); 134 | mSuffixTextColor = ta.getColor(R.styleable.CountupView_suffixTextColor, 0xFF000000); 135 | mSuffix = ta.getString(R.styleable.CountupView_suffix); 136 | mSuffixDay = ta.getString(R.styleable.CountupView_suffixDay); 137 | mSuffixHour = ta.getString(R.styleable.CountupView_suffixHour); 138 | mSuffixMinute = ta.getString(R.styleable.CountupView_suffixMinute); 139 | mSuffixSecond = ta.getString(R.styleable.CountupView_suffixSecond); 140 | mSuffixMillisecond = ta.getString(R.styleable.CountupView_suffixMillisecond); 141 | mSuffixGravity = ta.getInt(R.styleable.CountupView_suffixGravity, 1); 142 | mSuffixLRMargin = ta.getDimension(R.styleable.CountupView_suffixLRMargin, -1); 143 | mSuffixDayLeftMargin = ta.getDimension(R.styleable.CountupView_suffixDayLeftMargin, -1); 144 | mSuffixDayRightMargin = ta.getDimension(R.styleable.CountupView_suffixDayRightMargin, -1); 145 | mSuffixHourLeftMargin = ta.getDimension(R.styleable.CountupView_suffixHourLeftMargin, -1); 146 | mSuffixHourRightMargin = ta.getDimension(R.styleable.CountupView_suffixHourRightMargin, -1); 147 | mSuffixMinuteLeftMargin = ta.getDimension(R.styleable.CountupView_suffixMinuteLeftMargin, -1); 148 | mSuffixMinuteRightMargin = ta.getDimension(R.styleable.CountupView_suffixMinuteRightMargin, -1); 149 | mSuffixSecondLeftMargin = ta.getDimension(R.styleable.CountupView_suffixSecondLeftMargin, -1); 150 | mSuffixSecondRightMargin = ta.getDimension(R.styleable.CountupView_suffixSecondRightMargin, -1); 151 | mSuffixMillisecondLeftMargin = 152 | ta.getDimension(R.styleable.CountupView_suffixMillisecondLeftMargin, -1); 153 | ta.recycle(); 154 | 155 | // temporarily saved suffix left and right margins 156 | mTempSuffixDayLeftMargin = mSuffixDayLeftMargin; 157 | mTempSuffixDayRightMargin = mSuffixDayRightMargin; 158 | mTempSuffixHourLeftMargin = mSuffixHourLeftMargin; 159 | mTempSuffixHourRightMargin = mSuffixHourRightMargin; 160 | mTempSuffixMinuteLeftMargin = mSuffixMinuteLeftMargin; 161 | mTempSuffixMinuteRightMargin = mSuffixMinuteRightMargin; 162 | mTempSuffixSecondLeftMargin = mSuffixSecondLeftMargin; 163 | mTempSuffixSecondRightMargin = mSuffixSecondRightMargin; 164 | mTempSuffixMillisecondLeftMargin = mSuffixMillisecondLeftMargin; 165 | 166 | mTempSuffixMinute = mSuffixMinute; 167 | mTempSuffixSecond = mSuffixSecond; 168 | 169 | // initialize 170 | initPaint(); 171 | initSuffix(true); 172 | initSuffixMargin(); 173 | 174 | // regular time data 175 | // pick one of two (minute and second) 176 | if (!isShowMinute && !isShowSecond) { 177 | isShowSecond = true; 178 | } 179 | if (!isShowSecond) { 180 | isShowMillisecond = false; 181 | } 182 | 183 | // initialize time text width and height 184 | Rect rect = new Rect(); 185 | mTimeTextPaint.getTextBounds("00", 0, 2, rect); 186 | mTimeTextWidth = rect.width(); 187 | mTimeTextHeight = rect.height(); 188 | mTimeTextBottom = rect.bottom; 189 | 190 | if (!isHideTimeBackground && mTimeBgSize < mTimeTextWidth) { 191 | mTimeBgSize = mTimeTextWidth + (dp2px(2) * 4); 192 | } 193 | } 194 | 195 | /** 196 | * initialize Paint 197 | */ 198 | private void initPaint() { 199 | // time text 200 | mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 201 | mTimeTextPaint.setColor(mTimeTextColor); 202 | mTimeTextPaint.setTextAlign(Paint.Align.CENTER); 203 | mTimeTextPaint.setTextSize(mTimeTextSize); 204 | if (isTimeTextBold) { 205 | mTimeTextPaint.setFakeBoldText(true); 206 | } 207 | 208 | // suffix text 209 | mSuffixTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 210 | mSuffixTextPaint.setColor(mSuffixTextColor); 211 | mSuffixTextPaint.setTextSize(mSuffixTextSize); 212 | if (isSuffixTextBold) { 213 | mSuffixTextPaint.setFakeBoldText(true); 214 | } 215 | 216 | // time background 217 | mTimeTextBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 218 | mTimeTextBgPaint.setStyle(Paint.Style.FILL); 219 | mTimeTextBgPaint.setColor(mTimeBgColor); 220 | 221 | // time background division line 222 | mTimeTextBgDivisionLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 223 | mTimeTextBgDivisionLinePaint.setColor(mTimeBgDivisionLineColor); 224 | mTimeTextBgDivisionLinePaint.setStrokeWidth(mTimeBgDivisionLineSize); 225 | } 226 | 227 | /** 228 | * initialize suffix 229 | */ 230 | private void initSuffix(boolean isInit) { 231 | boolean isSuffixNull = true; 232 | float mSuffixTextWidth = 0; 233 | float mDefSuffixTextWidth = mSuffixTextPaint.measureText(DEFAULT_SUFFIX); 234 | 235 | if (!TextUtils.isEmpty(mSuffix)) { 236 | isSuffixNull = false; 237 | mSuffixTextWidth = mSuffixTextPaint.measureText(mSuffix); 238 | } 239 | 240 | boolean isSetSuffixDay = !TextUtils.isEmpty(mSuffixDay); 241 | boolean isSetSuffixHour = !TextUtils.isEmpty(mSuffixHour); 242 | boolean isSetSuffixMinute = !TextUtils.isEmpty(mSuffixMinute); 243 | boolean isSetSuffixSecond = !TextUtils.isEmpty(mSuffixSecond); 244 | boolean isSetSuffixMillisecond = !TextUtils.isEmpty(mSuffixMillisecond); 245 | 246 | if (isInit) { 247 | if ((isShowDay && isSetSuffixDay) || (isShowHour && isSetSuffixHour) || (isShowMinute 248 | && isSetSuffixMinute) || (isShowSecond && isSetSuffixSecond) || (isShowMillisecond 249 | && isSetSuffixMillisecond)) { 250 | hasCustomSomeSuffix = true; 251 | } 252 | } 253 | 254 | if (isShowDay) { 255 | if (isSetSuffixDay) { 256 | mSuffixDayTextWidth = mSuffixTextPaint.measureText(mSuffixDay); 257 | } else { 258 | if (!isSuffixNull) { 259 | mSuffixDay = mSuffix; 260 | mSuffixDayTextWidth = mSuffixTextWidth; 261 | } else if (!hasCustomSomeSuffix) { 262 | mSuffixDay = DEFAULT_SUFFIX; 263 | mSuffixDayTextWidth = mDefSuffixTextWidth; 264 | } 265 | } 266 | } else { 267 | mSuffixDayTextWidth = 0; 268 | } 269 | 270 | if (isShowHour) { 271 | if (isSetSuffixHour) { 272 | mSuffixHourTextWidth = mSuffixTextPaint.measureText(mSuffixHour); 273 | } else { 274 | if (!isSuffixNull) { 275 | mSuffixHour = mSuffix; 276 | mSuffixHourTextWidth = mSuffixTextWidth; 277 | } else if (!hasCustomSomeSuffix) { 278 | mSuffixHour = DEFAULT_SUFFIX; 279 | mSuffixHourTextWidth = mDefSuffixTextWidth; 280 | } 281 | } 282 | } else { 283 | mSuffixHourTextWidth = 0; 284 | } 285 | 286 | if (isShowMinute) { 287 | if (isSetSuffixMinute) { 288 | mSuffixMinuteTextWidth = mSuffixTextPaint.measureText(mSuffixMinute); 289 | } else if (isShowSecond) { 290 | if (!isSuffixNull) { 291 | mSuffixMinute = mSuffix; 292 | mSuffixMinuteTextWidth = mSuffixTextWidth; 293 | } else if (!hasCustomSomeSuffix) { 294 | mSuffixMinute = DEFAULT_SUFFIX; 295 | mSuffixMinuteTextWidth = mDefSuffixTextWidth; 296 | } 297 | } else { 298 | mSuffixMinuteTextWidth = 0; 299 | } 300 | } else { 301 | mSuffixMinuteTextWidth = 0; 302 | } 303 | 304 | if (isShowSecond) { 305 | if (isSetSuffixSecond) { 306 | mSuffixSecondTextWidth = mSuffixTextPaint.measureText(mSuffixSecond); 307 | } else if (isShowMillisecond) { 308 | if (!isSuffixNull) { 309 | mSuffixSecond = mSuffix; 310 | mSuffixSecondTextWidth = mSuffixTextWidth; 311 | } else if (!hasCustomSomeSuffix) { 312 | mSuffixSecond = DEFAULT_SUFFIX; 313 | mSuffixSecondTextWidth = mDefSuffixTextWidth; 314 | } 315 | } else { 316 | mSuffixSecondTextWidth = 0; 317 | } 318 | } else { 319 | mSuffixSecondTextWidth = 0; 320 | } 321 | 322 | if (isShowMillisecond && hasCustomSomeSuffix && isSetSuffixMillisecond) { 323 | mSuffixMillisecondTextWidth = mSuffixTextPaint.measureText(mSuffixMillisecond); 324 | } else { 325 | mSuffixMillisecondTextWidth = 0; 326 | } 327 | } 328 | 329 | /** 330 | * initialize suffix margin 331 | */ 332 | private void initSuffixMargin() { 333 | int defSuffixLRMargin = dp2px(DEFAULT_SUFFIX_LR_MARGIN); 334 | boolean isSuffixLRMarginNull = true; 335 | 336 | if (mSuffixLRMargin >= 0) { 337 | isSuffixLRMarginNull = false; 338 | } 339 | 340 | if (isShowDay && mSuffixDayTextWidth > 0) { 341 | if (mSuffixDayLeftMargin < 0) { 342 | if (!isSuffixLRMarginNull) { 343 | mSuffixDayLeftMargin = mSuffixLRMargin; 344 | } else { 345 | mSuffixDayLeftMargin = defSuffixLRMargin; 346 | } 347 | } 348 | 349 | if (mSuffixDayRightMargin < 0) { 350 | if (!isSuffixLRMarginNull) { 351 | mSuffixDayRightMargin = mSuffixLRMargin; 352 | } else { 353 | mSuffixDayRightMargin = defSuffixLRMargin; 354 | } 355 | } 356 | } else { 357 | mSuffixDayLeftMargin = 0; 358 | mSuffixDayRightMargin = 0; 359 | } 360 | 361 | if (isShowHour && mSuffixHourTextWidth > 0) { 362 | if (mSuffixHourLeftMargin < 0) { 363 | if (!isSuffixLRMarginNull) { 364 | mSuffixHourLeftMargin = mSuffixLRMargin; 365 | } else { 366 | mSuffixHourLeftMargin = defSuffixLRMargin; 367 | } 368 | } 369 | 370 | if (mSuffixHourRightMargin < 0) { 371 | if (!isSuffixLRMarginNull) { 372 | mSuffixHourRightMargin = mSuffixLRMargin; 373 | } else { 374 | mSuffixHourRightMargin = defSuffixLRMargin; 375 | } 376 | } 377 | } else { 378 | mSuffixHourLeftMargin = 0; 379 | mSuffixHourRightMargin = 0; 380 | } 381 | 382 | if (isShowMinute && mSuffixMinuteTextWidth > 0) { 383 | if (mSuffixMinuteLeftMargin < 0) { 384 | if (!isSuffixLRMarginNull) { 385 | mSuffixMinuteLeftMargin = mSuffixLRMargin; 386 | } else { 387 | mSuffixMinuteLeftMargin = defSuffixLRMargin; 388 | } 389 | } 390 | 391 | if (isShowSecond) { 392 | if (mSuffixMinuteRightMargin < 0) { 393 | if (!isSuffixLRMarginNull) { 394 | mSuffixMinuteRightMargin = mSuffixLRMargin; 395 | } else { 396 | mSuffixMinuteRightMargin = defSuffixLRMargin; 397 | } 398 | } 399 | } else { 400 | mSuffixMinuteRightMargin = 0; 401 | } 402 | } else { 403 | mSuffixMinuteLeftMargin = 0; 404 | mSuffixMinuteRightMargin = 0; 405 | } 406 | 407 | if (isShowSecond) { 408 | if (mSuffixSecondTextWidth > 0) { 409 | if (mSuffixSecondLeftMargin < 0) { 410 | if (!isSuffixLRMarginNull) { 411 | mSuffixSecondLeftMargin = mSuffixLRMargin; 412 | } else { 413 | mSuffixSecondLeftMargin = defSuffixLRMargin; 414 | } 415 | } 416 | 417 | if (isShowMillisecond) { 418 | if (mSuffixSecondRightMargin < 0) { 419 | if (!isSuffixLRMarginNull) { 420 | mSuffixSecondRightMargin = mSuffixLRMargin; 421 | } else { 422 | mSuffixSecondRightMargin = defSuffixLRMargin; 423 | } 424 | } 425 | } else { 426 | mSuffixSecondRightMargin = 0; 427 | } 428 | } else { 429 | mSuffixSecondLeftMargin = 0; 430 | mSuffixSecondRightMargin = 0; 431 | } 432 | 433 | if (isShowMillisecond && mSuffixMillisecondTextWidth > 0) { 434 | if (mSuffixMillisecondLeftMargin < 0) { 435 | if (!isSuffixLRMarginNull) { 436 | mSuffixMillisecondLeftMargin = mSuffixLRMargin; 437 | } else { 438 | mSuffixMillisecondLeftMargin = defSuffixLRMargin; 439 | } 440 | } 441 | } else { 442 | mSuffixMillisecondLeftMargin = 0; 443 | } 444 | } else { 445 | mSuffixSecondLeftMargin = 0; 446 | mSuffixSecondRightMargin = 0; 447 | mSuffixMillisecondLeftMargin = 0; 448 | } 449 | } 450 | 451 | /** 452 | * initialize time initialize rectF 453 | */ 454 | private void initTimeBgRect() { 455 | if (!isHideTimeBackground) { 456 | float mHourLeft; 457 | float mMinuteLeft; 458 | float mSecondLeft; 459 | 460 | if (isShowDay) { 461 | // initialize day background rectF 462 | mDayBgRectF = 463 | new RectF(mLeftPaddingSize, mTopPaddingSize, mLeftPaddingSize + mDayTimeBgWidth, 464 | mTopPaddingSize + mDayTimeBgWidth); 465 | // hour left point 466 | mHourLeft = mLeftPaddingSize 467 | + mDayTimeBgWidth 468 | + mSuffixDayTextWidth 469 | + mSuffixDayLeftMargin 470 | + mSuffixDayRightMargin; 471 | } else { 472 | // hour left point 473 | mHourLeft = mLeftPaddingSize; 474 | } 475 | 476 | if (isShowHour) { 477 | // initialize hour background rectF 478 | mHourBgRectF = new RectF(mHourLeft, mTopPaddingSize, mHourLeft + mTimeBgSize, 479 | mTopPaddingSize + mTimeBgSize); 480 | // minute left point 481 | mMinuteLeft = mHourLeft 482 | + mTimeBgSize 483 | + mSuffixHourTextWidth 484 | + mSuffixHourLeftMargin 485 | + mSuffixHourRightMargin; 486 | } else { 487 | // minute left point 488 | mMinuteLeft = mHourLeft; 489 | } 490 | 491 | if (isShowMinute) { 492 | // initialize minute background rectF 493 | mMinuteBgRectF = new RectF(mMinuteLeft, mTopPaddingSize, mMinuteLeft + mTimeBgSize, 494 | mTopPaddingSize + mTimeBgSize); 495 | // second left point 496 | mSecondLeft = mMinuteLeft 497 | + mTimeBgSize 498 | + mSuffixMinuteTextWidth 499 | + mSuffixMinuteLeftMargin 500 | + mSuffixMinuteRightMargin; 501 | } else { 502 | // second left point 503 | mSecondLeft = mMinuteLeft; 504 | } 505 | 506 | if (isShowSecond) { 507 | // initialize second background rectF 508 | mSecondBgRectF = new RectF(mSecondLeft, mTopPaddingSize, mSecondLeft + mTimeBgSize, 509 | mTopPaddingSize + mTimeBgSize); 510 | 511 | if (isShowMillisecond) { 512 | // millisecond left point 513 | float mMillisecondLeft = mSecondLeft 514 | + mTimeBgSize 515 | + mSuffixSecondTextWidth 516 | + mSuffixSecondLeftMargin 517 | + mSuffixSecondRightMargin; 518 | 519 | // initialize millisecond background rectF 520 | mMillisecondBgRectF = 521 | new RectF(mMillisecondLeft, mTopPaddingSize, mMillisecondLeft + mTimeBgSize, 522 | mTopPaddingSize + mTimeBgSize); 523 | } 524 | } 525 | 526 | // time text baseline 527 | Paint.FontMetrics timeFontMetrics = mTimeTextPaint.getFontMetrics(); 528 | mTimeTextBaseY = mSecondBgRectF.top 529 | + (mSecondBgRectF.bottom - mSecondBgRectF.top - timeFontMetrics.bottom 530 | + timeFontMetrics.top) / 2 - timeFontMetrics.top - mTimeTextBottom; 531 | 532 | // initialize background division line y point 533 | mTimeBgDivisionLineYPos = mSecondBgRectF.centerY() + ( 534 | mTimeBgDivisionLineSize == dp2px(DEFAULT_TIME_BG_DIVISION_LINE_SIZE) 535 | ? mTimeBgDivisionLineSize : mTimeBgDivisionLineSize / 2); 536 | } 537 | } 538 | 539 | @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 540 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 541 | 542 | mContentAllWidth = getAllContentWidth(); 543 | mContentAllHeight = isHideTimeBackground ? (int) mTimeTextHeight : (int) mTimeBgSize; 544 | 545 | mViewWidth = measureSize(1, mContentAllWidth, widthMeasureSpec); 546 | mViewHeight = measureSize(2, mContentAllHeight, heightMeasureSpec); 547 | 548 | setMeasuredDimension(mViewWidth, mViewHeight); 549 | 550 | initTimeTextBaselineAndTimeBgTopPadding(); 551 | initLeftPaddingSize(); 552 | initTimeBgRect(); 553 | } 554 | 555 | /** 556 | * initialize time text baseline 557 | * and 558 | * time background top padding 559 | */ 560 | private void initTimeTextBaselineAndTimeBgTopPadding() { 561 | if (getPaddingTop() == getPaddingBottom()) { 562 | // center 563 | mTimeTextBaseline = mViewHeight / 2 + mTimeTextHeight / 2 - mTimeTextBottom; 564 | 565 | mTopPaddingSize = (mViewHeight - mContentAllHeight) / 2; 566 | } else { 567 | // padding top 568 | mTimeTextBaseline = 569 | mViewHeight - (mViewHeight - getPaddingTop()) + mTimeTextHeight - mTimeTextBottom; 570 | 571 | mTopPaddingSize = getPaddingTop(); 572 | } 573 | 574 | if (isShowDay && mSuffixDayTextWidth > 0) { 575 | mSuffixDayTextBaseline = getSuffixTextBaseLine(mSuffixDay); 576 | } 577 | 578 | if (isShowHour && mSuffixHourTextWidth > 0) { 579 | mSuffixHourTextBaseline = getSuffixTextBaseLine(mSuffixHour); 580 | } 581 | 582 | if (isShowMinute && mSuffixMinuteTextWidth > 0) { 583 | mSuffixMinuteTextBaseline = getSuffixTextBaseLine(mSuffixMinute); 584 | } 585 | 586 | if (mSuffixSecondTextWidth > 0) { 587 | mSuffixSecondTextBaseline = getSuffixTextBaseLine(mSuffixSecond); 588 | } 589 | 590 | if (isShowMillisecond && mSuffixMillisecondTextWidth > 0) { 591 | mSuffixMillisecondTextBaseline = getSuffixTextBaseLine(mSuffixMillisecond); 592 | } 593 | } 594 | 595 | private float getSuffixTextBaseLine(String suffixText) { 596 | Rect tempRect = new Rect(); 597 | mSuffixTextPaint.getTextBounds(suffixText, 0, suffixText.length(), tempRect); 598 | 599 | float ret; 600 | switch (mSuffixGravity) { 601 | case 0: 602 | // top 603 | if (isHideTimeBackground) { 604 | ret = mTimeTextBaseline - mTimeTextHeight - tempRect.top; 605 | } else { 606 | ret = mTopPaddingSize - tempRect.top; 607 | } 608 | break; 609 | default: 610 | case 1: 611 | // center 612 | if (isHideTimeBackground) { 613 | ret = mTimeTextBaseline - mTimeTextHeight / 2 + tempRect.height() / 2; 614 | } else { 615 | ret = mTopPaddingSize + mTimeBgSize - mTimeBgSize / 2 + tempRect.height() / 2; 616 | } 617 | break; 618 | case 2: 619 | // bottom 620 | if (isHideTimeBackground) { 621 | ret = mTimeTextBaseline - tempRect.bottom; 622 | } else { 623 | ret = mTopPaddingSize + mTimeBgSize - tempRect.bottom; 624 | } 625 | break; 626 | } 627 | 628 | return ret; 629 | } 630 | 631 | /** 632 | * initialize view left padding 633 | */ 634 | private void initLeftPaddingSize() { 635 | if (getPaddingLeft() == getPaddingRight()) { 636 | // center 637 | mLeftPaddingSize = (mViewWidth - mContentAllWidth) / 2; 638 | } else { 639 | // padding left 640 | mLeftPaddingSize = getPaddingLeft(); 641 | } 642 | } 643 | 644 | /** 645 | * measure view Size 646 | * 647 | * @param specType 1 width 2 height 648 | * @param contentSize all content view size 649 | * @param measureSpec spec 650 | * @return measureSize 651 | */ 652 | private int measureSize(int specType, int contentSize, int measureSpec) { 653 | int result; 654 | int specMode = MeasureSpec.getMode(measureSpec); 655 | int specSize = MeasureSpec.getSize(measureSpec); 656 | 657 | if (specMode == MeasureSpec.EXACTLY) { 658 | result = Math.max(contentSize, specSize); 659 | } else { 660 | result = contentSize; 661 | 662 | if (specType == 1) { 663 | // width 664 | result += (getPaddingLeft() + getPaddingRight()); 665 | } else { 666 | // height 667 | result += (getPaddingTop() + getPaddingBottom()); 668 | } 669 | } 670 | 671 | return result; 672 | } 673 | 674 | /** 675 | * get all view width 676 | * 677 | * @return all view width 678 | */ 679 | private int getAllContentWidth() { 680 | float timeWidth = isHideTimeBackground ? mTimeTextWidth : mTimeBgSize; 681 | float width = (mSuffixDayTextWidth 682 | + mSuffixHourTextWidth 683 | + mSuffixMinuteTextWidth 684 | + mSuffixSecondTextWidth 685 | + mSuffixMillisecondTextWidth); 686 | width += (mSuffixDayLeftMargin 687 | + mSuffixDayRightMargin 688 | + mSuffixHourLeftMargin 689 | + mSuffixHourRightMargin 690 | + mSuffixMinuteLeftMargin 691 | + mSuffixMinuteRightMargin 692 | + mSuffixSecondLeftMargin 693 | + mSuffixSecondRightMargin 694 | + mSuffixMillisecondLeftMargin); 695 | 696 | if (isShowDay) { 697 | if (isDayLargeNinetyNine) { 698 | Rect rect = new Rect(); 699 | String tempDay = String.valueOf(mDay); 700 | mTimeTextPaint.getTextBounds(tempDay, 0, tempDay.length(), rect); 701 | mDayTimeTextWidth = rect.width(); 702 | 703 | if (!isHideTimeBackground) { 704 | mDayTimeBgWidth = mDayTimeTextWidth + (dp2px(2) * 4); 705 | width += mDayTimeBgWidth; 706 | } else { 707 | width += mDayTimeTextWidth; 708 | } 709 | } else { 710 | mDayTimeTextWidth = mTimeTextWidth; 711 | mDayTimeBgWidth = mTimeBgSize; 712 | width += timeWidth; 713 | } 714 | } 715 | 716 | if (isShowHour) { 717 | width += timeWidth; 718 | } 719 | 720 | if (isShowMinute) { 721 | width += timeWidth; 722 | } 723 | 724 | if (isShowSecond) { 725 | width += timeWidth; 726 | } 727 | 728 | if (isShowMillisecond) { 729 | width += timeWidth; 730 | } 731 | 732 | return (int) Math.ceil(width); 733 | } 734 | 735 | private void refTimeShow(boolean isShowDay, boolean isShowHour, boolean isShowMinute, 736 | boolean isShowSecond, boolean isShowMillisecond) { 737 | boolean isRef = false; 738 | 739 | if (this.isShowDay != isShowDay) { 740 | this.isShowDay = isShowDay; 741 | isRef = true; 742 | 743 | // reset day margins 744 | if (isShowDay) { 745 | mSuffixDayLeftMargin = mTempSuffixDayLeftMargin; 746 | mSuffixDayRightMargin = mTempSuffixDayRightMargin; 747 | } 748 | } 749 | 750 | if (this.isShowHour != isShowHour) { 751 | this.isShowHour = isShowHour; 752 | isRef = true; 753 | 754 | // reset hour margins 755 | if (isShowHour) { 756 | mSuffixHourLeftMargin = mTempSuffixHourLeftMargin; 757 | mSuffixHourRightMargin = mTempSuffixHourRightMargin; 758 | } 759 | } 760 | 761 | if (this.isShowMinute != isShowMinute) { 762 | this.isShowMinute = isShowMinute; 763 | isRef = true; 764 | 765 | // reset minute margins 766 | if (isShowMinute) { 767 | mSuffixMinuteLeftMargin = mTempSuffixMinuteLeftMargin; 768 | mSuffixMinuteRightMargin = mTempSuffixMinuteRightMargin; 769 | mSuffixMinute = mTempSuffixMinute; 770 | } 771 | } 772 | 773 | boolean isModCountdownInterval = false; 774 | 775 | if (this.isShowSecond != isShowSecond) { 776 | this.isShowSecond = isShowSecond; 777 | isRef = true; 778 | isModCountdownInterval = true; 779 | 780 | // reset second margins 781 | if (isShowSecond) { 782 | mSuffixSecondLeftMargin = mTempSuffixSecondLeftMargin; 783 | mSuffixSecondRightMargin = mTempSuffixSecondRightMargin; 784 | mSuffixSecond = mTempSuffixSecond; 785 | } else { 786 | mSuffixMinute = mTempSuffixMinute; 787 | } 788 | 789 | mSuffixMinuteLeftMargin = mTempSuffixMinuteLeftMargin; 790 | mSuffixMinuteRightMargin = mTempSuffixMinuteRightMargin; 791 | } 792 | 793 | if (this.isShowMillisecond != isShowMillisecond) { 794 | this.isShowMillisecond = isShowMillisecond; 795 | isRef = true; 796 | isModCountdownInterval = true; 797 | 798 | // reset millisecond margins 799 | if (isShowMillisecond) { 800 | mSuffixMillisecondLeftMargin = mTempSuffixMillisecondLeftMargin; 801 | } else { 802 | mSuffixSecond = mTempSuffixSecond; 803 | } 804 | 805 | mSuffixSecondLeftMargin = mTempSuffixSecondLeftMargin; 806 | mSuffixSecondRightMargin = mTempSuffixSecondRightMargin; 807 | } 808 | 809 | // judge modify countdown interval 810 | if (isModCountdownInterval) { 811 | start(mRemainTime); 812 | } 813 | 814 | if (isRef) { 815 | initSuffix(false); 816 | initSuffixMargin(); 817 | requestLayout(); 818 | } 819 | } 820 | 821 | @Override protected void onDraw(Canvas canvas) { 822 | super.onDraw(canvas); 823 | 824 | float mHourLeft; 825 | float mMinuteLeft; 826 | float mSecondLeft; 827 | 828 | if (isHideTimeBackground) { 829 | // no background 830 | 831 | if (isShowDay) { 832 | // draw day text 833 | canvas.drawText(formatNum(mDay), mLeftPaddingSize + mDayTimeTextWidth / 2, 834 | mTimeTextBaseline, mTimeTextPaint); 835 | if (mSuffixDayTextWidth > 0) { 836 | // draw day suffix 837 | canvas.drawText(mSuffixDay, mLeftPaddingSize + mDayTimeTextWidth + mSuffixDayLeftMargin, 838 | mSuffixDayTextBaseline, mSuffixTextPaint); 839 | } 840 | 841 | // hour left point 842 | mHourLeft = mLeftPaddingSize 843 | + mDayTimeTextWidth 844 | + mSuffixDayTextWidth 845 | + mSuffixDayLeftMargin 846 | + mSuffixDayRightMargin; 847 | } else { 848 | // hour left point 849 | mHourLeft = mLeftPaddingSize; 850 | } 851 | 852 | if (isShowHour) { 853 | // draw hour text 854 | canvas.drawText(formatNum(mHour), mHourLeft + mTimeTextWidth / 2, mTimeTextBaseline, 855 | mTimeTextPaint); 856 | if (mSuffixHourTextWidth > 0) { 857 | // draw hour suffix 858 | canvas.drawText(mSuffixHour, mHourLeft + mTimeTextWidth + mSuffixHourLeftMargin, 859 | mSuffixHourTextBaseline, mSuffixTextPaint); 860 | } 861 | 862 | // minute left point 863 | mMinuteLeft = mHourLeft 864 | + mTimeTextWidth 865 | + mSuffixHourTextWidth 866 | + mSuffixHourLeftMargin 867 | + mSuffixHourRightMargin; 868 | } else { 869 | // minute left point 870 | mMinuteLeft = mHourLeft; 871 | } 872 | 873 | if (isShowMinute) { 874 | // draw minute text 875 | canvas.drawText(formatNum(mMinute), mMinuteLeft + mTimeTextWidth / 2, mTimeTextBaseline, 876 | mTimeTextPaint); 877 | if (mSuffixMinuteTextWidth > 0) { 878 | // draw minute suffix 879 | canvas.drawText(mSuffixMinute, mMinuteLeft + mTimeTextWidth + mSuffixMinuteLeftMargin, 880 | mSuffixMinuteTextBaseline, mSuffixTextPaint); 881 | } 882 | 883 | // second left point 884 | mSecondLeft = mMinuteLeft 885 | + mTimeTextWidth 886 | + mSuffixMinuteTextWidth 887 | + mSuffixMinuteLeftMargin 888 | + mSuffixMinuteRightMargin; 889 | } else { 890 | // second left point 891 | mSecondLeft = mMinuteLeft; 892 | } 893 | 894 | if (isShowSecond) { 895 | // draw second text 896 | canvas.drawText(formatNum(mSecond), mSecondLeft + mTimeTextWidth / 2, mTimeTextBaseline, 897 | mTimeTextPaint); 898 | if (mSuffixSecondTextWidth > 0) { 899 | // draw second suffix 900 | canvas.drawText(mSuffixSecond, mSecondLeft + mTimeTextWidth + mSuffixSecondLeftMargin, 901 | mSuffixSecondTextBaseline, mSuffixTextPaint); 902 | } 903 | 904 | if (isShowMillisecond) { 905 | // millisecond left point 906 | float mMillisecondLeft = mSecondLeft 907 | + mTimeTextWidth 908 | + mSuffixSecondTextWidth 909 | + mSuffixSecondLeftMargin 910 | + mSuffixSecondRightMargin; 911 | // draw millisecond text 912 | canvas.drawText(formatMillisecond(), mMillisecondLeft + mTimeTextWidth / 2, 913 | mTimeTextBaseline, mTimeTextPaint); 914 | if (mSuffixMillisecondTextWidth > 0) { 915 | // draw millisecond suffix 916 | canvas.drawText(mSuffixMillisecond, 917 | mMillisecondLeft + mTimeTextWidth + mSuffixMillisecondLeftMargin, 918 | mSuffixMillisecondTextBaseline, mSuffixTextPaint); 919 | } 920 | } 921 | } 922 | } else { 923 | // show background 924 | 925 | if (isShowDay) { 926 | // draw day background 927 | canvas.drawRoundRect(mDayBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint); 928 | if (isShowTimeBgDivisionLine) { 929 | // draw day background division line 930 | canvas.drawLine(mLeftPaddingSize, mTimeBgDivisionLineYPos, 931 | mLeftPaddingSize + mDayTimeBgWidth, mTimeBgDivisionLineYPos, 932 | mTimeTextBgDivisionLinePaint); 933 | } 934 | // draw day text 935 | canvas.drawText(formatNum(mDay), mDayBgRectF.centerX(), mTimeTextBaseY, mTimeTextPaint); 936 | if (mSuffixDayTextWidth > 0) { 937 | // draw day suffix 938 | canvas.drawText(mSuffixDay, mLeftPaddingSize + mDayTimeBgWidth + mSuffixDayLeftMargin, 939 | mSuffixDayTextBaseline, mSuffixTextPaint); 940 | } 941 | 942 | // hour left point 943 | mHourLeft = mLeftPaddingSize 944 | + mDayTimeBgWidth 945 | + mSuffixDayTextWidth 946 | + mSuffixDayLeftMargin 947 | + mSuffixDayRightMargin; 948 | } else { 949 | // hour left point 950 | mHourLeft = mLeftPaddingSize; 951 | } 952 | 953 | if (isShowHour) { 954 | // draw hour background 955 | canvas.drawRoundRect(mHourBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint); 956 | if (isShowTimeBgDivisionLine) { 957 | // draw hour background division line 958 | canvas.drawLine(mHourLeft, mTimeBgDivisionLineYPos, mTimeBgSize + mHourLeft, 959 | mTimeBgDivisionLineYPos, mTimeTextBgDivisionLinePaint); 960 | } 961 | // draw hour text 962 | canvas.drawText(formatNum(mHour), mHourBgRectF.centerX(), mTimeTextBaseY, mTimeTextPaint); 963 | if (mSuffixHourTextWidth > 0) { 964 | // draw hour suffix 965 | canvas.drawText(mSuffixHour, mHourLeft + mTimeBgSize + mSuffixHourLeftMargin, 966 | mSuffixHourTextBaseline, mSuffixTextPaint); 967 | } 968 | 969 | // minute left point 970 | mMinuteLeft = mHourLeft 971 | + mTimeBgSize 972 | + mSuffixHourTextWidth 973 | + mSuffixHourLeftMargin 974 | + mSuffixHourRightMargin; 975 | } else { 976 | // minute left point 977 | mMinuteLeft = mHourLeft; 978 | } 979 | 980 | if (isShowMinute) { 981 | // draw minute background 982 | canvas.drawRoundRect(mMinuteBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint); 983 | if (isShowTimeBgDivisionLine) { 984 | // draw minute background division line 985 | canvas.drawLine(mMinuteLeft, mTimeBgDivisionLineYPos, mTimeBgSize + mMinuteLeft, 986 | mTimeBgDivisionLineYPos, mTimeTextBgDivisionLinePaint); 987 | } 988 | // draw minute text 989 | canvas.drawText(formatNum(mMinute), mMinuteBgRectF.centerX(), mTimeTextBaseY, 990 | mTimeTextPaint); 991 | if (mSuffixMinuteTextWidth > 0) { 992 | // draw minute suffix 993 | canvas.drawText(mSuffixMinute, mMinuteLeft + mTimeBgSize + mSuffixMinuteLeftMargin, 994 | mSuffixMinuteTextBaseline, mSuffixTextPaint); 995 | } 996 | 997 | // second left point 998 | mSecondLeft = mMinuteLeft 999 | + mTimeBgSize 1000 | + mSuffixMinuteTextWidth 1001 | + mSuffixMinuteLeftMargin 1002 | + mSuffixMinuteRightMargin; 1003 | } else { 1004 | // second left point 1005 | mSecondLeft = mMinuteLeft; 1006 | } 1007 | 1008 | if (isShowSecond) { 1009 | // draw second background 1010 | canvas.drawRoundRect(mSecondBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint); 1011 | if (isShowTimeBgDivisionLine) { 1012 | // draw second background division line 1013 | canvas.drawLine(mSecondLeft, mTimeBgDivisionLineYPos, mTimeBgSize + mSecondLeft, 1014 | mTimeBgDivisionLineYPos, mTimeTextBgDivisionLinePaint); 1015 | } 1016 | // draw second text 1017 | canvas.drawText(formatNum(mSecond), mSecondBgRectF.centerX(), mTimeTextBaseY, 1018 | mTimeTextPaint); 1019 | if (mSuffixSecondTextWidth > 0) { 1020 | // draw second suffix 1021 | canvas.drawText(mSuffixSecond, mSecondLeft + mTimeBgSize + mSuffixSecondLeftMargin, 1022 | mSuffixSecondTextBaseline, mSuffixTextPaint); 1023 | } 1024 | 1025 | if (isShowMillisecond) { 1026 | // millisecond left point 1027 | float mMillisecondLeft = mSecondLeft 1028 | + mTimeBgSize 1029 | + mSuffixSecondTextWidth 1030 | + mSuffixSecondLeftMargin 1031 | + mSuffixSecondRightMargin; 1032 | // draw millisecond background 1033 | canvas.drawRoundRect(mMillisecondBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint); 1034 | if (isShowTimeBgDivisionLine) { 1035 | // draw millisecond background division line 1036 | canvas.drawLine(mMillisecondLeft, mTimeBgDivisionLineYPos, 1037 | mTimeBgSize + mMillisecondLeft, mTimeBgDivisionLineYPos, 1038 | mTimeTextBgDivisionLinePaint); 1039 | } 1040 | // draw millisecond text 1041 | canvas.drawText(formatMillisecond(), mMillisecondBgRectF.centerX(), mTimeTextBaseY, 1042 | mTimeTextPaint); 1043 | if (mSuffixMillisecondTextWidth > 0) { 1044 | // draw millisecond suffix 1045 | canvas.drawText(mSuffixMillisecond, 1046 | mMillisecondLeft + mTimeBgSize + mSuffixMillisecondLeftMargin, 1047 | mSuffixMillisecondTextBaseline, mSuffixTextPaint); 1048 | } 1049 | } 1050 | } 1051 | } 1052 | } 1053 | 1054 | 1055 | @Override protected void onDetachedFromWindow() { 1056 | LogUtils.d("被回收",""); 1057 | stop();//现在不会导致bug 1058 | super.onDetachedFromWindow(); 1059 | } 1060 | 1061 | @Override protected void onAttachedToWindow() { 1062 | LogUtils.i("视图创建",""); 1063 | if (null != mCustomCountUpTimer && mCustomCountUpTimer.isStarted()) { 1064 | mCustomCountUpTimer.recycledstart(); 1065 | LogUtils.i("时钟复用",""); 1066 | } 1067 | super.onAttachedToWindow(); 1068 | } 1069 | 1070 | /** 1071 | * start countdown 1072 | * 1073 | * @param millisecond millisecond 1074 | */ 1075 | public void start(long millisecond) { 1076 | if (millisecond <= 0) { 1077 | return; 1078 | } 1079 | 1080 | if (null != mCustomCountUpTimer) { 1081 | mCustomCountUpTimer.stop(); 1082 | mCustomCountUpTimer = null; 1083 | } 1084 | 1085 | long countDownInterval; 1086 | if (isShowMillisecond) { 1087 | countDownInterval = 10; 1088 | updateShow(millisecond); 1089 | } else { 1090 | countDownInterval = 1000; 1091 | } 1092 | 1093 | mCustomCountUpTimer = new CustomCountUpTimer(millisecond, countDownInterval) { 1094 | @Override public void onTick(long millisUntilFinished) { 1095 | updateShow(millisUntilFinished); 1096 | } 1097 | 1098 | @Override public void onFinish() { 1099 | // countdown end 1100 | allShowZero(); 1101 | // callback 1102 | if (null != mOnCountdownEndListener) { 1103 | mOnCountdownEndListener.onEnd(CountupView.this); 1104 | } 1105 | } 1106 | }; 1107 | mCustomCountUpTimer.start(); 1108 | } 1109 | 1110 | /** 1111 | * stop countdown 1112 | */ 1113 | public void stop() { 1114 | if (null != mCustomCountUpTimer) mCustomCountUpTimer.stop(); 1115 | } 1116 | 1117 | /** 1118 | * pause countdown 1119 | */ 1120 | public void pause() { 1121 | if (null != mCustomCountUpTimer) mCustomCountUpTimer.pause(); 1122 | } 1123 | 1124 | /** 1125 | * pause countdown 1126 | */ 1127 | public void restart() { 1128 | if (null != mCustomCountUpTimer) mCustomCountUpTimer.restart(); 1129 | } 1130 | 1131 | /** 1132 | * custom time show 1133 | * 1134 | * @param isShowDay isShowDay 1135 | * @param isShowHour isShowHour 1136 | * @param isShowMinute isShowMinute 1137 | * @param isShowSecond isShowSecond 1138 | * @param isShowMillisecond isShowMillisecond 1139 | */ 1140 | public void customTimeShow(boolean isShowDay, boolean isShowHour, boolean isShowMinute, 1141 | boolean isShowSecond, boolean isShowMillisecond) { 1142 | mHasSetIsShowDay = true; 1143 | mHasSetIsShowHour = true; 1144 | 1145 | // regular time data 1146 | // pick one of two (minute and second) 1147 | if (!isShowMinute && !isShowSecond) { 1148 | isShowSecond = true; 1149 | } 1150 | if (!isShowSecond) { 1151 | isShowMillisecond = false; 1152 | } 1153 | 1154 | refTimeShow(isShowDay, isShowHour, isShowMinute, isShowSecond, isShowMillisecond); 1155 | } 1156 | 1157 | /** 1158 | * set all time zero 1159 | */ 1160 | public void allShowZero() { 1161 | mHour = 0; 1162 | mMinute = 0; 1163 | mSecond = 0; 1164 | mMillisecond = 0; 1165 | invalidate(); 1166 | } 1167 | 1168 | /** 1169 | * set countdown end callback listener 1170 | * 1171 | * @param onCountdownEndListener OnCountdownEndListener 1172 | */ 1173 | public void setOnCountdownEndListener(OnCountdownEndListener onCountdownEndListener) { 1174 | this.mOnCountdownEndListener = onCountdownEndListener; 1175 | } 1176 | 1177 | /** 1178 | * set interval callback listener 1179 | * 1180 | * @param interval interval time 1181 | * @param onCountdownIntervalListener OnCountdownIntervalListener 1182 | */ 1183 | public void setOnCountdownIntervalListener(long interval, 1184 | OnCountdownIntervalListener onCountdownIntervalListener) { 1185 | this.mInterval = interval; 1186 | this.mOnCountdownIntervalListener = onCountdownIntervalListener; 1187 | } 1188 | 1189 | /** 1190 | * get day 1191 | * 1192 | * @return current day 1193 | */ 1194 | public int getDay() { 1195 | return mDay; 1196 | } 1197 | 1198 | /** 1199 | * get hour 1200 | * 1201 | * @return current hour 1202 | */ 1203 | public int getHour() { 1204 | return mHour; 1205 | } 1206 | 1207 | /** 1208 | * get minute 1209 | * 1210 | * @return current minute 1211 | */ 1212 | public int getMinute() { 1213 | return mMinute; 1214 | } 1215 | 1216 | /** 1217 | * get second 1218 | * 1219 | * @return current second 1220 | */ 1221 | public int getSecond() { 1222 | return mSecond; 1223 | } 1224 | 1225 | /** 1226 | * get remain time 1227 | * 1228 | * @return remain time ( millisecond ) 1229 | */ 1230 | public long getRemainTime() { 1231 | return mRemainTime; 1232 | } 1233 | 1234 | public void updateShow(long ms) { 1235 | 1236 | this.mRemainTime = ms; 1237 | 1238 | mDay = (int) (ms / (1000 * 60 * 60 * 24)); 1239 | mHour = (int) ((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 1240 | mMinute = (int) ((ms % (1000 * 60 * 60)) / (1000 * 60)); 1241 | mSecond = (int) ((ms % (1000 * 60)) / 1000); 1242 | mMillisecond = (int) (ms % 1000); 1243 | // interval callback 1244 | if (mInterval > 0 && null != mOnCountdownIntervalListener) { 1245 | if (mPreviouIntervalCallbackTime == 0) { 1246 | mPreviouIntervalCallbackTime = ms; 1247 | } else if (ms + mInterval <= mPreviouIntervalCallbackTime) { 1248 | mPreviouIntervalCallbackTime = ms; 1249 | mOnCountdownIntervalListener.onInterval(this, mRemainTime); 1250 | } 1251 | } 1252 | 1253 | handlerAutoShowTimeAndDayLargeNinetyNine(); 1254 | 1255 | invalidate(); 1256 | } 1257 | 1258 | private void handlerAutoShowTimeAndDayLargeNinetyNine() { 1259 | if (!mHasSetIsShowDay) { 1260 | if (!isShowDay && mDay > 0) { 1261 | // auto show day 1262 | // judge auto show hour 1263 | if (!mHasSetIsShowHour) { 1264 | refTimeShow(true, true, isShowMinute, isShowSecond, isShowMillisecond); 1265 | } else { 1266 | refTimeShow(true, isShowHour, isShowMinute, isShowSecond, isShowMillisecond); 1267 | } 1268 | } else if (isShowDay && mDay == 0) { 1269 | // auto hide day 1270 | refTimeShow(false, isShowHour, isShowMinute, isShowSecond, isShowMillisecond); 1271 | } else { 1272 | if (!mHasSetIsShowHour) { 1273 | if (!isShowHour && (mDay > 0 || mHour > 0)) { 1274 | // auto show hour 1275 | refTimeShow(isShowDay, true, isShowMinute, isShowSecond, isShowMillisecond); 1276 | } else if (isShowHour && mDay == 0 && mHour == 0) { 1277 | // auto hide hour 1278 | refTimeShow(false, false, isShowMinute, isShowSecond, isShowMillisecond); 1279 | } 1280 | } 1281 | } 1282 | } else { 1283 | if (!mHasSetIsShowHour) { 1284 | if (!isShowHour && (mDay > 0 || mHour > 0)) { 1285 | // auto show hour 1286 | refTimeShow(isShowDay, true, isShowMinute, isShowSecond, isShowMillisecond); 1287 | } else if (isShowHour && mDay == 0 && mHour == 0) { 1288 | // auto hide hour 1289 | refTimeShow(isShowDay, false, isShowMinute, isShowSecond, isShowMillisecond); 1290 | } 1291 | } 1292 | } 1293 | 1294 | if (isShowDay) { 1295 | // handler large ninety nine 1296 | if (!isDayLargeNinetyNine && mDay > 99) { 1297 | isDayLargeNinetyNine = true; 1298 | requestLayout(); 1299 | } else if (isDayLargeNinetyNine && mDay <= 99) { 1300 | isDayLargeNinetyNine = false; 1301 | requestLayout(); 1302 | } 1303 | } 1304 | } 1305 | 1306 | private String formatNum(int time) { 1307 | return time < 10 ? "0" + time : String.valueOf(time); 1308 | } 1309 | 1310 | private String formatMillisecond() { 1311 | String retMillisecondStr; 1312 | 1313 | if (mMillisecond > 99) { 1314 | retMillisecondStr = String.valueOf(mMillisecond / 10); 1315 | } else if (mMillisecond <= 9) { 1316 | retMillisecondStr = "0" + mMillisecond; 1317 | } else { 1318 | retMillisecondStr = String.valueOf(mMillisecond); 1319 | } 1320 | 1321 | return retMillisecondStr; 1322 | } 1323 | 1324 | public interface OnCountdownEndListener { 1325 | void onEnd(CountupView cv); 1326 | } 1327 | 1328 | public interface OnCountdownIntervalListener { 1329 | void onInterval(CountupView cv, long remainTime); 1330 | } 1331 | 1332 | private int dp2px(float dpValue) { 1333 | final float scale = mContext.getResources().getDisplayMetrics().density; 1334 | return (int) (dpValue * scale + 0.5f); 1335 | } 1336 | 1337 | private float sp2px(float spValue) { 1338 | final float scale = mContext.getResources().getDisplayMetrics().scaledDensity; 1339 | return spValue * scale; 1340 | } 1341 | } 1342 | -------------------------------------------------------------------------------- /library/src/main/java/cn/iwgang/countuptime/CustomCountUpTimer.java: -------------------------------------------------------------------------------- 1 | package cn.iwgang.countuptime; 2 | 3 | import android.os.Handler; 4 | import android.os.Message; 5 | import android.os.SystemClock; 6 | 7 | /** 8 | * 改成SystemClock.elapsedRealtime()计算时间,即使放在listview或者recyclerview中被回收了始终也不会停止 9 | * listview的demo等着有空改进下。 10 | * Created by hanjiahu on 16/5/11. 11 | */ 12 | public abstract class CustomCountUpTimer { 13 | private static final int MSG = 1; 14 | private long mMillisInFuture; 15 | private final long mCountupInterval; 16 | private long mStopTimeInFuture; 17 | private long mPauseTimeInFuture; 18 | private boolean isStop = false; 19 | private boolean isPause = false; 20 | private boolean started = false; 21 | 22 | public CustomCountUpTimer(long millisInFuture, long countDownInterval) { 23 | mMillisInFuture = millisInFuture; 24 | mCountupInterval = countDownInterval; 25 | } 26 | 27 | private synchronized CustomCountUpTimer start(long millisInFuture) { 28 | isStop = false; 29 | if (millisInFuture <= 0) { 30 | onFinish(); 31 | return this; 32 | } 33 | mStopTimeInFuture = SystemClock.elapsedRealtime() - millisInFuture; 34 | mHandler.removeMessages(MSG); 35 | mHandler.sendMessage(mHandler.obtainMessage(MSG)); 36 | return this; 37 | } 38 | 39 | /** 40 | * 开始倒计时 41 | */ 42 | public synchronized final void start() { 43 | start(mMillisInFuture); 44 | } 45 | 46 | /** 47 | * 停止倒计时 48 | */ 49 | public synchronized final void stop() { 50 | isStop = true; 51 | mHandler.removeMessages(MSG); 52 | } 53 | 54 | /** 55 | * 暂时倒计时 56 | * 调用{@link #restart()}方法重新开始 57 | */ 58 | public synchronized final void pause() { 59 | if (isStop) return; 60 | 61 | isPause = true; 62 | mPauseTimeInFuture = SystemClock.elapsedRealtime() - mStopTimeInFuture; 63 | mHandler.removeMessages(MSG); 64 | } 65 | 66 | /** 67 | * 重新开始 68 | */ 69 | public synchronized final void restart() { 70 | if (isStop || !isPause) return; 71 | 72 | isPause = false; 73 | start(mPauseTimeInFuture); 74 | } 75 | 76 | public boolean isStarted() { 77 | return started; 78 | } 79 | 80 | public synchronized final void recycledstart() { 81 | isStop = false; 82 | isPause = false; 83 | mHandler.removeMessages(MSG); 84 | mHandler.sendMessage(mHandler.obtainMessage(MSG)); 85 | } 86 | 87 | /** 88 | * 倒计时间隔回调 89 | * 90 | * @param millisUntilFinished 剩余毫秒数 91 | */ 92 | public abstract void onTick(long millisUntilFinished); 93 | 94 | /** 95 | * 倒计时结束回调 96 | */ 97 | public abstract void onFinish(); 98 | 99 | private Handler mHandler = new Handler() { 100 | 101 | @Override public void handleMessage(Message msg) { 102 | 103 | synchronized (CustomCountUpTimer.this) { 104 | if (isStop || isPause) { 105 | return; 106 | } 107 | 108 | final long millisLeft = SystemClock.elapsedRealtime() - mStopTimeInFuture; 109 | if (millisLeft <= 0) { 110 | onFinish(); 111 | } else { 112 | onTick(millisLeft); 113 | sendMessageDelayed(obtainMessage(MSG), mCountupInterval); 114 | } 115 | } 116 | } 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /library/src/main/java/cn/iwgang/countuptime/LogUtils.java: -------------------------------------------------------------------------------- 1 | package cn.iwgang.countuptime; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * 统一管理log类 7 | */ 8 | public class LogUtils { 9 | 10 | private static final boolean LOGGER = true;// 11 | 12 | private static String getVersion() { 13 | return " version is " + ""; 14 | } 15 | 16 | public static void v(String tag,String mess) { 17 | if (LOGGER) { 18 | Log.v(tag ,getTags()+" "+ mess + getVersion()); 19 | } 20 | } 21 | 22 | public static void d(String tag,String mess) { 23 | if (LOGGER) { 24 | Log.d(tag ,getTags()+" "+ mess + getVersion()); 25 | } 26 | } 27 | 28 | public static void i(String tag,String mess) { 29 | if (LOGGER) { 30 | Log.i(tag ,getTags()+" "+ mess + getVersion()); 31 | } 32 | } 33 | 34 | public static void w(String tag,String mess) { 35 | if (LOGGER) { 36 | Log.w(tag ,getTags()+" "+ mess + getVersion()); 37 | } 38 | } 39 | 40 | public static void e(String tag,String mess) { 41 | if (LOGGER) { 42 | Log.e(tag ,getTags()+" "+ mess + getVersion()); 43 | } 44 | } 45 | 46 | public static void e(String tag, String msg, Throwable tr) { 47 | if (LOGGER) { 48 | Log.e(tag, msg + getVersion()); 49 | } 50 | } 51 | 52 | public static void v(String mess) { 53 | if (LOGGER) { 54 | v("",mess); 55 | } 56 | } 57 | 58 | public static void d(String mess) { 59 | if (LOGGER) { 60 | d("",mess); 61 | } 62 | } 63 | 64 | public static void i(String mess) { 65 | if (LOGGER) { 66 | i("",mess); 67 | } 68 | } 69 | 70 | public static void w(String mess) { 71 | if (LOGGER) { 72 | w("",mess); 73 | } 74 | } 75 | 76 | public static void e(String mess) { 77 | if (LOGGER) { 78 | e("",mess); 79 | } 80 | } 81 | 82 | /** 83 | * 获取日志的标签 格式:类名_方法名_行号 (需要权限:android.permission.GET_TASKS) 84 | * 85 | * @return tag 86 | * @see [类、类#方法、类#成员] 87 | */ 88 | /** 89 | * 异常栈位移 90 | */ 91 | private static final int EXCEPTION_STACK_INDEX = 2; 92 | private static String getTags() 93 | { 94 | try 95 | { 96 | Exception exception = new LogException(); 97 | if (exception.getStackTrace() == null || exception.getStackTrace().length <= EXCEPTION_STACK_INDEX) 98 | { 99 | return "***"; 100 | } 101 | StackTraceElement element = exception.getStackTrace()[EXCEPTION_STACK_INDEX]; 102 | String className = element.getClassName(); 103 | int index = className.lastIndexOf("."); 104 | if (index > 0) 105 | { 106 | className = className.substring(index + 1); 107 | } 108 | return className + "_" + element.getMethodName() + "_" + element.getLineNumber(); 109 | } 110 | catch (Throwable e) 111 | { 112 | e.printStackTrace(); 113 | return "***"; 114 | } 115 | } 116 | 117 | /** 118 | * 取日志标签用的的异常类,只是用于取得日志标签 119 | */ 120 | private static class LogException extends Exception 121 | { 122 | /** 123 | * 注释内容 124 | */ 125 | private static final long serialVersionUID = 1L; 126 | } 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Library 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /screenshot/androidCountUpTimeView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/screenshot/androidCountUpTimeView.gif -------------------------------------------------------------------------------- /screenshot/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackuhan/AndroidCountUpTimerView/f48e3f19a0d2ee46beda9e6bf64622b9295e5dc2/screenshot/screenshot2.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------