├── .gitignore
├── .idea
├── .gitignore
├── .name
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release
│ ├── datetimepicker.apk
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── gredicer
│ │ └── datetimepickerDemo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── gredicer
│ │ │ └── datetimepickerDemo
│ │ │ └── MainActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── shape_button_corners.xml
│ │ ├── shape_button_corners0.xml
│ │ ├── shape_button_corners1.xml
│ │ ├── shape_button_corners2.xml
│ │ ├── shape_button_corners3.xml
│ │ ├── shape_button_corners4.xml
│ │ └── shape_button_corners5.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── gredicer
│ └── datetimepickerDemo
│ └── ExampleUnitTest.kt
├── build.gradle
├── datetimepicker
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── gredicer
│ │ └── datetimepicker
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── gredicer
│ │ │ └── datetimepicker
│ │ │ ├── DatePickerAdapter.kt
│ │ │ ├── DateTimePickerFragment.kt
│ │ │ ├── DecelerateAnimator.kt
│ │ │ ├── OutsideClickDialog.kt
│ │ │ ├── PickAdapter.kt
│ │ │ └── ScrollPickerView.kt
│ └── res
│ │ ├── drawable
│ │ ├── shape_button_corners.xml
│ │ └── shape_dialog_corners.xml
│ │ ├── layout
│ │ └── fragment_datetime_picker.xml
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── gredicer
│ └── datetimepicker
│ └── ExampleUnitTest.kt
├── demo.gif
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── 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 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | DatetimePickerDemo
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # datetimepicker
2 | 一个非常好看的时间选择控件
3 |
4 | [](https://jitpack.io/#Gredicer/datetimepicker)
5 |
6 | ### 演示效果
7 | 
8 |
9 | ### 引用
10 | Step 1. Add it in your root build.gradle at the end of repositories:
11 | ```kotlin
12 | allprojects {
13 | repositories {
14 | ...
15 | maven { url 'https://jitpack.io' }
16 | }
17 | }
18 | ```
19 |
20 | Step 2. Add the dependency
21 | ```kotlin
22 | dependencies {
23 | implementation 'com.github.Gredicer:datetimepicker:V1.0.0'
24 | }
25 | ```
26 |
27 | ### 使用
28 |
29 | 示例代码:
30 |
31 | ```
32 | val dialog = DateTimePickerFragment.newInstance().mode(0).default("2010-10-10 11:11:11")
33 |
34 | btn.setOnClickListener {
35 | dialog.show(this.supportFragmentManager, null)
36 | }
37 |
38 | dialog.listener = object : DateTimePickerFragment.OnClickListener {
39 | override fun onClickListener(selectTime: String) {
40 | Toast.makeText(applicationContext, selectTime, Toast.LENGTH_SHORT).show()
41 | }
42 | }
43 | ```
44 |
45 | `.default` 用来设置初始值,如果不设置默认为当前时间
46 | `mode`的值对应为:
47 |
48 | ```
49 | 0:默认,年月日时分
50 | 1:年选择
51 | 2:年月选择
52 | 3:年月日选择
53 | 4:时间选择
54 | 5:设定初始时间
55 | ```
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-android-extensions'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | applicationId "com.gredicer.datetimepickerDemo"
13 | minSdkVersion 19
14 | targetSdkVersion 30
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | buildFeatures {
35 | viewBinding true
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
42 | implementation 'androidx.core:core-ktx:1.6.0'
43 | implementation 'androidx.appcompat:appcompat:1.3.1'
44 | implementation 'com.google.android.material:material:1.4.0'
45 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
46 | implementation project(path: ':datetimepicker')
47 | testImplementation 'junit:junit:4.13.2'
48 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
49 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
50 | implementation 'com.yanzhenjie:sofia:1.0.5'
51 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/release/datetimepicker.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/release/datetimepicker.apk
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.gredicer.datetimepickerDemo",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "versionCode": 1,
14 | "versionName": "1.0",
15 | "outputFile": "app-release.apk"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/gredicer/datetimepickerDemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepickerDemo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.gredicer.datetimepickerDemo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/gredicer/datetimepickerDemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepickerDemo
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.widget.Toast
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.gredicer.datetimepicker.DateTimePickerFragment
8 | import com.yanzhenjie.sofia.Sofia
9 | import kotlinx.android.synthetic.main.activity_main.*
10 | import java.text.SimpleDateFormat
11 | import java.util.*
12 |
13 | class MainActivity : AppCompatActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 |
18 | Sofia.with(this)
19 | .statusBarDarkFont() // 状态栏深色字体。
20 | .invasionStatusBar() //内容入侵状态栏
21 | .navigationBarBackgroundAlpha(0) // 导航栏背景透明度。
22 | .statusBarBackgroundAlpha(0) // 状态栏背景透明度。
23 | .invasionNavigationBar() // 内容入侵导航栏。
24 |
25 | val dialog = DateTimePickerFragment.newInstance()
26 | btn1.setOnClickListener {
27 | dialog.mode(0)
28 | dialog.show(this.supportFragmentManager, null)
29 | }
30 |
31 | btn2.setOnClickListener {
32 | dialog.mode(1)
33 | dialog.show(this.supportFragmentManager, null)
34 | }
35 |
36 | btn3.setOnClickListener {
37 | dialog.mode(2)
38 | dialog.show(this.supportFragmentManager, null)
39 | }
40 |
41 | btn4.setOnClickListener {
42 | dialog.mode(3)
43 | dialog.show(this.supportFragmentManager, null)
44 | }
45 |
46 | btn5.setOnClickListener {
47 | dialog.mode(4)
48 | dialog.show(this.supportFragmentManager, null)
49 | }
50 |
51 |
52 | btn6.setOnClickListener {
53 | dialog.mode(0).default("2010-10-10 11:11:11")
54 | dialog.show(this.supportFragmentManager, null)
55 | }
56 |
57 |
58 | dialog.listener = object : DateTimePickerFragment.OnClickListener {
59 | override fun onClickListener(selectTime: String) {
60 | Toast.makeText(applicationContext, selectTime, Toast.LENGTH_SHORT).show()
61 | }
62 | }
63 |
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners0.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners1.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners2.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners3.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners4.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_button_corners5.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
20 |
21 |
30 |
31 |
40 |
41 |
50 |
51 |
60 |
61 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/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_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #00FFFFFF
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DateTimePicker
3 | 时间选择
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gredicer/datetimepickerDemo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepickerDemo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.5.21"
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:4.2.2"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | }
23 |
24 | task clean(type: Delete) {
25 | delete rootProject.buildDir
26 | }
--------------------------------------------------------------------------------
/datetimepicker/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/datetimepicker/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-android-extensions'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | minSdkVersion 19
13 | targetSdkVersion 30
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | consumerProguardFiles "consumer-rules.pro"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | buildFeatures {
35 | viewBinding true
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
42 | implementation 'androidx.core:core-ktx:1.6.0'
43 | implementation 'androidx.appcompat:appcompat:1.3.1'
44 | implementation 'com.google.android.material:material:1.4.0'
45 | testImplementation 'junit:junit:4.13.2'
46 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
48 | implementation 'com.google.android.flexbox:flexbox:3.0.0'
49 |
50 | }
--------------------------------------------------------------------------------
/datetimepicker/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/datetimepicker/consumer-rules.pro
--------------------------------------------------------------------------------
/datetimepicker/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
--------------------------------------------------------------------------------
/datetimepicker/src/androidTest/java/com/gredicer/datetimepicker/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.gredicer.datetimepicker.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/datetimepicker/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/DatePickerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import java.text.DecimalFormat
4 |
5 | /**
6 | * 日期选择适配器
7 | *
8 | * @author Simon Lee
9 | * @e-mail jmlixiaomeng@163.com
10 | * @github https://github.com/Simon-Leeeeeeeee/SLWidget
11 | * @createdTime 2018-05-17
12 | */
13 | class DatePickerAdapter @JvmOverloads constructor(
14 | var minValue: Int,
15 | var maxValue: Int,
16 | private val mDecimalFormat: DecimalFormat? = null
17 | ) :
18 | PickAdapter {
19 | override val count: Int
20 | get() = maxValue - minValue + 1
21 |
22 | override fun getItem(position: Int): String? {
23 | return if (position in 0 until count) {
24 | if (mDecimalFormat == null) {
25 | (minValue + position).toString()
26 | } else {
27 | mDecimalFormat.format((minValue + position).toLong())
28 | }
29 | } else null
30 | }
31 |
32 | fun getDate(position: Int): Int {
33 | return if (position in 0 until count) {
34 | minValue + position
35 | } else 0
36 | }
37 |
38 | fun indexOf(valueString: String): Int {
39 | val value: Int = try {
40 | valueString.toInt()
41 | } catch (e: NumberFormatException) {
42 | return -1
43 | }
44 | return indexOf(value)
45 | }
46 |
47 | fun indexOf(value: Int): Int {
48 | return if (value < minValue || value > maxValue) {
49 | -1
50 | } else value - minValue
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/DateTimePickerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import android.animation.ObjectAnimator
4 | import android.animation.PropertyValuesHolder
5 | import android.app.Activity
6 | import android.app.Dialog
7 | import android.graphics.Insets
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.util.DisplayMetrics
11 | import android.view.*
12 | import android.view.animation.DecelerateInterpolator
13 | import android.view.animation.OvershootInterpolator
14 | import androidx.core.animation.doOnEnd
15 | import androidx.fragment.app.DialogFragment
16 | import kotlinx.android.synthetic.main.fragment_datetime_picker.*
17 | import java.text.DecimalFormat
18 | import java.util.*
19 |
20 |
21 | class DateTimePickerFragment : DialogFragment(), ScrollPickerView.OnItemSelectedListener {
22 | private var window: Window? = null
23 |
24 | // 当前模式 0-年月日时分 1-年 2-年月 3-年月日 4-时分
25 | private var mMode: Int = 0
26 |
27 | // 退出状态
28 | private var exitStatus: Boolean = false
29 |
30 | // 是否设置初始值
31 | private var hasSetDefault: Boolean = false
32 |
33 | // 初始时间
34 | private var mDefaultTime = "2000-01-01 00:00:00"
35 |
36 |
37 | private var mYearAdapter = DatePickerAdapter(1900, 2200, DecimalFormat("0000"))
38 | private var mSelectedYear: Int = 0
39 | private var mMonthAdapter = DatePickerAdapter(1, 12, DecimalFormat("00"))
40 | private var mSelectedMonth: Int = 0
41 | private var mDayAdapter = DatePickerAdapter(1, 31, DecimalFormat("00"))
42 | private var mSelectedDay: Int = 0
43 | private var mHourAdapter = DatePickerAdapter(0, 23, DecimalFormat("00"))
44 | private var mSelectedHour: Int = 0
45 | private var mMinuteAdapter = DatePickerAdapter(0, 59, DecimalFormat("00"))
46 | private var mSelectedMinute: Int = 0
47 |
48 |
49 | companion object {
50 | fun newInstance(): DateTimePickerFragment {
51 | return DateTimePickerFragment()
52 | }
53 | }
54 |
55 | override fun onCreateView(
56 | inflater: LayoutInflater,
57 | container: ViewGroup?,
58 | savedInstanceState: Bundle?
59 | ): View? {
60 | return inflater.inflate(R.layout.fragment_datetime_picker, container, false)
61 | }
62 |
63 | override fun onStart() {
64 | super.onStart()
65 | if (dialog != null && dialog!!.window != null) {
66 | window = dialog!!.window!!
67 | val params = window!!.attributes
68 | params.width = WindowManager.LayoutParams.MATCH_PARENT
69 | params.height = WindowManager.LayoutParams.WRAP_CONTENT
70 | // 显示在页面的底部
71 | params.gravity = Gravity.BOTTOM
72 | window!!.attributes = params
73 | window!!.setBackgroundDrawableResource(R.drawable.shape_dialog_corners)
74 | // dialog弹出后会点击屏幕或物理返回键,dialog不消失
75 | dialog!!.setCancelable(true)
76 | // dialog弹出后会点击屏幕,dialog不消失;点击物理返回键dialog消失
77 | dialog!!.setCanceledOnTouchOutside(true)
78 |
79 | enterAnimation()
80 | }
81 | }
82 |
83 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
84 | val mOutsideClickDialog = OutsideClickDialog(requireContext(), theme)
85 | // 监听外部点击
86 | mOutsideClickDialog.onOutsideClickListener = {
87 | exitAnimation()
88 | true
89 | }
90 | // 监听返回点击
91 | mOutsideClickDialog.onBackClickListener = {
92 | exitAnimation()
93 | true
94 | }
95 | return mOutsideClickDialog
96 | }
97 |
98 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
99 | super.onViewCreated(view, savedInstanceState)
100 | // 初始化退出状态为现在可以退出,不在退出状态
101 | exitStatus = false
102 |
103 | when (mMode) {
104 | 0 -> {
105 | initYear()
106 | initMonth()
107 | initDay()
108 | initHour()
109 | initMinute()
110 | }
111 | 1 -> {
112 | initYear()
113 | }
114 | 2 -> {
115 | initYear()
116 | initMonth()
117 | resetUI(2)
118 | }
119 | 3 -> {
120 | initYear()
121 | initMonth()
122 | initDay()
123 | resetUI(3)
124 | }
125 | 4 -> {
126 | initHour()
127 | initMinute()
128 | resetUI(2)
129 | }
130 | }
131 |
132 |
133 |
134 | if (!hasSetDefault) resetTime()
135 |
136 | btn_back_now.setOnClickListener { resetTime() }
137 |
138 | btn_enter.setOnClickListener {
139 | listener?.onClickListener(returnTime())
140 | exitAnimation()
141 | }
142 | }
143 |
144 |
145 | var listener: OnClickListener? = null
146 |
147 | interface OnClickListener {
148 | fun onClickListener(selectTime: String)
149 | }
150 |
151 | override fun onItemSelected(view: View?, position: Int) {
152 | when (view?.id) {
153 | R.id.date_picker_year -> {
154 | mSelectedYear = mYearAdapter.getDate(position)
155 | // 根据年月计算日期的最大值,并刷新
156 | mDayAdapter.maxValue = getMonthLastDay(mSelectedYear, mSelectedMonth)
157 | }
158 | R.id.date_picker_month -> {
159 | mSelectedMonth = mMonthAdapter.getDate(position)
160 | // 根据年月计算日期的最大值,并刷新
161 | mDayAdapter.maxValue = getMonthLastDay(mSelectedYear, mSelectedMonth)
162 | date_picker_day.setAdapter(mDayAdapter)
163 | }
164 | R.id.date_picker_day -> {
165 | mSelectedDay = mDayAdapter.getDate(position)
166 | }
167 | R.id.date_picker_hour -> {
168 | mSelectedHour = mHourAdapter.getDate(position)
169 | }
170 | R.id.date_picker_minute -> {
171 | mSelectedMinute = mMinuteAdapter.getDate(position)
172 | }
173 | else -> {
174 | }
175 | }
176 | showTime()
177 | }
178 |
179 | /**
180 | * 增加年的显示
181 | * */
182 | fun mode(mode: Int): DateTimePickerFragment {
183 | mMode = mode
184 | return this
185 | }
186 |
187 |
188 | /**
189 | * 设置初始值
190 | * */
191 | fun default(defaultTime: String): DateTimePickerFragment {
192 | mDefaultTime = defaultTime
193 | hasSetDefault = true
194 | return this
195 | }
196 |
197 | /**
198 | * 初始化年
199 | * */
200 | private fun initYear() {
201 | year_show.visibility = View.VISIBLE
202 | date_picker_year.setAdapter(mYearAdapter)
203 | date_picker_year.setOnItemSelectedListener(this)
204 | setSelectValue(0, mDefaultTime.substring(0, 4).toInt())
205 | }
206 |
207 | /**
208 | * 初始化月
209 | * */
210 | private fun initMonth() {
211 | month_show.visibility = View.VISIBLE
212 | date_picker_month.setAdapter(mMonthAdapter)
213 | date_picker_month.setOnItemSelectedListener(this)
214 | setSelectValue(1, mDefaultTime.substring(5, 7).toInt())
215 | }
216 |
217 | /**
218 | * 初始化日
219 | * */
220 | private fun initDay() {
221 | day_show.visibility = View.VISIBLE
222 | date_picker_day.setAdapter(mDayAdapter)
223 | date_picker_day.setOnItemSelectedListener(this)
224 | setSelectValue(2, mDefaultTime.substring(8, 10).toInt())
225 | }
226 |
227 | /**
228 | * 初始化时
229 | * */
230 | private fun initHour() {
231 | hour_show.visibility = View.VISIBLE
232 | date_picker_hour.setAdapter(mHourAdapter)
233 | date_picker_hour.setOnItemSelectedListener(this)
234 | setSelectValue(3, mDefaultTime.substring(11, 13).toInt())
235 | }
236 |
237 | /**
238 | * 初始化分
239 | * */
240 | private fun initMinute() {
241 | minute_show.visibility = View.VISIBLE
242 | date_picker_minute.setAdapter(mMinuteAdapter)
243 | date_picker_minute.setOnItemSelectedListener(this)
244 | setSelectValue(4, mDefaultTime.substring(14, 16).toInt())
245 | }
246 |
247 | /**
248 | * 设置当前选择的值
249 | * type: 0-年,1-月,2-日,3-时,4-分
250 | * */
251 | private fun setSelectValue(type: Int, value: Int) {
252 | when (type) {
253 | 0 -> {
254 | date_picker_year.setSelectedPosition(mYearAdapter.indexOf(value))
255 | }
256 | 1 -> {
257 | date_picker_month.setSelectedPosition(mMonthAdapter.indexOf(value))
258 | }
259 | 2 -> {
260 | date_picker_day.setSelectedPosition(mDayAdapter.indexOf(value))
261 | }
262 | 3 -> {
263 | date_picker_hour.setSelectedPosition(mHourAdapter.indexOf(value))
264 | }
265 | 4 -> {
266 | date_picker_minute.setSelectedPosition(mMinuteAdapter.indexOf(value))
267 | }
268 | }
269 | }
270 |
271 | /**
272 | * 重置UI
273 | * */
274 | private fun resetUI(showCount: Int) {
275 | when (showCount) {
276 | 2 -> {
277 | fl_datetimepicker.setPadding(200, 0, 200, 0)
278 | }
279 | 3 -> {
280 | fl_datetimepicker.setPadding(100, 0, 100, 0)
281 | }
282 | }
283 | }
284 |
285 | /**
286 | * 文字显示当前的时间
287 | * */
288 | private fun showTime() {
289 | var showText = ""
290 | when (mMode) {
291 | 0 -> {
292 | showText += "$mSelectedYear 年"
293 | showText += " ${formatTime(mSelectedMonth)} 月"
294 | showText += " ${formatTime(mSelectedDay)} 日 "
295 | showText += "${formatTime(mSelectedHour)} :"
296 | showText += " ${formatTime(mSelectedMinute)}"
297 | }
298 | 1 -> {
299 | showText = "$mSelectedYear 年"
300 | }
301 | 2 -> {
302 | showText = "$mSelectedYear 年 ${formatTime(mSelectedMonth)} 月"
303 | }
304 | 3 -> {
305 | showText += "$mSelectedYear 年"
306 | showText += " ${formatTime(mSelectedMonth)} 月"
307 | showText += " ${formatTime(mSelectedDay)} 日"
308 | }
309 | 4 -> {
310 | showText = "${formatTime(mSelectedHour)}:${formatTime(mSelectedMinute)}"
311 | }
312 | }
313 | tv_time_show.text = showText
314 | }
315 |
316 | /**
317 | * 返回的时间
318 | * */
319 | private fun returnTime(): String {
320 | var text = ""
321 | when (mMode) {
322 | 0 -> {
323 | text += "$mSelectedYear-"
324 | text += "${formatTime(mSelectedMonth)}-"
325 | text += "${formatTime(mSelectedDay)} "
326 | text += "${formatTime(mSelectedHour)}:"
327 | text += formatTime(mSelectedMinute)
328 | }
329 | 1 -> {
330 | text = "$mSelectedYear"
331 | }
332 | 2 -> {
333 | text = "$mSelectedYear-${formatTime(mSelectedMonth)}"
334 | }
335 | 3 -> {
336 | text = "$mSelectedYear-${formatTime(mSelectedMonth)}-${formatTime(mSelectedDay)}"
337 | }
338 | 4 -> {
339 | text = "${formatTime(mSelectedHour)}:${formatTime(mSelectedMinute)}"
340 | }
341 | }
342 | return text
343 | }
344 |
345 | /**
346 | * 格式化时间
347 | **/
348 | private fun formatTime(value: Int): String {
349 | return DecimalFormat("00").format(value)
350 | }
351 |
352 |
353 | /**
354 | * 重置到现在的时间
355 | * */
356 | private fun resetTime() {
357 | val calendar = Calendar.getInstance()
358 | // 年
359 | val year = calendar.get(Calendar.YEAR)
360 | // 月
361 | val month = calendar.get(Calendar.MONTH) + 1
362 | // 日
363 | val day = calendar.get(Calendar.DAY_OF_MONTH)
364 | // 小时
365 | val hour = calendar.get(Calendar.HOUR_OF_DAY)
366 | // 分钟
367 | val minute = calendar.get(Calendar.MINUTE)
368 | mDayAdapter.maxValue = getMonthLastDay(year, month)
369 | setSelectValue(0, year)
370 | setSelectValue(1, month)
371 | setSelectValue(2, day)
372 | setSelectValue(3, hour)
373 | setSelectValue(4, minute)
374 | }
375 |
376 | /**
377 | * 进入动画
378 | * */
379 | private fun enterAnimation() {
380 | val holder1 = PropertyValuesHolder.ofFloat("scaleX", 1f, 1f)
381 | val holder2 = PropertyValuesHolder.ofFloat("scaleY", 0f, 1f)
382 | val deCoverView = window!!.decorView
383 | deCoverView.pivotY = getScreenHeight(context as Activity).toFloat() / 2
384 | val scaleDown = ObjectAnimator.ofPropertyValuesHolder(deCoverView, holder1, holder2)
385 | scaleDown.interpolator = OvershootInterpolator(0.7f)
386 | scaleDown.duration = 200
387 | scaleDown.start()
388 | }
389 |
390 | /**
391 | * 退出动画
392 | * */
393 | private fun exitAnimation() {
394 | if (exitStatus) return
395 | exitStatus = true
396 |
397 | val params = window!!.attributes
398 | params.dimAmount = 0.1f
399 | window!!.attributes = params
400 |
401 | val a = getScreenHeight(context as Activity).toFloat() / 2
402 | val holder1 = PropertyValuesHolder.ofFloat("scaleX", 1f, 1f)
403 | val holder2 = PropertyValuesHolder.ofFloat("translationY", 0f, a)
404 | val deCoverView = window!!.decorView
405 | val scaleDown = ObjectAnimator.ofPropertyValuesHolder(deCoverView, holder1, holder2)
406 | scaleDown.interpolator = DecelerateInterpolator()
407 | scaleDown.duration = 200
408 | scaleDown.start()
409 | scaleDown.doOnEnd {
410 | dismiss()
411 | }
412 |
413 | }
414 |
415 | /**
416 | * 获取屏幕的宽度
417 | * */
418 | private fun getScreenWidth(activity: Activity): Int {
419 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
420 | val windowMetrics = activity.windowManager.currentWindowMetrics
421 | val insets: Insets = windowMetrics.windowInsets
422 | .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
423 | windowMetrics.bounds.width() - insets.left - insets.right
424 | } else {
425 | val displayMetrics = DisplayMetrics()
426 | activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
427 | displayMetrics.widthPixels
428 | }
429 | }
430 |
431 | /**
432 | * 获取屏幕的高度
433 | * */
434 | private fun getScreenHeight(activity: Activity): Int {
435 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
436 | val windowMetrics = activity.windowManager.currentWindowMetrics
437 | val insets: Insets = windowMetrics.windowInsets
438 | .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
439 | windowMetrics.bounds.height() - insets.top - insets.bottom
440 | } else {
441 | val displayMetrics = DisplayMetrics()
442 | activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
443 | displayMetrics.heightPixels
444 | }
445 | }
446 |
447 |
448 | /**
449 | * 得到指定月的天数
450 | */
451 | private fun getMonthLastDay(year: Int, month: Int): Int {
452 | val a = Calendar.getInstance()
453 | a[Calendar.YEAR] = year
454 | a[Calendar.MONTH] = month - 1
455 | a[Calendar.DATE] = 1 //把日期设置为当月第一天
456 | a.roll(Calendar.DATE, -1) //日期回滚一天,也就是最后一天
457 | return a[Calendar.DATE]
458 | }
459 |
460 | }
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/DecelerateAnimator.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import android.animation.TypeEvaluator
4 | import android.animation.ValueAnimator
5 | import android.annotation.SuppressLint
6 | import android.content.Context
7 | import android.hardware.SensorManager
8 | import android.view.ViewConfiguration
9 | import android.view.animation.LinearInterpolator
10 |
11 |
12 | /**
13 | * 减速动画,默认启用回弹效果。
14 | *
15 | * @author Simon Lee
16 | * @e-mail jmlixiaomeng@163.com
17 | * @github https://github.com/Simon-Leeeeeeeee/SLWidget
18 | * @createdTime 2018-07-23
19 | */
20 | @SuppressLint("Recycle")
21 | class DecelerateAnimator @JvmOverloads constructor(
22 | context: Context,
23 | /**
24 | * 弹性系数
25 | */
26 | private val mBounceCoeff: Float = 10f,
27 | /**
28 | * 是否启用回弹效果
29 | */
30 | private var isBouncing: Boolean = true
31 | ) :
32 | ValueAnimator() {
33 | private val DECELERATION_RATE = 2.358201815f //Math.log(0.78) / Math.log(0.9)
34 | private val INFLEXION = 0.35f // Tension lines cross at (INFLEXION, 1)
35 |
36 | /**
37 | * 动摩擦系数
38 | */
39 | private var mFlingFriction = 0f
40 |
41 | /**
42 | * 动摩擦系数倍率
43 | */
44 | private var mFlingFrictionRatio = 0.4f
45 |
46 | /**
47 | * 物理系数
48 | */
49 | private val mPhysicalCoeff: Float
50 |
51 | /**
52 | * 估值器
53 | */
54 | private val mDecelerateEvaluator: DecelerateEvaluator
55 |
56 | /**
57 | * 动画起始值
58 | */
59 | private var mInitialValue = 0f
60 |
61 | /**
62 | * 动画终止值
63 | */
64 | private var mFinalValue = 0f
65 |
66 | /**
67 | * 动画总持续时间
68 | */
69 | private var mDuration: Long = 0
70 |
71 | /**
72 | * 位移距离
73 | */
74 | private var mDistance = 0f
75 |
76 | /**
77 | * 回弹持续时间
78 | */
79 | private var mBounceDuration: Long = 0
80 |
81 | /**
82 | * 回弹位移距离
83 | */
84 | private var mBounceDistance = 0f
85 |
86 | /**
87 | * 未处理越界情况下的动画时间
88 | */
89 | private var mOriginalDuration: Long = 0
90 |
91 | /**
92 | * 未处理越界情况下的位移距离
93 | */
94 | private var mOriginalDistance = 0f
95 |
96 | /**
97 | * 摩擦系数,用于计算越界情况下的动画时间和位移
98 | */
99 | private var mFrictionCoeff = 0f
100 |
101 | /**
102 | * 是否越界(只有越界了才可能会发生回弹)
103 | */
104 | private var isOutside = false
105 |
106 | constructor(context: Context, bouncing: Boolean) : this(context, 10f, bouncing) {}
107 |
108 | /**
109 | * 指定位移距离和最大动画时间,开始减速动画。
110 | *
111 | * @param startValue 起始值
112 | * @param finalValue 终止值
113 | * @param maxDuration 最大动画时间
114 | */
115 | fun startAnimator(startValue: Float, finalValue: Float, maxDuration: Long) {
116 | reset()
117 | mInitialValue = startValue
118 | mDistance = finalValue - startValue
119 | if (mDistance == 0f) {
120 | return
121 | }
122 | mFinalValue = finalValue
123 | mDuration = getDurationByDistance(mDistance)
124 | if (mDuration > maxDuration) {
125 | resetFlingFriction(mDistance, maxDuration)
126 | mDuration = maxDuration
127 | }
128 | startAnimator()
129 | }
130 |
131 | /**
132 | * 指定起止值和初始速度,开始减速动画
133 | * 终点值一定是极小值或者极大值
134 | *
135 | * @param startValue 初始值
136 | * @param minFinalValue 极小值
137 | * @param maxFinalValue 极大值
138 | * @param velocity 初速度
139 | */
140 | fun startAnimator(
141 | startValue: Float,
142 | minFinalValue: Float,
143 | maxFinalValue: Float,
144 | velocity: Float
145 | ) {
146 | if (minFinalValue >= maxFinalValue) {
147 | throw ArithmeticException("maxFinalValue must be larger than minFinalValue!")
148 | }
149 | reset()
150 | mInitialValue = startValue
151 | // 1.根据速度计算位移距离
152 | val distance = getDistanceByVelocity(velocity)
153 | val finalValue = startValue + distance
154 | // 2.确定终点值、位移距离、动画时间
155 | if (finalValue < minFinalValue || finalValue > maxFinalValue) { //终点值在界外
156 | //确定终点值
157 | mFinalValue = if (finalValue < minFinalValue) minFinalValue else maxFinalValue
158 | //起止值都在界外同侧
159 | if (startValue < minFinalValue && finalValue < minFinalValue || startValue > maxFinalValue && finalValue > maxFinalValue) {
160 | //改变动摩擦系数,减少动画时间
161 | mFrictionCoeff = mBounceCoeff
162 | //直接校正位移距离并计算动画时间
163 | mDistance = mFinalValue - startValue
164 | mDuration = getDurationByDistance(mDistance, mFrictionCoeff)
165 | } else if (isBouncing) { //起止值跨越边界,且启用回弹效果
166 | isOutside = true
167 | //记录未处理越界情况下的位移距离和动画时间,用于计算回弹第一阶段的位移
168 | mOriginalDistance = distance
169 | mOriginalDuration = getDurationByDistance(distance)
170 | //获取越界时的速度
171 | val bounceVelocity = getVelocityByDistance(finalValue - mFinalValue)
172 | //改变动摩擦系数,减少回弹时间
173 | mFrictionCoeff = mBounceCoeff
174 | //计算越界后的回弹时间
175 | mBounceDuration = getDurationByVelocity(bounceVelocity, mFrictionCoeff)
176 | //根据回弹时间计算回弹位移
177 | mBounceDistance =
178 | getDistanceByDuration(mBounceDuration / 2, mFrictionCoeff) * Math.signum(
179 | bounceVelocity
180 | )
181 | //总的动画时间 = 原本动画时间 - 界外时间 + 回弹时间
182 | mDuration =
183 | mOriginalDuration - getDurationByDistance(finalValue - mFinalValue) + mBounceDuration
184 | } else { //禁用回弹效果,按未越界处理。当越界达到边界值时会提前结束动画
185 | isOutside = true
186 | mDistance = distance
187 | //计算动画时间
188 | mDuration = getDurationByDistance(distance)
189 | }
190 | } else { //终点值在界内
191 | //校正终点值,计算位移距离和动画时间
192 | mFinalValue =
193 | if (finalValue * 2 < minFinalValue + maxFinalValue) minFinalValue else maxFinalValue
194 | mDistance = mFinalValue - startValue
195 | mDuration = getDurationByDistance(mDistance)
196 | }
197 | startAnimator()
198 | }
199 |
200 | /**
201 | * 指定初始速度,开始减速动画。
202 | * 无边界
203 | *
204 | * @param startValue 起始位置
205 | * @param velocity 初始速度
206 | * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
207 | */
208 | fun startAnimator_Velocity(startValue: Float, velocity: Float, modulus: Float) {
209 | startAnimator_Velocity(startValue, 0f, 0f, velocity, modulus)
210 | }
211 |
212 | /**
213 | * 指定初始速度,开始减速动画。
214 | * 当极大值大于极小值时有边界
215 | *
216 | * @param startValue 起始位置
217 | * @param minValue 极小值
218 | * @param maxValue 极大值
219 | * @param velocity 初始速度
220 | * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
221 | */
222 | fun startAnimator_Velocity(
223 | startValue: Float,
224 | minValue: Float,
225 | maxValue: Float,
226 | velocity: Float,
227 | modulus: Float
228 | ) {
229 | reset()
230 | mInitialValue = startValue
231 | // 1.计算位移距离
232 | var distance = getDistanceByVelocity(velocity)
233 | // 2.校正位移距离
234 | distance = reviseDistance(distance, startValue, modulus)
235 | val finalValue = startValue + distance
236 | // 3.确定终点值、位移距离、动画时间
237 | if (maxValue > minValue && (finalValue < minValue || finalValue > maxValue)) { //终点值在界外
238 | //确定终点值
239 | mFinalValue = if (finalValue < minValue) minValue else maxValue
240 | //起止值都在界外同侧
241 | if (startValue < minValue && finalValue < minValue || startValue > maxValue && finalValue > maxValue) {
242 | //改变动摩擦系数,减少动画时间
243 | mFrictionCoeff = mBounceCoeff
244 | //直接校正位移距离并计算动画时间
245 | mDistance = mFinalValue - startValue
246 | mDuration = getDurationByDistance(mDistance, mFrictionCoeff)
247 | } else if (isBouncing) { //起止值跨越边界,且启用回弹效果
248 | isOutside = true
249 | //记录未处理越界情况下的位移距离和动画时间,用于计算回弹第一阶段的位移
250 | mOriginalDistance = distance
251 | mOriginalDuration = getDurationByDistance(distance)
252 | //获取越界时的速度
253 | val bounceVelocity = getVelocityByDistance(finalValue - mFinalValue)
254 | //改变动摩擦系数,减少回弹时间
255 | mFrictionCoeff = mBounceCoeff
256 | //计算越界后的回弹时间
257 | mBounceDuration = getDurationByVelocity(bounceVelocity, mFrictionCoeff)
258 | //根据回弹时间计算回弹位移
259 | mBounceDistance =
260 | getDistanceByDuration(mBounceDuration / 2, mFrictionCoeff) * Math.signum(
261 | bounceVelocity
262 | )
263 | //总的动画时间 = 原本动画时间 - 界外时间 + 回弹时间
264 | mDuration =
265 | mOriginalDuration - getDurationByDistance(finalValue - mFinalValue) + mBounceDuration
266 | } else { //禁用回弹效果,按未越界处理。当越界达到边界值时会提前结束动画
267 | isOutside = true
268 | mDistance = distance
269 | //计算动画时间
270 | mDuration = getDurationByDistance(distance)
271 | }
272 | } else { //终点值在界内
273 | //确定终点值、位移距离和动画时间
274 | mFinalValue = finalValue
275 | mDistance = distance
276 | mDuration = getDurationByDistance(mDistance)
277 | }
278 | startAnimator()
279 | }
280 |
281 | /**
282 | * 指定位移距离,开始减速动画。
283 | * 无边界
284 | *
285 | * @param startValue 起始位置
286 | * @param distance 位移距离
287 | * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
288 | */
289 | fun startAnimator_Distance(startValue: Float, distance: Float, modulus: Float) {
290 | startAnimator_Distance(startValue, 0f, 0f, distance, modulus)
291 | }
292 |
293 | /**
294 | * 指定位移距离,开始减速动画。
295 | * 当极大值大于极小值时有边界
296 | *
297 | * @param startValue 起始位置
298 | * @param minValue 极小值
299 | * @param maxValue 极大值
300 | * @param distance 位移距离
301 | * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
302 | */
303 | fun startAnimator_Distance(
304 | startValue: Float,
305 | minValue: Float,
306 | maxValue: Float,
307 | distance: Float,
308 | modulus: Float
309 | ) {
310 | reset()
311 | mInitialValue = startValue
312 | // 1.先校正位移
313 | mDistance = reviseDistance(distance, startValue, modulus)
314 | if (mDistance == 0f) {
315 | return
316 | }
317 | mFinalValue = startValue + mDistance
318 | // 2.极值处理
319 | if (maxValue > minValue && (mFinalValue < minValue || mFinalValue > maxValue)) {
320 | return
321 | }
322 | // 3.计算时间
323 | mDuration = getDurationByDistance(mDistance)
324 | startAnimator()
325 | }
326 |
327 | private fun reset() {
328 | isOutside = false
329 | mFrictionCoeff = 1f
330 | mBounceDuration = 0
331 | mBounceDistance = 0f
332 | mOriginalDuration = 0
333 | mOriginalDistance = 0f
334 | mFlingFriction = ViewConfiguration.getScrollFriction() * mFlingFrictionRatio
335 | }
336 |
337 | private fun startAnimator() {
338 | // 1.设置起止值
339 | setFloatValues(mInitialValue, mFinalValue)
340 | // 2.设置估值器
341 | setEvaluator(mDecelerateEvaluator)
342 | // 3.设置持续时间
343 | duration = mDuration
344 | // 4.开始动画
345 | start()
346 | }
347 |
348 | /**
349 | * 校正位移,确保终点值是模的整数倍
350 | *
351 | * @param distance 位移距离
352 | * @param startValue 起始位置
353 | * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
354 | */
355 | fun reviseDistance(distance: Float, startValue: Float, modulus: Float): Float {
356 | if (modulus != 0f) {
357 | val multiple = ((startValue + distance) / modulus).toInt()
358 | val remainder = startValue + distance - multiple * modulus
359 | if (remainder != 0f) {
360 | return if (remainder * 2 < -modulus) {
361 | distance - remainder - modulus
362 | } else if (remainder * 2 < modulus) {
363 | distance - remainder
364 | } else {
365 | distance - remainder + modulus
366 | }
367 | }
368 | }
369 | return distance
370 | }
371 |
372 | /**
373 | * 根据位移计算初速度
374 | *
375 | * @param distance 位移距离
376 | */
377 | fun getVelocityByDistance(distance: Float): Float {
378 | return getVelocityByDistance(distance, 1f)
379 | }
380 |
381 | /**
382 | * 根据位移计算初速度
383 | *
384 | * @param distance 位移距离
385 | * @param frictionCoeff 摩擦系数
386 | */
387 | fun getVelocityByDistance(distance: Float, frictionCoeff: Float): Float {
388 | var velocity = 0f
389 | if (distance != 0f) {
390 | val decelMinusOne = DECELERATION_RATE - 1.0
391 | val l = Math.pow(
392 | (Math.abs(distance) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
393 | decelMinusOne / DECELERATION_RATE
394 | )
395 | velocity =
396 | (l * mFlingFriction * frictionCoeff * mPhysicalCoeff / INFLEXION * 4 * Math.signum(
397 | distance
398 | )).toFloat()
399 | }
400 | return velocity
401 | }
402 |
403 | /**
404 | * 根据初速度计算位移
405 | *
406 | * @param velocity 初速度
407 | */
408 | fun getDistanceByVelocity(velocity: Float): Float {
409 | return getDistanceByVelocity(velocity, 1f)
410 | }
411 |
412 | /**
413 | * 根据初速度计算位移
414 | *
415 | * @param velocity 初速度
416 | * @param frictionCoeff 摩擦系数
417 | */
418 | fun getDistanceByVelocity(velocity: Float, frictionCoeff: Float): Float {
419 | var distance = 0f
420 | if (velocity != 0f) {
421 | val decelMinusOne = DECELERATION_RATE - 1.0
422 | val l = Math.pow(
423 | (INFLEXION * Math.abs(velocity / 4) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
424 | DECELERATION_RATE / decelMinusOne
425 | )
426 | distance =
427 | (l * mFlingFriction * frictionCoeff * mPhysicalCoeff * Math.signum(velocity)).toFloat()
428 | }
429 | return distance
430 | }
431 |
432 | /**
433 | * 根据时间计算位移距离,无方向性
434 | *
435 | * @param duration 动画时间
436 | */
437 | fun getDistanceByDuration(duration: Long): Float {
438 | return getDistanceByDuration(duration, 1f)
439 | }
440 |
441 | /**
442 | * 根据时间计算位移距离,无方向性
443 | *
444 | * @param duration 动画时间
445 | * @param frictionCoeff 摩擦系数
446 | */
447 | fun getDistanceByDuration(duration: Long, frictionCoeff: Float): Float {
448 | var distance = 0f
449 | if (duration > 0) {
450 | val base = Math.pow((duration / 1000f).toDouble(), DECELERATION_RATE.toDouble())
451 | distance = (base * mFlingFriction * frictionCoeff * mPhysicalCoeff).toFloat()
452 | }
453 | return distance
454 | }
455 |
456 | /**
457 | * 根据初速度计算持续时间
458 | *
459 | * @param velocity 初速度
460 | */
461 | fun getDurationByVelocity(velocity: Float): Long {
462 | return getDurationByVelocity(velocity, 1f)
463 | }
464 |
465 | /**
466 | * 根据初速度计算持续时间
467 | *
468 | * @param velocity 初速度
469 | * @param frictionCoeff 摩擦系数
470 | */
471 | fun getDurationByVelocity(velocity: Float, frictionCoeff: Float): Long {
472 | var duration: Long = 0
473 | if (velocity != 0f) {
474 | val decelMinusOne = DECELERATION_RATE - 1.0
475 | duration = (1000 * Math.pow(
476 | (INFLEXION * Math.abs(velocity / 4) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
477 | 1 / decelMinusOne
478 | )).toLong()
479 | }
480 | return duration
481 | }
482 |
483 | /**
484 | * 根据位移距离计算持续时间
485 | *
486 | * @param distance 位移距离
487 | */
488 | fun getDurationByDistance(distance: Float): Long {
489 | return getDurationByDistance(distance, 1f)
490 | }
491 |
492 | /**
493 | * 根据位移距离计算持续时间
494 | *
495 | * @param distance 位移距离
496 | * @param frictionCoeff 摩擦系数
497 | */
498 | fun getDurationByDistance(distance: Float, frictionCoeff: Float): Long {
499 | var duration: Long = 0
500 | if (distance != 0f) {
501 | val base =
502 | (Math.abs(distance) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble()
503 | duration = (1000 * Math.pow(base, (1 / DECELERATION_RATE).toDouble())).toLong()
504 | }
505 | return duration
506 | }
507 |
508 | /**
509 | * 根据位移距离和时间重置动摩擦系数
510 | *
511 | * @param distance 位移距离
512 | */
513 | private fun resetFlingFriction(distance: Float, duration: Long) {
514 | val base = Math.pow((duration / 1000f).toDouble(), DECELERATION_RATE.toDouble())
515 | mFlingFriction = Math.abs(distance / (base * mPhysicalCoeff)).toFloat()
516 | }
517 |
518 | /**
519 | * 设置动摩擦系数倍率
520 | */
521 | fun setFlingFrictionRatio(ratio: Float) {
522 | if (ratio > 0) {
523 | mFlingFrictionRatio = ratio
524 | }
525 | }
526 |
527 | private inner class DecelerateEvaluator :
528 | TypeEvaluator {
529 | override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
530 | var fraction = fraction
531 | return if (!isBouncing) { //禁用回弹效果(可能越界,需要提前结束动画)
532 | val distance = getDistance(fraction, duration, mDistance, mFrictionCoeff)
533 | if (isOutside && (distance - endValue + startValue) * mDistance > 0) { //越界了
534 | if (fraction > 0 && fraction < 1) { //动画还将继续,提前结束
535 | end()
536 | }
537 | return endValue
538 | }
539 | startValue + distance
540 | } else if (isOutside) { //回弹效果触发(发生越界)
541 | val bounceFraction = 1f * mBounceDuration / duration
542 | if (fraction <= 1f - bounceFraction) { //第一阶段,按原本位移距离和动画时间进行计算
543 | //校正进度值
544 | fraction = fraction * duration / mOriginalDuration
545 | val distance = getDistance(fraction, mOriginalDuration, mOriginalDistance, 1f)
546 | startValue + distance
547 | } else if (fraction <= 1f - bounceFraction / 2f) { //第二阶段,越过边界开始减速
548 | //校正进度值
549 | fraction = 2f * (fraction + bounceFraction - 1f) / bounceFraction
550 | val distance =
551 | getDistance(fraction, mBounceDuration / 2, mBounceDistance, mFrictionCoeff)
552 | endValue + distance
553 | } else { //第三阶段,加速回归边界
554 | //校正进度值
555 | fraction = 2f * (1f - fraction) / bounceFraction
556 | val distance =
557 | getDistance(fraction, mBounceDuration / 2, mBounceDistance, mFrictionCoeff)
558 | endValue + distance
559 | }
560 | } else { //回弹效果未触发(未越界)
561 | val distance = getDistance(fraction, duration, mDistance, mFrictionCoeff)
562 | startValue + distance
563 | }
564 | }
565 |
566 | /**
567 | * 计算位移距离
568 | *
569 | * @param fraction 动画进度
570 | * @param duration 动画时间
571 | * @param distance 动画总距离
572 | * @param frictionCoeff 摩擦系数
573 | */
574 | private fun getDistance(
575 | fraction: Float,
576 | duration: Long,
577 | distance: Float,
578 | frictionCoeff: Float
579 | ): Float {
580 | //获取剩余动画时间
581 | val surplusDuration = ((1f - fraction) * duration).toLong()
582 | //计算剩余位移距离
583 | val surplusDistance =
584 | getDistanceByDuration(surplusDuration, frictionCoeff) * Math.signum(distance)
585 | //计算位移距离
586 | return distance - surplusDistance
587 | }
588 | }
589 |
590 | /**
591 | * 减速动画
592 | *
593 | * @param context 上下文
594 | * @param bounceCoeff 回弹系数
595 | * @param bouncing 是否开启回弹效果
596 | */
597 | init {
598 | isBouncing = isBouncing
599 | mDecelerateEvaluator = DecelerateEvaluator()
600 | mPhysicalCoeff = (context.resources.displayMetrics.density
601 | * SensorManager.GRAVITY_EARTH * 5291.328f) // = 160.0f * 39.37f * 0.84f
602 | interpolator = LinearInterpolator()
603 | }
604 | }
605 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/OutsideClickDialog.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.view.KeyEvent
6 | import android.view.MotionEvent
7 | import android.view.ViewConfiguration
8 |
9 | /**
10 | * 提供返回事件,外部点击事件
11 | */
12 | class OutsideClickDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) {
13 |
14 | private val mCancelable = true
15 |
16 | var onBackClickListener: (() -> Boolean)? = null
17 | var onOutsideClickListener: (() -> Boolean)? = null
18 |
19 | override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
20 | if (keyCode == KeyEvent.KEYCODE_BACK) {
21 | val consume = onBackClickListener?.invoke()
22 | if (consume == true) {
23 | return true
24 | }
25 | }
26 | return super.onKeyDown(keyCode, event)
27 | }
28 |
29 | override fun onTouchEvent(event: MotionEvent): Boolean {
30 | if (mCancelable && isShowing &&
31 | (event.action == MotionEvent.ACTION_UP && isOutOfBounds(context, event) ||
32 | event.action == MotionEvent.ACTION_OUTSIDE)
33 | ) {
34 | val consume = onOutsideClickListener?.invoke()
35 | if (consume == true) {
36 | return true
37 | }
38 | }
39 | return super.onTouchEvent(event)
40 | }
41 |
42 | private fun isOutOfBounds(context: Context, event: MotionEvent): Boolean {
43 | val x = event.x.toInt()
44 | val y = event.y.toInt()
45 | val slop = ViewConfiguration.get(context).scaledWindowTouchSlop
46 | val decorView = window?.decorView
47 | return (x < -slop || y < -slop
48 | || x > decorView?.width!! + slop
49 | || y > decorView.height + slop)
50 | }
51 | }
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/PickAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | /**
4 | * @author Simon Lee
5 | * @e-mail jmlixiaomeng@163.com
6 | * @github https://github.com/Simon-Leeeeeeeee/SLWidget
7 | * @createdTime 2018-05-17
8 | */
9 | interface PickAdapter {
10 | /**
11 | * 返回数据总个数
12 | */
13 | val count: Int
14 |
15 | /**
16 | * 返回一条对应index的数据
17 | */
18 | fun getItem(position: Int): String?
19 | }
--------------------------------------------------------------------------------
/datetimepicker/src/main/java/com/gredicer/datetimepicker/ScrollPickerView.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import android.animation.ValueAnimator
4 | import android.animation.ValueAnimator.AnimatorUpdateListener
5 | import android.annotation.SuppressLint
6 | import android.content.Context
7 | import android.graphics.*
8 | import android.text.TextPaint
9 | import android.util.AttributeSet
10 | import android.view.*
11 | import kotlin.math.abs
12 |
13 |
14 | /**
15 | * @author Simon Lee
16 | * @e-mail jmlixiaomeng@163.com
17 | * @github https://github.com/Simon-Leeeeeeeee/SLWidget
18 | * @createdTime 2018-05-11
19 | */
20 | class ScrollPickerView : View, AnimatorUpdateListener {
21 | /**
22 | * dp&sp转px的系数
23 | */
24 | private var mDensityDP = 0f
25 | private var mDensitySP = 0f
26 |
27 | /**
28 | * LayoutParams宽度
29 | */
30 | private var mLayoutWidth = 0
31 |
32 | /**
33 | * LayoutParams高度
34 | */
35 | private var mLayoutHeight = 0
36 |
37 | /**
38 | * 显示行数,仅高度为wrap_content时有效。默认值5
39 | */
40 | private var mTextRows = 0
41 |
42 | /**
43 | * 文本的行高
44 | */
45 | private var mRowHeight = 0f
46 |
47 | /**
48 | * 文本的行距。默认4dp
49 | */
50 | private var mRowSpacing = 0f
51 |
52 | /**
53 | * item的高度,等于mRowHeight+mRowSpacing
54 | */
55 | private var mItemHeight = 0f
56 |
57 | /**
58 | * 字体大小。默认16sp
59 | */
60 | private var mTextSize = 0f
61 |
62 | /**
63 | * 选中项的缩放比例。默认2
64 | */
65 | private var mTextRatio = 0f
66 |
67 | /**
68 | * 文本格式,当宽为wrap_content时用于计算宽度
69 | */
70 | private var mTextFormat: String? = null
71 |
72 | /**
73 | * 中部字体颜色
74 | */
75 | private var mTextColor_Center = 0
76 |
77 | /**
78 | * 外部字体颜色
79 | */
80 | private var mTextColor_Outside = 0
81 |
82 | /**
83 | * 是否开启循环
84 | */
85 | private var mLoopEnable = false
86 |
87 | /**
88 | * 中部item的position
89 | */
90 | private var mMiddleItemPostion = 0
91 |
92 | /**
93 | * 中部item的偏移量,取值范围( -mItenHeight/2F , mItenHeight/2F ]
94 | */
95 | private var mMiddleItemOffset = 0f
96 |
97 | /**
98 | * 绘制区域中点的Y坐标
99 | */
100 | private var mCenterY = 0f
101 |
102 | /**
103 | * 总的累计偏移量,指针上移,position增大,偏移量增加
104 | */
105 | private var mTotalOffset = 0f
106 |
107 | /**
108 | * 文本对齐方式
109 | */
110 | private var mGravity = 0
111 |
112 | /**
113 | * 文本绘制起始点的X坐标
114 | */
115 | private var mDrawingOriginX = 0f
116 |
117 | /**
118 | * 存储每行文本边界值,用于计算文本的高度
119 | */
120 | private var mTextBounds: Rect? = null
121 |
122 | /**
123 | * 记录触摸事件的Y坐标
124 | */
125 | private var mStartY = 0f
126 |
127 | /**
128 | * 触摸移动最小距离
129 | */
130 | private var mTouchSlop = 0
131 |
132 | /**
133 | * 触摸点的ID
134 | */
135 | private var mTouchPointerId = 0
136 |
137 | /**
138 | * 是否触摸移动(手指在屏幕上拖动)
139 | */
140 | private var isMoveAction = false
141 |
142 | /**
143 | * 是否切换了触摸点(多点触摸中的手指切换)
144 | */
145 | private var isSwitchTouchPointer = false
146 |
147 | /**
148 | * 用于记录指定的position
149 | */
150 | private var mSpecifyPosition: Int? = null
151 | private var mMatrix: Matrix? = null
152 |
153 | /**
154 | * 减速动画
155 | */
156 | private var mDecelerateAnimator: DecelerateAnimator? = null
157 |
158 | /**
159 | * 线性颜色选择器
160 | */
161 | private var mLinearShader: LinearGradient? = null
162 |
163 | /**
164 | * 速度追踪器,结束触摸事件时计算手势速度,用于减速动画
165 | */
166 | private var mVelocityTracker: VelocityTracker? = null
167 | private var mTextPaint: TextPaint? = null
168 | private var mAdapter: PickAdapter? = null
169 | private var mItemSelectedListener: OnItemSelectedListener? = null
170 |
171 | interface OnItemSelectedListener {
172 | /**
173 | * 选中时的回调
174 | */
175 | fun onItemSelected(view: View?, position: Int)
176 | }
177 |
178 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
179 | initView(context, attrs)
180 | }
181 |
182 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
183 | context,
184 | attrs,
185 | defStyleAttr
186 | ) {
187 | initView(context, attrs)
188 | }
189 |
190 | private fun initView(context: Context, attributeSet: AttributeSet?) {
191 | mDensityDP = context.resources.displayMetrics.density //DP密度
192 | mDensitySP = context.resources.displayMetrics.scaledDensity //SP密度
193 | val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ScrollPickerView)
194 | mTextRows = typedArray.getInteger(R.styleable.ScrollPickerView_scrollpicker_rows, 5)
195 | mTextSize = typedArray.getDimension(
196 | R.styleable.ScrollPickerView_scrollpicker_textSize,
197 | 16 * mDensitySP
198 | )
199 | mTextRatio = typedArray.getFloat(R.styleable.ScrollPickerView_scrollpicker_textRatio, 2f)
200 | mRowSpacing = typedArray.getDimension(R.styleable.ScrollPickerView_scrollpicker_spacing, 0f)
201 | mTextFormat = typedArray.getString(R.styleable.ScrollPickerView_scrollpicker_textFormat)
202 | mTextColor_Center = typedArray.getColor(
203 | R.styleable.ScrollPickerView_scrollpicker_textColor_center,
204 | -0x2277de
205 | )
206 | mTextColor_Outside = typedArray.getColor(
207 | R.styleable.ScrollPickerView_scrollpicker_textColor_outside,
208 | -0x2267
209 | )
210 | mLoopEnable = typedArray.getBoolean(R.styleable.ScrollPickerView_scrollpicker_loop, true)
211 | mGravity =
212 | typedArray.getInt(R.styleable.ScrollPickerView_scrollpicker_gravity, GRAVITY_LEFT)
213 | mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
214 | typedArray.recycle()
215 |
216 | //初始化画笔工具
217 | initTextPaint()
218 | //计算行高
219 | measureTextHeight()
220 | mMatrix = Matrix() //用户记录偏移量并设置给颜色渐变工具
221 | mTextBounds = Rect() //用于计算每行文本边界区域
222 | //减速动画
223 | mDecelerateAnimator = DecelerateAnimator(context)
224 | mDecelerateAnimator!!.addUpdateListener(this)
225 | }
226 |
227 | /**
228 | * 初始化画笔工具
229 | */
230 | private fun initTextPaint() {
231 | mTextPaint = TextPaint()
232 | //防抖动
233 | mTextPaint!!.isDither = true
234 | //抗锯齿
235 | mTextPaint!!.isAntiAlias = true
236 | //不要文本缓存
237 | mTextPaint!!.isLinearText = true
238 | //设置亚像素
239 | mTextPaint!!.isSubpixelText = true
240 | //字体加粗
241 | mTextPaint!!.isFakeBoldText = true
242 |
243 | //设置字体大小
244 | mTextPaint!!.textSize = mTextSize
245 | //等宽字体
246 | mTextPaint!!.typeface = Typeface.MONOSPACE
247 | when (mGravity) {
248 | GRAVITY_LEFT -> {
249 | mTextPaint!!.textAlign = Paint.Align.LEFT
250 | }
251 | GRAVITY_CENTER -> {
252 | mTextPaint!!.textAlign = Paint.Align.CENTER
253 | }
254 | GRAVITY_RIGHT -> {
255 | mTextPaint!!.textAlign = Paint.Align.RIGHT
256 | }
257 | }
258 | }
259 |
260 | /**
261 | * 计算行高
262 | */
263 | private fun measureTextHeight() {
264 | val fontMetrics = mTextPaint!!.fontMetrics
265 | //确定行高
266 | mRowHeight =
267 | abs(fontMetrics.descent - fontMetrics.ascent) * if (mTextRatio > 1) mTextRatio else 1f
268 | //行距不得小于负行高的一半
269 | if (mRowSpacing < -mRowHeight / 2f) {
270 | mRowSpacing = -mRowHeight / 2f
271 | }
272 | mItemHeight = mRowHeight + mRowSpacing
273 | }
274 |
275 | fun setOnItemSelectedListener(itemSelectedListener: OnItemSelectedListener?) {
276 | mItemSelectedListener = itemSelectedListener
277 | }
278 |
279 | override fun setLayoutParams(params: ViewGroup.LayoutParams) {
280 | mLayoutWidth = params.width
281 | mLayoutHeight = params.height
282 | super.setLayoutParams(params)
283 | }
284 |
285 | /**
286 | * 计算PickerView的高宽,会多次调用,包括隐藏导航键也会调用
287 | */
288 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
289 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
290 | var widthSize = MeasureSpec.getSize(widthMeasureSpec)
291 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
292 | var heightSize = MeasureSpec.getSize(heightMeasureSpec)
293 | if (mLayoutWidth == ViewGroup.LayoutParams.WRAP_CONTENT && widthMode != MeasureSpec.EXACTLY) { //宽为WRAP
294 | widthSize = if (mTextFormat != null) {
295 | Math.ceil((mTextPaint!!.measureText(mTextFormat) * if (mTextRatio > 1) mTextRatio else 1f).toDouble())
296 | .toInt() + paddingLeft + paddingRight
297 | } else {
298 | paddingLeft + paddingRight
299 | }
300 | }
301 | if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT && heightMode != MeasureSpec.EXACTLY) { //高为WRAP
302 | heightSize =
303 | Math.ceil((mRowHeight * mTextRows + mRowSpacing * (mTextRows - mTextRows % 2)).toDouble())
304 | .toInt() + paddingTop + paddingBottom
305 | }
306 | setMeasuredDimension(
307 | resolveSize(widthSize, widthMeasureSpec),
308 | resolveSize(heightSize, heightMeasureSpec)
309 | )
310 | }
311 |
312 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
313 | super.onSizeChanged(w, h, oldw, oldh)
314 | measureOriginal() //计算中心位置、绘制起点
315 | setPaintShader() //设置颜色线性渐变
316 | }
317 |
318 | /**
319 | * 计算中心位置、绘制起点
320 | */
321 | private fun measureOriginal() {
322 | //计算绘制区域高度
323 | val drawHeight = height - paddingTop - paddingBottom
324 | //计算中心的Y值
325 | mCenterY = drawHeight / 2f + paddingTop
326 | when (mGravity) {
327 | GRAVITY_LEFT -> {
328 | mDrawingOriginX = paddingLeft.toFloat()
329 | }
330 | GRAVITY_CENTER -> {
331 | mDrawingOriginX = (width + paddingLeft - paddingRight) / 2f
332 | }
333 | GRAVITY_RIGHT -> {
334 | mDrawingOriginX = (width - paddingRight).toFloat()
335 | }
336 | }
337 | }
338 |
339 | /**
340 | * 设置颜色线性渐变
341 | */
342 | private fun setPaintShader() {
343 | mLinearShader = LinearGradient(
344 | 0f,
345 | mCenterY - (0.5f * mRowHeight + mItemHeight),
346 | 0f,
347 | mCenterY + (0.5f * mRowHeight + mItemHeight),
348 | intArrayOf(mTextColor_Outside, mTextColor_Center, mTextColor_Outside),
349 | floatArrayOf(0f, 0.5f, 1f),
350 | Shader.TileMode.CLAMP
351 | )
352 | mTextPaint!!.shader = mLinearShader
353 | }
354 |
355 | @SuppressLint("ClickableViewAccessibility")
356 | override fun onTouchEvent(event: MotionEvent): Boolean {
357 | if (mAdapter == null) {
358 | return super.onTouchEvent(event)
359 | }
360 | if (mVelocityTracker == null) {
361 | mVelocityTracker = VelocityTracker.obtain()
362 | }
363 | mVelocityTracker!!.addMovement(event)
364 | val actionIndex = event.actionIndex
365 | when (event.actionMasked) {
366 | MotionEvent.ACTION_DOWN -> {
367 | isSwitchTouchPointer = false
368 | //当前有减速动画未结束,则取消该动画,并直接进入滑动状态
369 | if (mDecelerateAnimator!!.isStarted) {
370 | isMoveAction = true
371 | mDecelerateAnimator!!.cancel()
372 | } else {
373 | isMoveAction = false
374 | }
375 | //记录偏移坐标
376 | mStartY = event.getY(actionIndex)
377 | //记录当前控制指针ID
378 | mTouchPointerId = event.getPointerId(actionIndex)
379 | }
380 | MotionEvent.ACTION_POINTER_UP -> {
381 |
382 | //如果抬起的指针是当前控制指针,则进行切换
383 | if (event.getPointerId(actionIndex) == mTouchPointerId) {
384 | mVelocityTracker!!.clear()
385 | //从列表中选择一个指针(非当前抬起的指针)作为下一个控制指针
386 | var index = 0
387 | while (index < event.pointerCount) {
388 | if (index != actionIndex) {
389 | //重置偏移坐标
390 | mStartY = event.getY(index)
391 | //重置触摸ID
392 | mTouchPointerId = event.getPointerId(index)
393 | //标记进行过手指切换
394 | isSwitchTouchPointer = true
395 | break
396 | }
397 | index++
398 | }
399 | }
400 | }
401 | MotionEvent.ACTION_MOVE -> {
402 |
403 | //只响应当前控制指针的移动操作
404 | var index = 0
405 | while (index < event.pointerCount) {
406 | if (event.getPointerId(index) == mTouchPointerId) {
407 | //计算偏移量,指针上移偏移量为正
408 | val offset = mStartY - event.getY(index)
409 | if (isMoveAction) {
410 | //已是滑动状态,累加偏移量,记录偏移坐标,请求重绘
411 | mTotalOffset += offset
412 | mStartY = event.getY(index)
413 | super.invalidate()
414 | } else if (Math.abs(offset) >= mTouchSlop) {
415 | //进入滑动状态,重置偏移坐标,标记当前为滑动状态
416 | mStartY = event.getY(index)
417 | isMoveAction = true
418 | }
419 | break
420 | }
421 | index++
422 | }
423 | }
424 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
425 |
426 | //计算偏移量,指针上移偏移量为正
427 | val offset = mStartY - event.getY(actionIndex)
428 | if (isMoveAction) {
429 | isMoveAction = false
430 | //计算手势速度
431 | mVelocityTracker!!.computeCurrentVelocity(1500)
432 | val velocityY = -mVelocityTracker!!.getYVelocity(mTouchPointerId)
433 | //累加偏移量
434 | mTotalOffset += offset
435 | //开启减速动画
436 | startDecelerateAnimator(mTotalOffset, velocityY, 0f, mItemHeight)
437 | } else if (!isSwitchTouchPointer && Math.abs(offset) < mTouchSlop) {
438 | //计算触摸点相对于中心位置的偏移距离
439 | val distance = event.getY(actionIndex) - mCenterY
440 | //开启减速动画
441 | startDecelerateAnimator(mTotalOffset, 0f, distance, mItemHeight)
442 | }
443 | if (mVelocityTracker != null) {
444 | mVelocityTracker!!.recycle()
445 | mVelocityTracker = null
446 | }
447 | }
448 | }
449 | return true
450 | }
451 |
452 | /**
453 | * 开始减速动画
454 | *
455 | * @param startValue 初始位移值
456 | * @param velocity 初始速度
457 | * @param distance 移动距离
458 | * @param modulus 距离的模
459 | */
460 | private fun startDecelerateAnimator(
461 | startValue: Float,
462 | velocity: Float,
463 | distance: Float,
464 | modulus: Float
465 | ) {
466 | val minValue = -1f
467 | val maxValue: Float = if (mLoopEnable) -1f else (mAdapter!!.count - 1) * mItemHeight + 1
468 | if (distance != 0f) {
469 | mDecelerateAnimator!!.startAnimator_Distance(
470 | startValue,
471 | minValue,
472 | maxValue,
473 | distance,
474 | modulus
475 | )
476 | } else {
477 | mDecelerateAnimator!!.startAnimator_Velocity(
478 | startValue,
479 | minValue,
480 | maxValue,
481 | velocity,
482 | modulus
483 | )
484 | }
485 | }
486 |
487 | override fun onAnimationUpdate(animation: ValueAnimator) {
488 | mTotalOffset = animation.animatedValue as Float
489 | super.invalidate()
490 | }
491 |
492 | override fun onDraw(canvas: Canvas) {
493 | if (!isInEditMode && mAdapter == null) {
494 | return
495 | }
496 | val measuredWidth = width
497 | val measuredHeight = height
498 | val paddingLeft = paddingLeft
499 | val paddingRight = paddingRight
500 | val paddingTop = paddingTop
501 | val paddingBottom = paddingBottom
502 |
503 | //根据padding限定绘制区域
504 | canvas.clipRect(
505 | paddingLeft,
506 | paddingTop,
507 | measuredWidth - paddingRight,
508 | measuredHeight - paddingBottom
509 | )
510 |
511 | //计算中部item的position及偏移量
512 | calculateMiddleItem()
513 | //绘制上半部分的item
514 | var curPosition = mMiddleItemPostion - 1
515 | var curOffset = mCenterY + mMiddleItemOffset - mRowHeight / 2f - mItemHeight
516 | while (curOffset > paddingTop - mRowHeight) {
517 | //绘制文本
518 | drawText(canvas, curPosition, curOffset)
519 | curOffset -= mItemHeight
520 | curPosition--
521 | }
522 |
523 | //绘制中部及下半部分的item
524 | curPosition = mMiddleItemPostion
525 | curOffset = mCenterY + mMiddleItemOffset - mRowHeight / 2f
526 | while (curOffset < measuredHeight - paddingBottom) {
527 | //绘制文本
528 | drawText(canvas, curPosition, curOffset)
529 | //下一个
530 | curOffset += mItemHeight
531 | curPosition++
532 | }
533 | //动画结束,进行选中回调
534 | if (!isMoveAction && !mDecelerateAnimator!!.isStarted && mItemSelectedListener != null) {
535 | //回调监听
536 | mItemSelectedListener!!.onItemSelected(this, mMiddleItemPostion)
537 | }
538 | }
539 |
540 | /**
541 | * 根据总偏移量计算中部item的偏移量及position
542 | * 偏移量的取值范围为(-mItenHeight/2F , mItenHeight/2F]
543 | */
544 | private fun calculateMiddleItem() {
545 | //计算偏移了多少个完整item
546 | var count =
547 | if (mSpecifyPosition != null) mSpecifyPosition!! else (mTotalOffset / mItemHeight).toInt()
548 | if (mSpecifyPosition != null) {
549 | if (mDecelerateAnimator!!.isStarted) {
550 | mDecelerateAnimator!!.cancel()
551 | }
552 | mTotalOffset = mSpecifyPosition!! * mItemHeight
553 | mMiddleItemOffset = 0f
554 | mSpecifyPosition = null
555 | } else {
556 | //对偏移量取余,注意这里不用取余运算符,因为可能造成严重错误!
557 | val offsetRem = mTotalOffset - mItemHeight * count //取值范围( -mItenHeight , mItenHeight )
558 | mMiddleItemOffset = if (offsetRem >= mItemHeight / 2f) {
559 | count++
560 | mItemHeight - offsetRem
561 | } else if (offsetRem >= -mItemHeight / 2f) {
562 | -offsetRem
563 | } else {
564 | count--
565 | -mItemHeight - offsetRem
566 | }
567 | }
568 | //对position取模
569 | mMiddleItemPostion = getRealPosition(count)
570 | //如果停止触摸且动画结束,对最终值和偏移量进行校正
571 | if (!isMoveAction && !mDecelerateAnimator!!.isStarted) {
572 | if (mMiddleItemPostion < 0 || mAdapter == null || mAdapter!!.count < 1) {
573 | mMiddleItemPostion = 0
574 | } else if (mMiddleItemPostion >= mAdapter!!.count) {
575 | mMiddleItemPostion = mAdapter!!.count - 1
576 | }
577 | mTotalOffset = mMiddleItemPostion * mItemHeight
578 | }
579 | }
580 |
581 | /**
582 | * 绘制文本
583 | */
584 | private fun drawText(canvas: Canvas, position: Int, offsetY: Float) {
585 | //对position取模
586 | var position = position
587 | position = getRealPosition(position)
588 | //position未越界
589 | if (isInEditMode || position >= 0 && position < mAdapter!!.count) {
590 | //获取文本
591 | val text = getDrawingText(position)
592 | if (text != null) {
593 | canvas.save()
594 | //平移画布
595 | canvas.translate(0f, offsetY)
596 | //操作线性颜色渐变
597 | mMatrix!!.setTranslate(0f, -offsetY)
598 | mLinearShader!!.setLocalMatrix(mMatrix)
599 | //计算缩放比例
600 | val scaling = getScaling(offsetY)
601 | canvas.scale(scaling, scaling, mDrawingOriginX, mRowHeight / 2f)
602 | //获取文本尺寸
603 | mTextPaint!!.getTextBounds(text, 0, text.length, mTextBounds)
604 | //根据文本尺寸计算基线位置
605 | val baseLineY = (mRowHeight - mTextBounds!!.top - mTextBounds!!.bottom) / 2f
606 | //绘制文本
607 | canvas.drawText(text, mDrawingOriginX, baseLineY, mTextPaint!!)
608 | canvas.restore()
609 | }
610 | }
611 | }
612 |
613 | /**
614 | * 循环模式下对position取模
615 | */
616 | private fun getRealPosition(position: Int): Int {
617 | var position = position
618 | if (mLoopEnable && mAdapter != null && mAdapter!!.count > 0) {
619 | position %= mAdapter!!.count
620 | if (position < 0) {
621 | position += mAdapter!!.count
622 | }
623 | }
624 | return position
625 | }
626 |
627 | /**
628 | * 根据获取要绘制的文本内容
629 | */
630 | private fun getDrawingText(position: Int): String? {
631 | if (isInEditMode) {
632 | return if (mTextFormat != null) mTextFormat else ("item$position").toString()
633 | }
634 | return if (position >= 0 && position < mAdapter!!.count) {
635 | mAdapter!!.getItem(position)
636 | } else null
637 | }
638 |
639 | /**
640 | * 根据偏移量计算缩放比例
641 | */
642 | private fun getScaling(offsetY: Float): Float {
643 | val abs = Math.abs(offsetY + mRowHeight / 2f - mCenterY)
644 | return if (abs < mItemHeight) {
645 | (1 - abs / mItemHeight) * (mTextRatio - 1f) + 1f
646 | } else {
647 | 1f
648 | }
649 | }
650 |
651 | /**
652 | * 设置适配器
653 | */
654 | fun setAdapter(adapter: PickAdapter?) {
655 | mAdapter = adapter
656 | super.invalidate()
657 | }
658 |
659 | /**
660 | * 设置当前选中项
661 | */
662 | fun setSelectedPosition(position: Int) {
663 | if (mAdapter == null) return
664 | if (position < 0 || position >= mAdapter!!.count) {
665 | throw ArrayIndexOutOfBoundsException()
666 | }
667 | if (mDecelerateAnimator!!.isStarted) {
668 | mDecelerateAnimator!!.cancel()
669 | }
670 | // 如果在onMeasure之前设置选中项,mItemHeight为0,无法得到正确偏移量,因此这里不能直接计算mTotalOffset
671 | mSpecifyPosition = position
672 | super.invalidate()
673 | }
674 |
675 | /**
676 | * 获取当前选中项
677 | */
678 | fun getSelectedPosition(): Int {
679 | return if (isMoveAction || mAdapter == null || mDecelerateAnimator!!.isStarted) {
680 | -1
681 | } else mMiddleItemPostion
682 | }
683 |
684 |
685 | /**
686 | * 设置文本对齐方式,计算文本绘制起始点的X坐标
687 | */
688 | fun setGravity(gravity: Int) {
689 | when (gravity) {
690 | GRAVITY_LEFT -> {
691 | mTextPaint!!.textAlign = Paint.Align.LEFT
692 | mDrawingOriginX = paddingLeft.toFloat()
693 | }
694 | GRAVITY_CENTER -> {
695 | mTextPaint!!.textAlign = Paint.Align.CENTER
696 | mDrawingOriginX = (width + paddingLeft - paddingRight) / 2f
697 | }
698 | GRAVITY_RIGHT -> {
699 | mTextPaint!!.textAlign = Paint.Align.RIGHT
700 | mDrawingOriginX = (width - paddingRight).toFloat()
701 | }
702 | else -> return
703 | }
704 | mGravity = gravity
705 | super.invalidate()
706 | }
707 |
708 | fun isLoopEnable(): Boolean {
709 | return mLoopEnable
710 | }
711 |
712 | fun setLoopEnable(enable: Boolean) {
713 | if (mLoopEnable != enable) {
714 | mLoopEnable = enable
715 | //循环将关闭且正在减速动画
716 | if (!mLoopEnable && mDecelerateAnimator!!.isStarted && mAdapter != null) {
717 | //停止减速动画,并指定position以确保item对齐
718 | mDecelerateAnimator!!.cancel()
719 | //防止position越界
720 | mSpecifyPosition =
721 | if (mMiddleItemPostion < 0) 0 else if (mMiddleItemPostion >= mAdapter!!.count) mAdapter!!.count - 1 else mMiddleItemPostion
722 | }
723 | super.invalidate()
724 | }
725 | }
726 |
727 | /**
728 | * 设置文本显示的行数,仅当高为WRAP_CONTENT时有效
729 | */
730 | fun setTextRows(rows: Int) {
731 | if (mTextRows != rows) {
732 | mTextRows = rows
733 | if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
734 | super.requestLayout()
735 | }
736 | }
737 | }
738 |
739 | /**
740 | * 设置文本字体大小,单位px
741 | *
742 | * @param textSize 必须大于0
743 | */
744 | fun setTextSize(textSize: Float) {
745 | if (textSize > 0 && mTextSize != textSize) {
746 | mTextSize = textSize
747 | mTextPaint!!.textSize = mTextSize
748 | measureTextHeight()
749 | reInvalidate()
750 | }
751 | }
752 |
753 | /**
754 | * 设置文本行间距,单位px
755 | */
756 | fun setRowSpacing(rowSpacing: Float) {
757 | if (mRowSpacing != rowSpacing) {
758 | mRowSpacing = rowSpacing
759 | measureTextHeight()
760 | reInvalidate()
761 | }
762 | }
763 |
764 | /**
765 | * 设置放大倍数
766 | */
767 | fun setTextRatio(textRatio: Float) {
768 | if (mTextRatio != textRatio) {
769 | mTextRatio = textRatio
770 | measureTextHeight()
771 | reInvalidate()
772 | }
773 | }
774 |
775 | /**
776 | * 设置中部字体颜色
777 | */
778 | fun setCenterTextColor(color: Int) {
779 | if (mTextColor_Center != color) {
780 | mTextColor_Center = color
781 | setPaintShader() //设置颜色线性渐变
782 | invalidate()
783 | }
784 | }
785 |
786 | /**
787 | * 设置外部字体颜色
788 | */
789 | fun setOutsideTextColor(color: Int) {
790 | if (mTextColor_Outside != color) {
791 | mTextColor_Outside = color
792 | setPaintShader() //设置颜色线性渐变
793 | invalidate()
794 | }
795 | }
796 |
797 | private fun reInvalidate() {
798 | if (mDecelerateAnimator!!.isStarted) {
799 | mDecelerateAnimator!!.cancel()
800 | }
801 | mSpecifyPosition = mMiddleItemPostion
802 | if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
803 | super.requestLayout()
804 | } else {
805 | super.invalidate()
806 | }
807 | }
808 |
809 | override fun canScrollVertically(direction: Int): Boolean {
810 | return true
811 | }
812 |
813 | companion object {
814 | /**
815 | * 文本对齐方式,居左
816 | */
817 | const val GRAVITY_LEFT = 3
818 |
819 | /**
820 | * 文本对齐方式,居右
821 | */
822 | const val GRAVITY_RIGHT = 5
823 |
824 | /**
825 | * 文本对齐方式,居中
826 | */
827 | const val GRAVITY_CENTER = 17
828 | }
829 | }
830 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/res/drawable/shape_button_corners.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/res/drawable/shape_dialog_corners.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/res/layout/fragment_datetime_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
31 |
32 |
39 |
40 |
46 |
47 |
59 |
60 |
68 |
69 |
70 |
71 |
77 |
78 |
90 |
91 |
99 |
100 |
101 |
107 |
108 |
120 |
121 |
129 |
130 |
131 |
137 |
138 |
150 |
151 |
159 |
160 |
161 |
167 |
168 |
180 |
181 |
189 |
190 |
191 |
192 |
193 |
194 |
207 |
208 |
220 |
221 |
--------------------------------------------------------------------------------
/datetimepicker/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 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 测试
3 | show_datetime
4 | 请选择时间
5 | 年
6 | 月
7 | 日
8 | 时
9 | 分
10 | 确定
11 | 现在
12 |
--------------------------------------------------------------------------------
/datetimepicker/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/datetimepicker/src/test/java/com/gredicer/datetimepicker/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.gredicer.datetimepicker
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/demo.gif
--------------------------------------------------------------------------------
/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=-Xmx2048m -Dfile.encoding=UTF-8
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 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gredicer/datetimepicker/4a41584100e0ca00ae03427b1f3113fae2545a5f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jul 28 23:22:48 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "DatetimePickerDemo"
2 | include ':app'
3 | include ':datetimepicker'
4 |
--------------------------------------------------------------------------------