├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lishang │ │ └── checkin │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── lishang │ │ │ └── checkin │ │ │ ├── MainActivity.java │ │ │ └── bean │ │ │ └── CheckIn.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── icon_date_checkin.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── lishang │ └── checkin │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat ├── img └── img.jpg ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lishang │ │ └── library │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── lishang │ │ │ └── checkin │ │ │ ├── CheckInProgress.java │ │ │ └── OnClickCheckInListener.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── lishang │ └── library │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 一款签到控件 2 | 引用 3 | 4 | implementation 'com.lishang:checkInProgress:1.0.1' 5 | 6 | 7 | 8 | | 属性 | 类型 | 描述 | 9 | | ------ | ------ | ------ | 10 | | text_date_size | sp | 日期文字大小 | 11 | | text_date_color | color | 日期文字颜色 | 12 | | radius | dp | 签到圆半径 | 13 | | circle_color | color | 圆的背景色 | 14 | | line_height | dp | 线高 | 15 | | line_color | color | 线的颜色 | 16 | | text_score_size | sp | 签到积分字体大小 | 17 | | text_score_color | color | 签到积分文字颜色 | 18 | | check_in_bitmap | drawble | 签到后的图片 | 19 | | check_in_color | color | 没有签到图片时,签到的颜色 | 20 | | check_in_hook_color | color | 没有签到图片时,签到内部勾的颜色| 21 | | check_in_hook_size | dp | 没有签到图片时,签到内部勾的大小 | 22 | | circle_margin | dp | 签到圆顶部与日期字体距离 | 23 | | circle_stroke_width | dp | 签到圆描边宽度 | 24 | | circle_stroke_color | color | 签到圆边描颜色 | 25 | | check_in_progress_show | boolean | 是否显示签到进度 | 26 | | check_in_progress_color | color | 签到进度颜色 | 27 | | check_in_leak_show | boolean | 是否支持补签 | 28 | | circle_style | enum | 签到圆样式 fill 填充 stroke描边(circle_stroke_width、circle_stroke_color生效) | 29 | | align | enum | 位置 top/center/bottom | 30 | 31 | 54 | 55 | # 56 | 57 | checkIn.setAdapter(new CheckInProgress.Adapter() { 58 | /** 59 | * 日期 60 | * @param position 61 | * @return 62 | */ 63 | @Override 64 | public String getDateText(int position) { 65 | CheckIn in = list.get(position); 66 | return in.date; 67 | } 68 | 69 | /** 70 | * 积分 71 | * @param position 72 | * @return 73 | */ 74 | @Override 75 | public String getScoreText(int position) { 76 | CheckIn in = list.get(position); 77 | return in.score; 78 | } 79 | 80 | /** 81 | * 是否签到 82 | * @param position 83 | * @return 84 | */ 85 | @Override 86 | public boolean isCheckIn(int position) { 87 | CheckIn in = list.get(position); 88 | return in.isCheckIn; 89 | } 90 | 91 | /** 92 | * 数量 93 | * @return 94 | */ 95 | @Override 96 | public int size() { 97 | return list.size(); 98 | } 99 | 100 | /** 101 | * 是否支持补签 102 | * @param position 103 | * @return 104 | */ 105 | @Override 106 | public boolean isLeakCheckIn(int position) { 107 | CheckIn in = list.get(position); 108 | 109 | return in.isLeakChekIn; 110 | } 111 | }); 112 | 113 | # 114 | checkIn.setOnClickCheckInListener(new OnClickCheckInListener() { 115 | @Override 116 | public void OnClick(int position) { 117 | CheckIn checkIn = list1.get(position); 118 | if (checkIn.isCheckIn) { 119 | Toast.makeText(getApplicationContext(), "已签到", Toast.LENGTH_SHORT).show(); 120 | } else { 121 | if (checkIn.isLeakChekIn) { 122 | Toast.makeText(getApplicationContext(), "补卡", Toast.LENGTH_SHORT).show(); 123 | checkIn.isLeakChekIn = false; 124 | checkIn.isCheckIn = true; 125 | 126 | Log.e("CheckIn", Arrays.toString(list1.toArray())); 127 | 128 | checkIn.getAdapter().notifyDataSetChanged(); 129 | } else { 130 | Toast.makeText(getApplicationContext(), "签到", Toast.LENGTH_SHORT).show(); 131 | } 132 | } 133 | } 134 | }); -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.lishang.checkin" 7 | minSdkVersion 15 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | // implementation project(path: ':library') 29 | 30 | implementation 'com.lishang:checkInProgress:1.0.1' 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lishang/checkin/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin; 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 | * Instrumented 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() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.lishang.checkin", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/lishang/checkin/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.widget.Toast; 7 | 8 | import com.lishang.checkin.bean.CheckIn; 9 | 10 | import java.text.SimpleDateFormat; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Calendar; 14 | import java.util.Date; 15 | import java.util.List; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | 19 | private CheckInProgress checkIn; 20 | private CheckInProgress checkIn1; 21 | 22 | private List list = new ArrayList<>(); 23 | private List list1 = new ArrayList<>(); 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_main); 29 | checkIn = findViewById(R.id.checkIn); 30 | checkIn1 = findViewById(R.id.checkIn_1); 31 | 32 | init(); 33 | 34 | checkIn.setAdapter(new CheckInProgress.Adapter() { 35 | 36 | @Override 37 | public String getDateText(int position) { 38 | CheckIn in = list.get(position); 39 | return in.date; 40 | } 41 | 42 | @Override 43 | public String getScoreText(int position) { 44 | CheckIn in = list.get(position); 45 | return in.score; 46 | } 47 | 48 | @Override 49 | public boolean isCheckIn(int position) { 50 | CheckIn in = list.get(position); 51 | return in.isCheckIn; 52 | } 53 | 54 | @Override 55 | public int size() { 56 | return list.size(); 57 | } 58 | }); 59 | 60 | checkIn1.setAdapter(new CheckInProgress.Adapter() { 61 | /** 62 | * 日期 63 | * @param position 64 | * @return 65 | */ 66 | @Override 67 | public String getDateText(int position) { 68 | CheckIn in = list.get(position); 69 | return in.date; 70 | } 71 | 72 | /** 73 | * 积分 74 | * @param position 75 | * @return 76 | */ 77 | @Override 78 | public String getScoreText(int position) { 79 | CheckIn in = list1.get(position); 80 | return in.score; 81 | } 82 | 83 | /** 84 | * 是否签到 85 | * @param position 86 | * @return 87 | */ 88 | @Override 89 | public boolean isCheckIn(int position) { 90 | CheckIn in = list1.get(position); 91 | return in.isCheckIn; 92 | } 93 | 94 | /** 95 | * 数量 96 | * @return 97 | */ 98 | @Override 99 | public int size() { 100 | return list1.size(); 101 | } 102 | 103 | /** 104 | * 是否支持补签 105 | * @param position 106 | * @return 107 | */ 108 | @Override 109 | public boolean isLeakCheckIn(int position) { 110 | CheckIn in = list1.get(position); 111 | 112 | return in.isLeakChekIn; 113 | } 114 | }); 115 | 116 | 117 | checkIn1.setOnClickCheckInListener(new OnClickCheckInListener() { 118 | @Override 119 | public void OnClick(int position) { 120 | CheckIn checkIn = list1.get(position); 121 | if (checkIn.isCheckIn) { 122 | Toast.makeText(getApplicationContext(), "已签到", Toast.LENGTH_SHORT).show(); 123 | } else { 124 | if (checkIn.isLeakChekIn) { 125 | Toast.makeText(getApplicationContext(), "补卡", Toast.LENGTH_SHORT).show(); 126 | checkIn.isLeakChekIn = false; 127 | checkIn.isCheckIn = true; 128 | 129 | Log.e("CheckIn", Arrays.toString(list1.toArray())); 130 | 131 | checkIn1.getAdapter().notifyDataSetChanged(); 132 | } else { 133 | Toast.makeText(getApplicationContext(), "签到", Toast.LENGTH_SHORT).show(); 134 | } 135 | } 136 | } 137 | }); 138 | 139 | } 140 | 141 | private void init() { 142 | for (int i = 0; i < 7; i++) { 143 | CheckIn checkIn = new CheckIn(); 144 | checkIn.date = "06.2" + i; 145 | if (i == 3) { 146 | checkIn.date = "今日"; 147 | } 148 | checkIn.score = "" + (i + 1); 149 | checkIn.isCheckIn = i < 4; 150 | list.add(checkIn); 151 | } 152 | 153 | for (int i = 0; i < 7; i++) { 154 | CheckIn checkIn = new CheckIn(); 155 | checkIn.date = "06.2" + i; 156 | if (i == 3) { 157 | checkIn.date = "今日"; 158 | } 159 | checkIn.score = "" + (i + 1); 160 | checkIn.isCheckIn = i < 4 && i != 1; 161 | checkIn.isLeakChekIn = i == 1; 162 | list1.add(checkIn); 163 | } 164 | } 165 | 166 | 167 | private void initDate() { 168 | SimpleDateFormat sdf = new SimpleDateFormat("MM.dd"); 169 | 170 | Calendar cal = Calendar.getInstance(); 171 | cal.setTime(new Date()); 172 | // 判断要计算的日期是否是周日,如果是则减一天计算周六的,否则会出问题,计算到下一周去了 173 | int dayWeek = cal.get(Calendar.DAY_OF_WEEK);// 获得当前日期是一个星期的第几天 174 | if (1 == dayWeek) { 175 | cal.add(Calendar.DAY_OF_MONTH, -1); 176 | } 177 | // 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一 178 | cal.setFirstDayOfWeek(Calendar.MONDAY); 179 | // 获得当前日期是一个星期的第几天 180 | int day = cal.get(Calendar.DAY_OF_WEEK); 181 | // 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值 182 | cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day); 183 | Calendar start = Calendar.getInstance(); 184 | start.setTime(cal.getTime()); 185 | int score = 1; 186 | if (isToday(start)) { 187 | 188 | CheckIn in = new CheckIn(); 189 | in.date = "今日"; 190 | in.score = "" + score; 191 | list.add(in); 192 | } else { 193 | CheckIn in = new CheckIn(); 194 | in.date = sdf.format(start.getTime()); 195 | in.score = "" + score; 196 | list.add(in); 197 | } 198 | 199 | cal.add(Calendar.DATE, 6); 200 | Calendar end = Calendar.getInstance(); 201 | end.setTime(cal.getTime()); 202 | while (end.after(start)) { 203 | // 根据日历的规则,为给定的日历字段添加或减去指定的时间量 204 | start.add(Calendar.DAY_OF_MONTH, 1); 205 | score++; 206 | 207 | if (isToday(start)) { 208 | CheckIn in = new CheckIn(); 209 | in.date = "今日"; 210 | in.score = "" + score; 211 | list.add(in); 212 | } else { 213 | CheckIn in = new CheckIn(); 214 | in.date = sdf.format(start.getTime()); 215 | in.score = "" + score; 216 | list.add(in); 217 | } 218 | } 219 | } 220 | 221 | private boolean isToday(Calendar other) { 222 | Calendar calendar = Calendar.getInstance(); 223 | 224 | if (other.get(Calendar.YEAR) == (calendar.get(Calendar.YEAR))) { 225 | int diffDay = other.get(Calendar.DAY_OF_YEAR) 226 | - calendar.get(Calendar.DAY_OF_YEAR); 227 | if (diffDay == 0) { 228 | return true; 229 | } 230 | } 231 | return false; 232 | } 233 | 234 | 235 | } 236 | -------------------------------------------------------------------------------- /app/src/main/java/com/lishang/checkin/bean/CheckIn.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin.bean; 2 | 3 | public class CheckIn { 4 | public String date; 5 | public String score; 6 | public boolean isCheckIn; 7 | public boolean isLeakChekIn; //漏签 8 | 9 | 10 | @Override 11 | public String toString() { 12 | return "CheckIn{" + 13 | "date='" + date + '\'' + 14 | ", score='" + score + '\'' + 15 | ", isCheckIn=" + isCheckIn + 16 | ", isLeakChekIn=" + isLeakChekIn + 17 | '}'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_date_checkin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/drawable/icon_date_checkin.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 33 | 34 | 35 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CheckIn 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/lishang/checkin/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin; 2 | 3 | import org.junit.Test; 4 | 5 | import java.text.SimpleDateFormat; 6 | import java.util.Calendar; 7 | import java.util.Date; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | /** 12 | * Example local unit test, which will execute on the development machine (host). 13 | * 14 | * @see Testing documentation 15 | */ 16 | public class ExampleUnitTest { 17 | @Test 18 | public void addition_isCorrect() { 19 | assertEquals(4, 2 + 2); 20 | getTimeInterval(new Date()); 21 | } 22 | 23 | public void getTimeInterval(Date date) { 24 | SimpleDateFormat sdf = new SimpleDateFormat("MM.dd"); 25 | 26 | Calendar cal = Calendar.getInstance(); 27 | cal.setTime(date); 28 | // 判断要计算的日期是否是周日,如果是则减一天计算周六的,否则会出问题,计算到下一周去了 29 | int dayWeek = cal.get(Calendar.DAY_OF_WEEK);// 获得当前日期是一个星期的第几天 30 | if (1 == dayWeek) { 31 | cal.add(Calendar.DAY_OF_MONTH, -1); 32 | } 33 | // 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一 34 | cal.setFirstDayOfWeek(Calendar.MONDAY); 35 | // 获得当前日期是一个星期的第几天 36 | int day = cal.get(Calendar.DAY_OF_WEEK); 37 | // 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值 38 | cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day); 39 | Calendar start = Calendar.getInstance(); 40 | start.setTime(cal.getTime()); 41 | // System.out.println(sdf.format(start.getTime())); 42 | 43 | cal.add(Calendar.DATE, 6); 44 | Calendar end = Calendar.getInstance(); 45 | end.setTime(cal.getTime()); 46 | while (end.after(start)) { 47 | // 根据日历的规则,为给定的日历字段添加或减去指定的时间量 48 | start.add(Calendar.DAY_OF_MONTH, 1); 49 | if (isToday(start)) { 50 | System.out.println(sdf.format(start.getTime()) + " 今天"); 51 | } else { 52 | System.out.println(sdf.format(start.getTime())); 53 | } 54 | 55 | 56 | } 57 | 58 | } 59 | 60 | public boolean isToday(Calendar other) { 61 | Calendar calendar = Calendar.getInstance(); 62 | 63 | if (other.get(Calendar.YEAR) == (calendar.get(Calendar.YEAR))) { 64 | int diffDay = other.get(Calendar.DAY_OF_YEAR) 65 | - calendar.get(Calendar.DAY_OF_YEAR); 66 | 67 | if (diffDay == 0) { 68 | return true; 69 | } 70 | } 71 | return false; 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /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 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.1' 11 | classpath 'com.novoda:bintray-release:0.9' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | 16 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /img/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiShang007/CheckIn/1ff74c8114df4bb5e7e2dcd4df85930dcee4b0b5/img/img.jpg -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' 3 | 4 | android { 5 | compileSdkVersion 28 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 15 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'com.android.support:appcompat-v7:28.0.0' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 33 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 34 | } 35 | 36 | publish { 37 | userOrg = 'lishang007' 38 | groupId = 'com.lishang' 39 | repoName = "maven" 40 | artifactId = 'checkInProgress' 41 | publishVersion = '1.0.1' 42 | desc = '签到进度' 43 | website = 'https://github.com/LiShang007/CheckIn' 44 | } 45 | 46 | task javadoc(type: Javadoc) { 47 | options.encoding = "utf-8" 48 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/lishang/library/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.lishang.library; 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 | * Instrumented 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() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.lishang.library.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /library/src/main/java/com/lishang/checkin/CheckInProgress.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.database.Observable; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.graphics.Canvas; 9 | import android.graphics.Matrix; 10 | import android.graphics.Paint; 11 | import android.graphics.Point; 12 | import android.graphics.Rect; 13 | import android.support.annotation.NonNull; 14 | import android.support.annotation.Nullable; 15 | import android.util.AttributeSet; 16 | import android.util.Log; 17 | import android.util.SparseArray; 18 | import android.util.TypedValue; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | import android.widget.Adapter; 22 | 23 | import com.lishang.library.R; 24 | 25 | 26 | /** 27 | * 签到进度 28 | */ 29 | public class CheckInProgress extends View { 30 | private final static String TAG = "CheckInProgress"; 31 | 32 | private int textDateSize = 12; //日期文字大小sp 33 | private int textDateColor = 0xff8e8e8e; 34 | private int radius = 9;// 半径 dp 35 | private int circleColor = 0xffFF4441; 36 | private int lineHeight = 1;//背景线的高度 37 | private int lineColor = 0xfffF4241; 38 | private int textScoreSize = 12; 39 | private int textScoreColor = 0xff8e8e8e; 40 | private Bitmap checkIn; 41 | private int circleMargin = 10; //圆与日期间隔 42 | private Paint.Style circleStyle = Paint.Style.FILL; 43 | private int circleStrokeWidth = 1; 44 | private int circleStrokeColor = 0xffffffff; 45 | private boolean checkInProgressShow = false; //是否显示签到进度 46 | private int checkInProgressColor = lineColor; 47 | private int checkInColor = checkInProgressColor; 48 | private int checkInHookColor = lineColor; 49 | private int checkInHookSize = circleStrokeWidth; 50 | private boolean checkInLeakShow = false; //是否显示漏签 51 | 52 | 53 | private Paint datePaint; //日期画笔 54 | private Paint scorePaint;//分数画笔 55 | private Paint linePaint;//线画笔 56 | 57 | private Align align = Align.TOP; //对齐方式 58 | private int verticalHeight; //从日期到积分垂直高度; 59 | 60 | 61 | private SparseArray datePointPool = new SparseArray<>(); //记录日期位置 62 | private SparseArray scorePointPool = new SparseArray<>(); //记录分数位置 63 | private SparseArray circlePointPool = new SparseArray<>(); //记录圆位置 64 | private Adapter adapter; 65 | 66 | private Point downPoint = new Point(); 67 | private OnClickCheckInListener listener; 68 | private CheckInProgressDataObserver mObserver; 69 | 70 | enum Align { 71 | TOP, CENTER, BOTTOM 72 | } 73 | 74 | 75 | public CheckInProgress(Context context) { 76 | this(context, null); 77 | } 78 | 79 | public CheckInProgress(Context context, @Nullable AttributeSet attrs) { 80 | this(context, attrs, 0); 81 | } 82 | 83 | public CheckInProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 84 | super(context, attrs, defStyleAttr); 85 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CheckInProgress); 86 | int align = array.getInt(R.styleable.CheckInProgress_align, 1); 87 | if (align == 3) { 88 | this.align = Align.BOTTOM; 89 | } else if (align == 2) { 90 | this.align = Align.CENTER; 91 | } else { 92 | this.align = Align.TOP; 93 | } 94 | textDateSize = array.getDimensionPixelOffset(R.styleable.CheckInProgress_text_date_size, textDateSize); 95 | textDateColor = array.getColor(R.styleable.CheckInProgress_text_date_color, textDateColor); 96 | radius = array.getDimensionPixelOffset(R.styleable.CheckInProgress_radius, radius); 97 | circleColor = array.getColor(R.styleable.CheckInProgress_circle_color, circleColor); 98 | lineHeight = array.getDimensionPixelOffset(R.styleable.CheckInProgress_line_height, lineHeight); 99 | lineColor = array.getColor(R.styleable.CheckInProgress_line_color, lineColor); 100 | textScoreSize = array.getDimensionPixelOffset(R.styleable.CheckInProgress_text_score_size, textScoreSize); 101 | textScoreColor = array.getColor(R.styleable.CheckInProgress_text_score_color, textScoreColor); 102 | int resId = array.getResourceId(R.styleable.CheckInProgress_check_in_bitmap, -1); 103 | if (resId != -1) { 104 | checkIn = BitmapFactory.decodeResource(getResources(), resId); 105 | } 106 | circleMargin = array.getDimensionPixelOffset(R.styleable.CheckInProgress_circle_margin, circleMargin); 107 | circleStrokeWidth = array.getDimensionPixelOffset(R.styleable.CheckInProgress_circle_stroke_width, circleStrokeWidth); 108 | circleStrokeColor = array.getColor(R.styleable.CheckInProgress_circle_stroke_color, circleStrokeColor); 109 | int style = array.getInt(R.styleable.CheckInProgress_circle_style, 0); 110 | switch (style) { 111 | case 1: 112 | circleStyle = Paint.Style.STROKE; 113 | break; 114 | default: 115 | circleStyle = Paint.Style.FILL; 116 | break; 117 | } 118 | 119 | checkInProgressShow = array.getBoolean(R.styleable.CheckInProgress_check_in_progress_show, checkInProgressShow); 120 | checkInProgressColor = array.getColor(R.styleable.CheckInProgress_check_in_progress_color, checkInProgressColor); 121 | checkInColor = array.getColor(R.styleable.CheckInProgress_check_in_color, checkInColor); 122 | checkInHookColor = array.getColor(R.styleable.CheckInProgress_check_in_hook_color, checkInHookColor); 123 | checkInHookSize = array.getDimensionPixelOffset(R.styleable.CheckInProgress_check_in_hook_size, checkInHookSize); 124 | checkInLeakShow = array.getBoolean(R.styleable.CheckInProgress_check_in_leak_show, checkInLeakShow); 125 | 126 | array.recycle(); 127 | init(); 128 | } 129 | 130 | private void init() { 131 | mObserver = new CheckInProgressDataObserver(); 132 | 133 | linePaint = new Paint(); 134 | linePaint.setAntiAlias(true); 135 | linePaint.setStrokeWidth((lineHeight)); 136 | linePaint.setColor(lineColor); 137 | 138 | scorePaint = new Paint(); 139 | scorePaint.setAntiAlias(true); 140 | 141 | datePaint = new Paint(); 142 | datePaint.setAntiAlias(true); 143 | datePaint.setTextAlign(Paint.Align.CENTER); 144 | datePaint.setTextSize((textDateSize)); 145 | 146 | } 147 | 148 | @Override 149 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 150 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 151 | //计算元素位置 152 | onCalculation(); 153 | 154 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 155 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 156 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 157 | 158 | if (heightMode == MeasureSpec.AT_MOST && verticalHeight != 0) { 159 | heightSize = verticalHeight; 160 | } 161 | 162 | setMeasuredDimension(widthSize, heightSize); 163 | } 164 | 165 | /** 166 | * 先计算好画布上每个元素的位置 167 | */ 168 | private void onCalculation() { 169 | if (adapter == null) return; 170 | calculationDate(); 171 | calculationScore(); 172 | 173 | //元素垂直高度 174 | int total = datePointPool.get(0).y - getPaddingTop(); //日期的高度 175 | total += (circleMargin); // + 间距 176 | total += (radius) * 2; //+积分圆的直径 177 | verticalHeight = total; 178 | 179 | } 180 | 181 | /** 182 | * 日期元素位置 183 | */ 184 | private void calculationDate() { 185 | int left = getPaddingLeft(); 186 | int right = getPaddingRight(); 187 | int top = getPaddingTop(); 188 | 189 | int width = getMeasuredWidth() - left - right; 190 | 191 | 192 | int margin = width / (adapter.size()); 193 | 194 | //日期位置 195 | int cy = 0; 196 | for (int i = 0; i < adapter.size(); i++) { 197 | 198 | String str = adapter.getDateText(i); 199 | Rect rect = new Rect(); 200 | datePaint.getTextBounds(str, 0, str.length(), rect); 201 | int y = top + rect.height(); 202 | if (cy < y) { 203 | cy = y; 204 | } 205 | } 206 | 207 | for (int i = 0; i < adapter.size(); i++) { 208 | int cx = left + margin / 2 + i * margin; 209 | Point point = new Point(cx, cy); 210 | datePointPool.put(i, point); 211 | } 212 | } 213 | 214 | /** 215 | * 积分元素位置 216 | */ 217 | private void calculationScore() { 218 | 219 | int radiusPx = (radius); 220 | int left = datePointPool.get(0).x; 221 | int right = datePointPool.get(datePointPool.size() - 1).x; 222 | int top = datePointPool.get(0).y; 223 | 224 | int width = right - left; 225 | int cy = top + radiusPx + (circleMargin); 226 | 227 | int margin = width / (adapter.size() - 1); 228 | for (int i = 0; i < adapter.size(); i++) { 229 | 230 | int cx = left + i * margin; 231 | Point p = new Point(cx, cy); 232 | circlePointPool.put(i, p); 233 | 234 | scorePaint.setTextSize((textScoreSize)); 235 | scorePaint.setStyle(Paint.Style.FILL); 236 | scorePaint.setColor(textScoreColor); 237 | scorePaint.setTextAlign(Paint.Align.CENTER); 238 | String str = "+" + adapter.getScoreText(i); 239 | if (adapter.isLeakCheckIn(i) && checkInLeakShow) { 240 | str = "补"; 241 | } 242 | Rect rect = new Rect(); 243 | scorePaint.getTextBounds(str, 0, str.length(), rect); 244 | 245 | Paint.FontMetricsInt fontMetrics = scorePaint.getFontMetricsInt(); 246 | 247 | Point point = new Point(p.x, p.y + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent); 248 | scorePointPool.put(i, point); 249 | } 250 | 251 | } 252 | 253 | private int calculationAlign() { 254 | int margin = 0; 255 | if (align == Align.CENTER) { 256 | margin = (getMeasuredHeight() - verticalHeight) / 2 - getPaddingTop(); 257 | } else if (align == Align.BOTTOM) { 258 | margin = (getMeasuredHeight() - verticalHeight) - getPaddingTop(); 259 | } 260 | return margin; 261 | } 262 | 263 | @Override 264 | protected void onDraw(Canvas canvas) { 265 | super.onDraw(canvas); 266 | 267 | drawDate(canvas); 268 | drawBgLine(canvas); 269 | drawScore(canvas); 270 | 271 | 272 | } 273 | 274 | /** 275 | * 画日期 276 | * 277 | * @param canvas 278 | */ 279 | private void drawDate(Canvas canvas) { 280 | 281 | int margin = calculationAlign(); 282 | if (datePointPool.size() != 0) { 283 | 284 | for (int i = 0; i < adapter.size(); i++) { 285 | 286 | String str = adapter.getDateText(i); 287 | 288 | datePaint.setColor(textDateColor); 289 | 290 | Point point = datePointPool.get(i); 291 | 292 | canvas.drawText(str, point.x, point.y + margin, datePaint); 293 | 294 | } 295 | 296 | } 297 | } 298 | 299 | private void drawBgLine(Canvas canvas) { 300 | int margin = calculationAlign(); 301 | 302 | int startX = datePointPool.get(0).x; 303 | int startY = datePointPool.get(0).y + (radius) + (circleMargin) + margin; 304 | int stopX = datePointPool.get(datePointPool.size() - 1).x; 305 | int stopY = startY; 306 | canvas.drawLine(startX, startY, stopX, stopY, linePaint); 307 | 308 | 309 | } 310 | 311 | private void drawScore(Canvas canvas) { 312 | int radiusPx = (radius); 313 | int margin = calculationAlign(); 314 | 315 | for (int i = 0; i < adapter.size(); i++) { 316 | Point p = circlePointPool.get(i); 317 | 318 | 319 | if (adapter.isCheckIn(i)) { 320 | 321 | if (checkInProgressShow && i + 1 < adapter.size()) { 322 | //进度 323 | scorePaint.setStyle(Paint.Style.FILL); 324 | scorePaint.setColor(checkInProgressColor); 325 | scorePaint.setStrokeWidth((lineHeight)); 326 | 327 | Point p1 = circlePointPool.get(i + 1); 328 | canvas.drawLine(p.x, p.y + margin, p1.x, p1.y + margin, scorePaint); 329 | } 330 | 331 | if (checkIn != null) { 332 | float scale = radiusPx * 2.0f / checkIn.getWidth(); 333 | Matrix matrix = new Matrix(); 334 | matrix.postScale(scale, scale); 335 | canvas.save(); 336 | canvas.translate(p.x - radiusPx, p.y + margin - radiusPx); 337 | canvas.drawBitmap(checkIn, matrix, scorePaint); 338 | canvas.restore(); 339 | } else { 340 | scorePaint.setColor(checkInColor); 341 | scorePaint.setStyle(Paint.Style.FILL); 342 | canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint); 343 | 344 | //画勾 345 | scorePaint.setStyle(Paint.Style.FILL); 346 | scorePaint.setColor(checkInHookColor); 347 | scorePaint.setStrokeWidth((checkInHookSize)); 348 | int startX = p.x - radiusPx / 4 * 3; 349 | int startY = p.y + margin; 350 | int stopX = p.x - radiusPx / 4; 351 | int stopY = p.y + margin + radiusPx / 2; 352 | canvas.drawLine(startX, startY, stopX, stopY, scorePaint); 353 | 354 | startX = stopX; 355 | startY = stopY; 356 | stopX = p.x + radiusPx / 4 * 3; 357 | stopY = p.y + margin - radiusPx / 2; 358 | canvas.drawLine(startX, startY, stopX, stopY, scorePaint); 359 | 360 | canvas.drawCircle(startX, startY, checkInHookSize / 2.0f, scorePaint); 361 | } 362 | 363 | 364 | } else { 365 | scorePaint.setStyle(Paint.Style.FILL); 366 | scorePaint.setColor(circleColor); 367 | canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint); 368 | 369 | if (circleStyle == Paint.Style.STROKE) { 370 | scorePaint.setColor(circleStrokeColor); 371 | scorePaint.setStrokeWidth((circleStrokeWidth)); 372 | scorePaint.setStyle(Paint.Style.STROKE); 373 | canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint); 374 | } 375 | 376 | scorePaint.setTextSize((textScoreSize)); 377 | scorePaint.setStyle(Paint.Style.FILL); 378 | scorePaint.setColor(textScoreColor); 379 | scorePaint.setTextAlign(Paint.Align.CENTER); 380 | String str = "+" + adapter.getScoreText(i); 381 | if (adapter.isLeakCheckIn(i) && checkInLeakShow) { 382 | str = "补"; 383 | } 384 | 385 | Point point = scorePointPool.get(i); 386 | 387 | canvas.drawText(str, point.x, point.y + margin, scorePaint); 388 | } 389 | 390 | } 391 | } 392 | 393 | public void setAdapter(Adapter adapter) { 394 | if (this.adapter != null) { 395 | adapter.unregisterAdapterDataObserver(mObserver); 396 | } 397 | if (adapter != null) { 398 | this.adapter = adapter; 399 | adapter.registerAdapterDataObserver(mObserver); 400 | requestLayout(); 401 | } 402 | } 403 | 404 | public Adapter getAdapter() { 405 | return adapter; 406 | } 407 | 408 | public void setOnClickCheckInListener(OnClickCheckInListener listener) { 409 | this.listener = listener; 410 | } 411 | 412 | public int getTextDateSize() { 413 | return textDateSize; 414 | } 415 | 416 | /** 417 | * 设置日期字体大小 418 | * 419 | * @param textDateSize 单位sp 420 | */ 421 | public void setTextDateSize(int textDateSize) { 422 | this.textDateSize = sp2px(textDateSize); 423 | } 424 | 425 | public int getTextDateColor() { 426 | return textDateColor; 427 | } 428 | 429 | /** 430 | * 日期字体颜色 431 | * 432 | * @param textDateColor 433 | */ 434 | public void setTextDateColor(int textDateColor) { 435 | this.textDateColor = textDateColor; 436 | } 437 | 438 | public int getRadius() { 439 | return radius; 440 | } 441 | 442 | /** 443 | * 签到圆的半径 444 | * 445 | * @param radius dp 446 | */ 447 | public void setRadius(int radius) { 448 | this.radius = dp2px(radius); 449 | } 450 | 451 | public int getCircleColor() { 452 | return circleColor; 453 | } 454 | 455 | /** 456 | * 签到圆的背景色 457 | * 458 | * @param circleColor 459 | */ 460 | public void setCircleColor(int circleColor) { 461 | this.circleColor = circleColor; 462 | } 463 | 464 | public int getLineHeight() { 465 | return lineHeight; 466 | } 467 | 468 | /** 469 | * 连接线高度 单位dp 470 | * 471 | * @param lineHeight 472 | */ 473 | public void setLineHeight(int lineHeight) { 474 | this.lineHeight = dp2px(lineHeight); 475 | } 476 | 477 | public int getLineColor() { 478 | return lineColor; 479 | } 480 | 481 | /** 482 | * 连接线颜色 483 | * 484 | * @param lineColor 485 | */ 486 | public void setLineColor(int lineColor) { 487 | this.lineColor = lineColor; 488 | } 489 | 490 | public int getTextScoreSize() { 491 | return textScoreSize; 492 | } 493 | 494 | /** 495 | * 设置签到积分字体大小 496 | * 497 | * @param textScoreSize 498 | */ 499 | public void setTextScoreSize(int textScoreSize) { 500 | this.textScoreSize = sp2px(textScoreSize); 501 | } 502 | 503 | public int getTextScoreColor() { 504 | return textScoreColor; 505 | } 506 | 507 | /** 508 | * 设置签到积分颜色 509 | * 510 | * @param textScoreColor 511 | */ 512 | public void setTextScoreColor(int textScoreColor) { 513 | this.textScoreColor = textScoreColor; 514 | } 515 | 516 | public Bitmap getCheckIn() { 517 | return checkIn; 518 | } 519 | 520 | /** 521 | * 设置签到图片 522 | * 523 | * @param checkIn 524 | */ 525 | public void setCheckIn(Bitmap checkIn) { 526 | this.checkIn = checkIn; 527 | } 528 | 529 | public int getCircleMargin() { 530 | return circleMargin; 531 | } 532 | 533 | /** 534 | * 设置签到圆与日期的间距 535 | * 536 | * @param circleMargin 537 | */ 538 | public void setCircleMargin(int circleMargin) { 539 | this.circleMargin = dp2px(circleMargin); 540 | } 541 | 542 | public Paint.Style getCircleStyle() { 543 | return circleStyle; 544 | } 545 | 546 | /** 547 | * 未签到圆的样式 548 | * 549 | * @param circleStyle 550 | */ 551 | public void setCircleStyle(Paint.Style circleStyle) { 552 | this.circleStyle = circleStyle; 553 | } 554 | 555 | public int getCircleStrokeWidth() { 556 | return circleStrokeWidth; 557 | } 558 | 559 | /** 560 | * 未签到圆描边大小 561 | * 562 | * @param circleStrokeWidth 563 | */ 564 | public void setCircleStrokeWidth(int circleStrokeWidth) { 565 | this.circleStrokeWidth = dp2px(circleStrokeWidth); 566 | } 567 | 568 | public int getCircleStrokeColor() { 569 | return circleStrokeColor; 570 | } 571 | 572 | /** 573 | * 未签到圆描边颜色 574 | * 575 | * @param circleStrokeColor 576 | */ 577 | public void setCircleStrokeColor(int circleStrokeColor) { 578 | this.circleStrokeColor = circleStrokeColor; 579 | } 580 | 581 | public boolean isCheckInProgressShow() { 582 | return checkInProgressShow; 583 | } 584 | 585 | /** 586 | * 是否显示签到进度 587 | * 588 | * @param checkInProgressShow 589 | */ 590 | public void setCheckInProgressShow(boolean checkInProgressShow) { 591 | this.checkInProgressShow = checkInProgressShow; 592 | } 593 | 594 | public int getCheckInProgressColor() { 595 | return checkInProgressColor; 596 | } 597 | 598 | /** 599 | * 签到进度的颜色 600 | * 601 | * @param checkInProgressColor 602 | */ 603 | public void setCheckInProgressColor(int checkInProgressColor) { 604 | this.checkInProgressColor = checkInProgressColor; 605 | } 606 | 607 | public int getCheckInColor() { 608 | return checkInColor; 609 | } 610 | 611 | /** 612 | * 未设置敲到图片时,签到背景的颜色 613 | * 614 | * @param checkInColor 615 | */ 616 | public void setCheckInColor(int checkInColor) { 617 | this.checkInColor = checkInColor; 618 | } 619 | 620 | public int getCheckInHookColor() { 621 | return checkInHookColor; 622 | } 623 | 624 | /** 625 | * 未设置敲到图片时,签到中线勾的颜色 626 | * 627 | * @param checkInHookColor 628 | */ 629 | public void setCheckInHookColor(int checkInHookColor) { 630 | this.checkInHookColor = checkInHookColor; 631 | } 632 | 633 | public int getCheckInHookSize() { 634 | return checkInHookSize; 635 | } 636 | 637 | /** 638 | * 未设置敲到图片时,签到中线勾的大小 639 | * 640 | * @param checkInHookSize 641 | */ 642 | public void setCheckInHookSize(int checkInHookSize) { 643 | this.checkInHookSize = dp2px(checkInHookSize); 644 | } 645 | 646 | public boolean isCheckInLeakShow() { 647 | return checkInLeakShow; 648 | } 649 | 650 | /** 651 | * 是否显示补签 652 | * 653 | * @param checkInLeakShow 654 | */ 655 | public void setCheckInLeakShow(boolean checkInLeakShow) { 656 | this.checkInLeakShow = checkInLeakShow; 657 | } 658 | 659 | 660 | public Align getAlign() { 661 | return align; 662 | } 663 | 664 | /** 665 | * 位置 666 | * 667 | * @param align 668 | */ 669 | public void setAlign(Align align) { 670 | this.align = align; 671 | } 672 | 673 | /** 674 | * dp转像素 675 | * 676 | * @param size 677 | * @return 678 | */ 679 | private int dp2px(int size) { 680 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, getResources().getDisplayMetrics()); 681 | } 682 | 683 | /** 684 | * sp转像素 685 | * 686 | * @param size 687 | * @return 688 | */ 689 | private int sp2px(int size) { 690 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, size, getResources().getDisplayMetrics()); 691 | } 692 | 693 | @Override 694 | public boolean onTouchEvent(MotionEvent event) { 695 | 696 | if (listener == null) return false; 697 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 698 | downPoint.x = (int) event.getX(); 699 | downPoint.y = (int) event.getY(); 700 | } else if (event.getAction() == MotionEvent.ACTION_UP) { 701 | int x = (int) event.getX(); 702 | int y = (int) event.getY(); 703 | int margin = calculationAlign(); 704 | for (int i = 0; i < circlePointPool.size(); i++) { 705 | Point p = circlePointPool.get(i); 706 | Rect rect = new Rect(p.x - radius, p.y + margin - radius, p.x + radius, p.y + margin + radius); 707 | if (rect.contains(downPoint.x, downPoint.y) && rect.contains(x, y)) { 708 | listener.OnClick(i); 709 | break; 710 | } 711 | } 712 | } 713 | 714 | return true; 715 | } 716 | 717 | static class AdapterDataObservable extends Observable { 718 | 719 | public void notifyChanged() { 720 | Log.d(TAG, "notifyChanged observable changed"); 721 | for (int i = this.mObservers.size() - 1; i >= 0; --i) { 722 | (this.mObservers.get(i)).onChanged(); 723 | } 724 | } 725 | } 726 | 727 | 728 | private abstract class AdapterDataObserver { 729 | public void onChanged() { 730 | } 731 | } 732 | 733 | private class CheckInProgressDataObserver extends AdapterDataObserver { 734 | @Override 735 | public void onChanged() { 736 | Log.d(TAG, "CheckInProgressDataObserver changed"); 737 | CheckInProgress that = CheckInProgress.this; 738 | 739 | that.requestLayout(); 740 | 741 | that.postInvalidate(); 742 | } 743 | } 744 | 745 | public static abstract class Adapter { 746 | private final AdapterDataObservable mObservable = new AdapterDataObservable(); 747 | 748 | public abstract String getDateText(int position); 749 | 750 | public abstract String getScoreText(int position); 751 | 752 | public abstract boolean isCheckIn(int position); 753 | 754 | public abstract int size(); 755 | 756 | public boolean isLeakCheckIn(int position) { 757 | return false; 758 | } 759 | 760 | private void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { 761 | Log.d(TAG, "register adapter observer"); 762 | this.mObservable.registerObserver(observer); 763 | } 764 | 765 | private void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { 766 | Log.d(TAG, "unregister adapter observer"); 767 | this.mObservable.unregisterObserver(observer); 768 | } 769 | 770 | public void notifyDataSetChanged() { 771 | Log.d(TAG, "notifyDataSetChanged observable changed"); 772 | this.mObservable.notifyChanged(); 773 | } 774 | 775 | } 776 | 777 | } 778 | -------------------------------------------------------------------------------- /library/src/main/java/com/lishang/checkin/OnClickCheckInListener.java: -------------------------------------------------------------------------------- 1 | package com.lishang.checkin; 2 | 3 | public interface OnClickCheckInListener { 4 | void OnClick(int position); 5 | } 6 | -------------------------------------------------------------------------------- /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 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | -------------------------------------------------------------------------------- /library/src/test/java/com/lishang/library/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.lishang.library; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------