diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 7b46144..03852f9 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,6 +12,7 @@
+
diff --git a/app/build.gradle b/app/build.gradle
index 74f7f27..16b9fb3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,7 +21,7 @@ android {
minSdk 24
targetSdk 32
versionCode 1
- versionName "1.0"
+ versionName "1.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -104,8 +104,9 @@ dependencies {
// 文件选择器 https://github.com/rosuH/AndroidFilePicker/blob/master/README_CN.md
implementation 'me.rosuh:AndroidFilePicker:0.8.2'
- // 时间选择器 https://github.com/Gredicer/datetimepicker
- implementation 'com.github.Gredicer:datetimepicker:V1.0.0'
+// // 时间选择器 https://github.com/Gredicer/datetimepicker
+// implementation 'com.github.Gredicer:datetimepicker:V1.0.0'
+ implementation project(path: ':datetimepicker')
//带侧滑的自定义列表
implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
deleted file mode 100644
index 380cd07..0000000
Binary files a/app/release/app-release.apk and /dev/null differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
deleted file mode 100644
index d3e5748..0000000
--- a/app/release/output-metadata.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "version": 3,
- "artifactType": {
- "type": "APK",
- "kind": "Directory"
- },
- "applicationId": "com.navinfo.volvo",
- "variantName": "release",
- "elements": [
- {
- "type": "SINGLE",
- "filters": [],
- "attributes": [],
- "versionCode": 1,
- "versionName": "1.0",
- "outputFile": "app-release.apk"
- }
- ],
- "elementType": "File"
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt b/app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt
index 7543852..195792c 100644
--- a/app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt
+++ b/app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt
@@ -13,6 +13,7 @@ import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
+import android.widget.Button
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
@@ -35,6 +36,7 @@ import com.gredicer.datetimepicker.DateTimePickerFragment
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
+import com.navinfo.volvo.Constant
import com.navinfo.volvo.R
import com.navinfo.volvo.RecorderLifecycleObserver
import com.navinfo.volvo.database.entity.Attachment
@@ -65,7 +67,7 @@ class ObtainMessageFragment : Fragment() {
private var _binding: FragmentObtainMessageBinding? = null
private val obtainMessageViewModel by viewModels()
private val photoHelper by lazy {
- EasyMediaFile().setCrop(true)
+ EasyMediaFile().setCrop(false)
}
private val recorderLifecycleObserver by lazy {
RecorderLifecycleObserver()
@@ -179,6 +181,25 @@ class ObtainMessageFragment : Fragment() {
return root
}
+ override fun onResume() {
+ super.onResume()
+ if (obtainMessageViewModel.getMessageLiveData().value!=null&&Constant.message_status_send_over.equals((obtainMessageViewModel.getMessageLiveData().value as GreetingMessage).status)) {
+ binding.tvMessageTitle.isEnabled=false
+ binding.btnStartPhoto.isEnabled=false
+ binding.btnStartCamera.isEnabled=false
+ binding.btnStartRecord.isEnabled=false
+ binding.btnSelectSound.isEnabled=false
+ binding.edtSendFrom.isEnabled=false
+ binding.edtSendTo.isEnabled=false
+ binding.btnSendTime.isEnabled=false
+ binding.btnObtainMessageConfirm.isEnabled=false
+ binding.tvPhotoName.isEnabled = false
+ binding.tvAudioName.isEnabled = false
+ binding.imgPhotoDelete.isEnabled = false
+ binding.imgAudioDelete.isEnabled = false
+ }
+ }
+
fun initView() {
// 设置问候信息提示的红色星号
binding.tiLayoutTitle.markRequiredInRed()
@@ -236,7 +257,6 @@ class ObtainMessageFragment : Fragment() {
)
}
}
-
}
dialog.show(parentFragmentManager, "SelectSendTime")
}
@@ -256,7 +276,7 @@ class ObtainMessageFragment : Fragment() {
return
}
// 开始启动拍照界面
- photoHelper.setCrop(true).takePhoto(requireActivity())
+ photoHelper.setCrop(false).takePhoto(requireActivity())
}
override fun onDenied(permissions: MutableList, never: Boolean) {
@@ -274,7 +294,7 @@ class ObtainMessageFragment : Fragment() {
}
binding.btnStartPhoto.setOnClickListener {
- photoHelper.setCrop(true).selectPhoto(requireActivity())
+ photoHelper.setCrop(false).selectPhoto(requireActivity())
}
// 用户选择录音文件
diff --git a/app/src/main/res/layout/fragment_obtain_message.xml b/app/src/main/res/layout/fragment_obtain_message.xml
index bc59fb2..b7d9f3a 100644
--- a/app/src/main/res/layout/fragment_obtain_message.xml
+++ b/app/src/main/res/layout/fragment_obtain_message.xml
@@ -392,7 +392,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="确认提交">
+ android:text="修改保存">
\ No newline at end of file
diff --git a/datetimepicker/.gitignore b/datetimepicker/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/datetimepicker/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/datetimepicker/build.gradle b/datetimepicker/build.gradle
new file mode 100644
index 0000000..16c283e
--- /dev/null
+++ b/datetimepicker/build.gradle
@@ -0,0 +1,50 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'kotlin-android-extensions'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ minSdkVersion 19
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.10"
+ implementation 'androidx.core:core-ktx:1.6.0'
+ implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'com.google.android.material:material:1.4.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ implementation 'com.google.android.flexbox:flexbox:3.0.0'
+
+}
\ No newline at end of file
diff --git a/datetimepicker/consumer-rules.pro b/datetimepicker/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/datetimepicker/proguard-rules.pro b/datetimepicker/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/datetimepicker/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/datetimepicker/src/androidTest/java/com/gredicer/datetimepicker/ExampleInstrumentedTest.kt b/datetimepicker/src/androidTest/java/com/gredicer/datetimepicker/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..53e82da
--- /dev/null
+++ b/datetimepicker/src/androidTest/java/com/gredicer/datetimepicker/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.gredicer.datetimepicker
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.gredicer.datetimepicker.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/datetimepicker/src/main/AndroidManifest.xml b/datetimepicker/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cc41d6d
--- /dev/null
+++ b/datetimepicker/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/DatePickerAdapter.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DatePickerAdapter.kt
new file mode 100644
index 0000000..024c222
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DatePickerAdapter.kt
@@ -0,0 +1,53 @@
+package com.gredicer.datetimepicker
+
+import java.text.DecimalFormat
+
+/**
+ * 日期选择适配器
+ *
+ * @author Simon Lee
+ * @e-mail jmlixiaomeng@163.com
+ * @github https://github.com/Simon-Leeeeeeeee/SLWidget
+ * @createdTime 2018-05-17
+ */
+class DatePickerAdapter @JvmOverloads constructor(
+ var minValue: Int,
+ var maxValue: Int,
+ private val mDecimalFormat: DecimalFormat? = null
+) :
+ PickAdapter {
+ override val count: Int
+ get() = maxValue - minValue + 1
+
+ override fun getItem(position: Int): String? {
+ return if (position in 0 until count) {
+ if (mDecimalFormat == null) {
+ (minValue + position).toString()
+ } else {
+ mDecimalFormat.format((minValue + position).toLong())
+ }
+ } else null
+ }
+
+ fun getDate(position: Int): Int {
+ return if (position in 0 until count) {
+ minValue + position
+ } else 0
+ }
+
+ fun indexOf(valueString: String): Int {
+ val value: Int = try {
+ valueString.toInt()
+ } catch (e: NumberFormatException) {
+ return -1
+ }
+ return indexOf(value)
+ }
+
+ fun indexOf(value: Int): Int {
+ return if (value < minValue || value > maxValue) {
+ -1
+ } else value - minValue
+ }
+
+}
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/DateTimePickerFragment.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DateTimePickerFragment.kt
new file mode 100644
index 0000000..2df0cbb
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DateTimePickerFragment.kt
@@ -0,0 +1,460 @@
+package com.gredicer.datetimepicker
+
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.app.Activity
+import android.app.Dialog
+import android.graphics.Insets
+import android.os.Build
+import android.os.Bundle
+import android.util.DisplayMetrics
+import android.view.*
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.OvershootInterpolator
+import androidx.core.animation.doOnEnd
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_datetime_picker.*
+import java.text.DecimalFormat
+import java.util.*
+
+
+class DateTimePickerFragment : DialogFragment(), ScrollPickerView.OnItemSelectedListener {
+ private var window: Window? = null
+
+ // 当前模式 0-年月日时分 1-年 2-年月 3-年月日 4-时分
+ private var mMode: Int = 0
+
+ // 退出状态
+ private var exitStatus: Boolean = false
+
+ // 是否设置初始值
+ private var hasSetDefault: Boolean = false
+
+ // 初始时间
+ private var mDefaultTime = "2000-01-01 00:00:00"
+
+
+ private var mYearAdapter = DatePickerAdapter(1900, 2200, DecimalFormat("0000"))
+ private var mSelectedYear: Int = 0
+ private var mMonthAdapter = DatePickerAdapter(1, 12, DecimalFormat("00"))
+ private var mSelectedMonth: Int = 0
+ private var mDayAdapter = DatePickerAdapter(1, 31, DecimalFormat("00"))
+ private var mSelectedDay: Int = 0
+ private var mHourAdapter = DatePickerAdapter(0, 23, DecimalFormat("00"))
+ private var mSelectedHour: Int = 0
+ private var mMinuteAdapter = DatePickerAdapter(0, 59, DecimalFormat("00"))
+ private var mSelectedMinute: Int = 0
+
+
+ companion object {
+ fun newInstance(): DateTimePickerFragment {
+ return DateTimePickerFragment()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_datetime_picker, container, false)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (dialog != null && dialog!!.window != null) {
+ window = dialog!!.window!!
+ val params = window!!.attributes
+ params.width = WindowManager.LayoutParams.MATCH_PARENT
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT
+ // 显示在页面的底部
+ params.gravity = Gravity.BOTTOM
+ window!!.attributes = params
+ window!!.setBackgroundDrawableResource(R.drawable.shape_dialog_corners)
+ // dialog弹出后会点击屏幕或物理返回键,dialog不消失
+ dialog!!.setCancelable(true)
+ // dialog弹出后会点击屏幕,dialog不消失;点击物理返回键dialog消失
+ dialog!!.setCanceledOnTouchOutside(true)
+
+ enterAnimation()
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val mOutsideClickDialog = OutsideClickDialog(requireContext(), theme)
+ // 监听外部点击
+ mOutsideClickDialog.onOutsideClickListener = {
+ exitAnimation()
+ true
+ }
+ // 监听返回点击
+ mOutsideClickDialog.onBackClickListener = {
+ exitAnimation()
+ true
+ }
+ return mOutsideClickDialog
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ // 初始化退出状态为现在可以退出,不在退出状态
+ exitStatus = false
+
+ when (mMode) {
+ 0 -> {
+ initYear()
+ initMonth()
+ initDay()
+ initHour()
+ initMinute()
+ }
+ 1 -> {
+ initYear()
+ }
+ 2 -> {
+ initYear()
+ initMonth()
+ resetUI(2)
+ }
+ 3 -> {
+ initYear()
+ initMonth()
+ initDay()
+ resetUI(3)
+ }
+ 4 -> {
+ initHour()
+ initMinute()
+ resetUI(2)
+ }
+ }
+
+
+
+ if (!hasSetDefault) resetTime()
+
+ btn_back_now.setOnClickListener { resetTime() }
+
+ btn_enter.setOnClickListener {
+ listener?.onClickListener(returnTime())
+ exitAnimation()
+ }
+ }
+
+
+ var listener: OnClickListener? = null
+
+ interface OnClickListener {
+ fun onClickListener(selectTime: String)
+ }
+
+ override fun onItemSelected(view: View?, position: Int) {
+ when (view?.id) {
+ R.id.date_picker_year -> {
+ mSelectedYear = mYearAdapter.getDate(position)
+ // 根据年月计算日期的最大值,并刷新
+ mDayAdapter.maxValue = getMonthLastDay(mSelectedYear, mSelectedMonth)
+ }
+ R.id.date_picker_month -> {
+ mSelectedMonth = mMonthAdapter.getDate(position)
+ // 根据年月计算日期的最大值,并刷新
+ mDayAdapter.maxValue = getMonthLastDay(mSelectedYear, mSelectedMonth)
+ date_picker_day.setAdapter(mDayAdapter)
+ }
+ R.id.date_picker_day -> {
+ mSelectedDay = mDayAdapter.getDate(position)
+ }
+ R.id.date_picker_hour -> {
+ mSelectedHour = mHourAdapter.getDate(position)
+ }
+ R.id.date_picker_minute -> {
+ mSelectedMinute = mMinuteAdapter.getDate(position)
+ }
+ else -> {
+ }
+ }
+ showTime()
+ }
+
+ /**
+ * 增加年的显示
+ * */
+ fun mode(mode: Int): DateTimePickerFragment {
+ mMode = mode
+ return this
+ }
+
+
+ /**
+ * 设置初始值
+ * */
+ fun default(defaultTime: String): DateTimePickerFragment {
+ mDefaultTime = defaultTime
+ hasSetDefault = true
+ return this
+ }
+
+ /**
+ * 初始化年
+ * */
+ private fun initYear() {
+ year_show.visibility = View.VISIBLE
+ date_picker_year.setAdapter(mYearAdapter)
+ date_picker_year.setOnItemSelectedListener(this)
+ setSelectValue(0, mDefaultTime.substring(0, 4).toInt())
+ }
+
+ /**
+ * 初始化月
+ * */
+ private fun initMonth() {
+ month_show.visibility = View.VISIBLE
+ date_picker_month.setAdapter(mMonthAdapter)
+ date_picker_month.setOnItemSelectedListener(this)
+ setSelectValue(1, mDefaultTime.substring(5, 7).toInt())
+ }
+
+ /**
+ * 初始化日
+ * */
+ private fun initDay() {
+ day_show.visibility = View.VISIBLE
+ date_picker_day.setAdapter(mDayAdapter)
+ date_picker_day.setOnItemSelectedListener(this)
+ setSelectValue(2, mDefaultTime.substring(8, 10).toInt())
+ }
+
+ /**
+ * 初始化时
+ * */
+ private fun initHour() {
+ hour_show.visibility = View.VISIBLE
+ date_picker_hour.setAdapter(mHourAdapter)
+ date_picker_hour.setOnItemSelectedListener(this)
+ setSelectValue(3, mDefaultTime.substring(11, 13).toInt())
+ }
+
+ /**
+ * 初始化分
+ * */
+ private fun initMinute() {
+ minute_show.visibility = View.VISIBLE
+ date_picker_minute.setAdapter(mMinuteAdapter)
+ date_picker_minute.setOnItemSelectedListener(this)
+ setSelectValue(4, mDefaultTime.substring(14, 16).toInt())
+ }
+
+ /**
+ * 设置当前选择的值
+ * type: 0-年,1-月,2-日,3-时,4-分
+ * */
+ private fun setSelectValue(type: Int, value: Int) {
+ when (type) {
+ 0 -> {
+ date_picker_year.setSelectedPosition(mYearAdapter.indexOf(value))
+ }
+ 1 -> {
+ date_picker_month.setSelectedPosition(mMonthAdapter.indexOf(value))
+ }
+ 2 -> {
+ date_picker_day.setSelectedPosition(mDayAdapter.indexOf(value))
+ }
+ 3 -> {
+ date_picker_hour.setSelectedPosition(mHourAdapter.indexOf(value))
+ }
+ 4 -> {
+ date_picker_minute.setSelectedPosition(mMinuteAdapter.indexOf(value))
+ }
+ }
+ }
+
+ /**
+ * 重置UI
+ * */
+ private fun resetUI(showCount: Int) {
+ when (showCount) {
+ 2 -> {
+ fl_datetimepicker.setPadding(200, 0, 200, 0)
+ }
+ 3 -> {
+ fl_datetimepicker.setPadding(100, 0, 100, 0)
+ }
+ }
+ }
+
+ /**
+ * 文字显示当前的时间
+ * */
+ private fun showTime() {
+ var showText = ""
+ when (mMode) {
+ 0 -> {
+ showText += "$mSelectedYear 年"
+ showText += " ${formatTime(mSelectedMonth)} 月"
+ showText += " ${formatTime(mSelectedDay)} 日 "
+ showText += "${formatTime(mSelectedHour)} :"
+ showText += " ${formatTime(mSelectedMinute)}"
+ }
+ 1 -> {
+ showText = "$mSelectedYear 年"
+ }
+ 2 -> {
+ showText = "$mSelectedYear 年 ${formatTime(mSelectedMonth)} 月"
+ }
+ 3 -> {
+ showText += "$mSelectedYear 年"
+ showText += " ${formatTime(mSelectedMonth)} 月"
+ showText += " ${formatTime(mSelectedDay)} 日"
+ }
+ 4 -> {
+ showText = "${formatTime(mSelectedHour)}:${formatTime(mSelectedMinute)}"
+ }
+ }
+ tv_time_show.text = showText
+ }
+
+ /**
+ * 返回的时间
+ * */
+ private fun returnTime(): String {
+ var text = ""
+ when (mMode) {
+ 0 -> {
+ text += "$mSelectedYear-"
+ text += "${formatTime(mSelectedMonth)}-"
+ text += "${formatTime(mSelectedDay)} "
+ text += "${formatTime(mSelectedHour)}:"
+ text += formatTime(mSelectedMinute)
+ }
+ 1 -> {
+ text = "$mSelectedYear"
+ }
+ 2 -> {
+ text = "$mSelectedYear-${formatTime(mSelectedMonth)}"
+ }
+ 3 -> {
+ text = "$mSelectedYear-${formatTime(mSelectedMonth)}-${formatTime(mSelectedDay)}"
+ }
+ 4 -> {
+ text = "${formatTime(mSelectedHour)}:${formatTime(mSelectedMinute)}"
+ }
+ }
+ return text
+ }
+
+ /**
+ * 格式化时间
+ **/
+ private fun formatTime(value: Int): String {
+ return DecimalFormat("00").format(value)
+ }
+
+
+ /**
+ * 重置到现在的时间
+ * */
+ private fun resetTime() {
+ val calendar = Calendar.getInstance()
+ // 年
+ val year = calendar.get(Calendar.YEAR)
+ // 月
+ val month = calendar.get(Calendar.MONTH) + 1
+ // 日
+ val day = calendar.get(Calendar.DAY_OF_MONTH)
+ // 小时
+ val hour = calendar.get(Calendar.HOUR_OF_DAY)
+ // 分钟
+ val minute = calendar.get(Calendar.MINUTE)
+ mDayAdapter.maxValue = getMonthLastDay(year, month)
+ setSelectValue(0, year)
+ setSelectValue(1, month)
+ setSelectValue(2, day)
+ setSelectValue(3, hour)
+ setSelectValue(4, minute)
+ }
+
+ /**
+ * 进入动画
+ * */
+ private fun enterAnimation() {
+ val holder1 = PropertyValuesHolder.ofFloat("scaleX", 1f, 1f)
+ val holder2 = PropertyValuesHolder.ofFloat("scaleY", 0f, 1f)
+ val deCoverView = window!!.decorView
+ deCoverView.pivotY = getScreenHeight(context as Activity).toFloat() / 2
+ val scaleDown = ObjectAnimator.ofPropertyValuesHolder(deCoverView, holder1, holder2)
+ scaleDown.interpolator = OvershootInterpolator(0.7f)
+ scaleDown.duration = 200
+ scaleDown.start()
+ }
+
+ /**
+ * 退出动画
+ * */
+ private fun exitAnimation() {
+ if (exitStatus) return
+ exitStatus = true
+
+ val params = window!!.attributes
+ params.dimAmount = 0.1f
+ window!!.attributes = params
+
+ val a = getScreenHeight(context as Activity).toFloat() / 2
+ val holder1 = PropertyValuesHolder.ofFloat("scaleX", 1f, 1f)
+ val holder2 = PropertyValuesHolder.ofFloat("translationY", 0f, a)
+ val deCoverView = window!!.decorView
+ val scaleDown = ObjectAnimator.ofPropertyValuesHolder(deCoverView, holder1, holder2)
+ scaleDown.interpolator = DecelerateInterpolator()
+ scaleDown.duration = 200
+ scaleDown.start()
+ scaleDown.doOnEnd {
+ dismiss()
+ }
+
+ }
+
+ /**
+ * 获取屏幕的宽度
+ * */
+ private fun getScreenWidth(activity: Activity): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics = activity.windowManager.currentWindowMetrics
+ val insets: Insets = windowMetrics.windowInsets
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+ windowMetrics.bounds.width() - insets.left - insets.right
+ } else {
+ val displayMetrics = DisplayMetrics()
+ activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
+ displayMetrics.widthPixels
+ }
+ }
+
+ /**
+ * 获取屏幕的高度
+ * */
+ private fun getScreenHeight(activity: Activity): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics = activity.windowManager.currentWindowMetrics
+ val insets: Insets = windowMetrics.windowInsets
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+ windowMetrics.bounds.height() - insets.top - insets.bottom
+ } else {
+ val displayMetrics = DisplayMetrics()
+ activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
+ displayMetrics.heightPixels
+ }
+ }
+
+
+ /**
+ * 得到指定月的天数
+ */
+ private fun getMonthLastDay(year: Int, month: Int): Int {
+ val a = Calendar.getInstance()
+ a[Calendar.YEAR] = year
+ a[Calendar.MONTH] = month - 1
+ a[Calendar.DATE] = 1 //把日期设置为当月第一天
+ a.roll(Calendar.DATE, -1) //日期回滚一天,也就是最后一天
+ return a[Calendar.DATE]
+ }
+
+}
\ No newline at end of file
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/DecelerateAnimator.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DecelerateAnimator.kt
new file mode 100644
index 0000000..94dc2e8
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/DecelerateAnimator.kt
@@ -0,0 +1,604 @@
+package com.gredicer.datetimepicker
+
+import android.animation.TypeEvaluator
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.hardware.SensorManager
+import android.view.ViewConfiguration
+import android.view.animation.LinearInterpolator
+
+
+/**
+ * 减速动画,默认启用回弹效果。
+ *
+ * @author Simon Lee
+ * @e-mail jmlixiaomeng@163.com
+ * @github https://github.com/Simon-Leeeeeeeee/SLWidget
+ * @createdTime 2018-07-23
+ */
+@SuppressLint("Recycle")
+class DecelerateAnimator @JvmOverloads constructor(
+ context: Context,
+ /**
+ * 弹性系数
+ */
+ private val mBounceCoeff: Float = 10f,
+ /**
+ * 是否启用回弹效果
+ */
+ private var isBouncing: Boolean = true
+) :
+ ValueAnimator() {
+ private val DECELERATION_RATE = 2.358201815f //Math.log(0.78) / Math.log(0.9)
+ private val INFLEXION = 0.35f // Tension lines cross at (INFLEXION, 1)
+
+ /**
+ * 动摩擦系数
+ */
+ private var mFlingFriction = 0f
+
+ /**
+ * 动摩擦系数倍率
+ */
+ private var mFlingFrictionRatio = 0.4f
+
+ /**
+ * 物理系数
+ */
+ private val mPhysicalCoeff: Float
+
+ /**
+ * 估值器
+ */
+ private val mDecelerateEvaluator: DecelerateEvaluator
+
+ /**
+ * 动画起始值
+ */
+ private var mInitialValue = 0f
+
+ /**
+ * 动画终止值
+ */
+ private var mFinalValue = 0f
+
+ /**
+ * 动画总持续时间
+ */
+ private var mDuration: Long = 0
+
+ /**
+ * 位移距离
+ */
+ private var mDistance = 0f
+
+ /**
+ * 回弹持续时间
+ */
+ private var mBounceDuration: Long = 0
+
+ /**
+ * 回弹位移距离
+ */
+ private var mBounceDistance = 0f
+
+ /**
+ * 未处理越界情况下的动画时间
+ */
+ private var mOriginalDuration: Long = 0
+
+ /**
+ * 未处理越界情况下的位移距离
+ */
+ private var mOriginalDistance = 0f
+
+ /**
+ * 摩擦系数,用于计算越界情况下的动画时间和位移
+ */
+ private var mFrictionCoeff = 0f
+
+ /**
+ * 是否越界(只有越界了才可能会发生回弹)
+ */
+ private var isOutside = false
+
+ constructor(context: Context, bouncing: Boolean) : this(context, 10f, bouncing) {}
+
+ /**
+ * 指定位移距离和最大动画时间,开始减速动画。
+ *
+ * @param startValue 起始值
+ * @param finalValue 终止值
+ * @param maxDuration 最大动画时间
+ */
+ fun startAnimator(startValue: Float, finalValue: Float, maxDuration: Long) {
+ reset()
+ mInitialValue = startValue
+ mDistance = finalValue - startValue
+ if (mDistance == 0f) {
+ return
+ }
+ mFinalValue = finalValue
+ mDuration = getDurationByDistance(mDistance)
+ if (mDuration > maxDuration) {
+ resetFlingFriction(mDistance, maxDuration)
+ mDuration = maxDuration
+ }
+ startAnimator()
+ }
+
+ /**
+ * 指定起止值和初始速度,开始减速动画
+ * 终点值一定是极小值或者极大值
+ *
+ * @param startValue 初始值
+ * @param minFinalValue 极小值
+ * @param maxFinalValue 极大值
+ * @param velocity 初速度
+ */
+ fun startAnimator(
+ startValue: Float,
+ minFinalValue: Float,
+ maxFinalValue: Float,
+ velocity: Float
+ ) {
+ if (minFinalValue >= maxFinalValue) {
+ throw ArithmeticException("maxFinalValue must be larger than minFinalValue!")
+ }
+ reset()
+ mInitialValue = startValue
+ // 1.根据速度计算位移距离
+ val distance = getDistanceByVelocity(velocity)
+ val finalValue = startValue + distance
+ // 2.确定终点值、位移距离、动画时间
+ if (finalValue < minFinalValue || finalValue > maxFinalValue) { //终点值在界外
+ //确定终点值
+ mFinalValue = if (finalValue < minFinalValue) minFinalValue else maxFinalValue
+ //起止值都在界外同侧
+ if (startValue < minFinalValue && finalValue < minFinalValue || startValue > maxFinalValue && finalValue > maxFinalValue) {
+ //改变动摩擦系数,减少动画时间
+ mFrictionCoeff = mBounceCoeff
+ //直接校正位移距离并计算动画时间
+ mDistance = mFinalValue - startValue
+ mDuration = getDurationByDistance(mDistance, mFrictionCoeff)
+ } else if (isBouncing) { //起止值跨越边界,且启用回弹效果
+ isOutside = true
+ //记录未处理越界情况下的位移距离和动画时间,用于计算回弹第一阶段的位移
+ mOriginalDistance = distance
+ mOriginalDuration = getDurationByDistance(distance)
+ //获取越界时的速度
+ val bounceVelocity = getVelocityByDistance(finalValue - mFinalValue)
+ //改变动摩擦系数,减少回弹时间
+ mFrictionCoeff = mBounceCoeff
+ //计算越界后的回弹时间
+ mBounceDuration = getDurationByVelocity(bounceVelocity, mFrictionCoeff)
+ //根据回弹时间计算回弹位移
+ mBounceDistance =
+ getDistanceByDuration(mBounceDuration / 2, mFrictionCoeff) * Math.signum(
+ bounceVelocity
+ )
+ //总的动画时间 = 原本动画时间 - 界外时间 + 回弹时间
+ mDuration =
+ mOriginalDuration - getDurationByDistance(finalValue - mFinalValue) + mBounceDuration
+ } else { //禁用回弹效果,按未越界处理。当越界达到边界值时会提前结束动画
+ isOutside = true
+ mDistance = distance
+ //计算动画时间
+ mDuration = getDurationByDistance(distance)
+ }
+ } else { //终点值在界内
+ //校正终点值,计算位移距离和动画时间
+ mFinalValue =
+ if (finalValue * 2 < minFinalValue + maxFinalValue) minFinalValue else maxFinalValue
+ mDistance = mFinalValue - startValue
+ mDuration = getDurationByDistance(mDistance)
+ }
+ startAnimator()
+ }
+
+ /**
+ * 指定初始速度,开始减速动画。
+ * 无边界
+ *
+ * @param startValue 起始位置
+ * @param velocity 初始速度
+ * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
+ */
+ fun startAnimator_Velocity(startValue: Float, velocity: Float, modulus: Float) {
+ startAnimator_Velocity(startValue, 0f, 0f, velocity, modulus)
+ }
+
+ /**
+ * 指定初始速度,开始减速动画。
+ * 当极大值大于极小值时有边界
+ *
+ * @param startValue 起始位置
+ * @param minValue 极小值
+ * @param maxValue 极大值
+ * @param velocity 初始速度
+ * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
+ */
+ fun startAnimator_Velocity(
+ startValue: Float,
+ minValue: Float,
+ maxValue: Float,
+ velocity: Float,
+ modulus: Float
+ ) {
+ reset()
+ mInitialValue = startValue
+ // 1.计算位移距离
+ var distance = getDistanceByVelocity(velocity)
+ // 2.校正位移距离
+ distance = reviseDistance(distance, startValue, modulus)
+ val finalValue = startValue + distance
+ // 3.确定终点值、位移距离、动画时间
+ if (maxValue > minValue && (finalValue < minValue || finalValue > maxValue)) { //终点值在界外
+ //确定终点值
+ mFinalValue = if (finalValue < minValue) minValue else maxValue
+ //起止值都在界外同侧
+ if (startValue < minValue && finalValue < minValue || startValue > maxValue && finalValue > maxValue) {
+ //改变动摩擦系数,减少动画时间
+ mFrictionCoeff = mBounceCoeff
+ //直接校正位移距离并计算动画时间
+ mDistance = mFinalValue - startValue
+ mDuration = getDurationByDistance(mDistance, mFrictionCoeff)
+ } else if (isBouncing) { //起止值跨越边界,且启用回弹效果
+ isOutside = true
+ //记录未处理越界情况下的位移距离和动画时间,用于计算回弹第一阶段的位移
+ mOriginalDistance = distance
+ mOriginalDuration = getDurationByDistance(distance)
+ //获取越界时的速度
+ val bounceVelocity = getVelocityByDistance(finalValue - mFinalValue)
+ //改变动摩擦系数,减少回弹时间
+ mFrictionCoeff = mBounceCoeff
+ //计算越界后的回弹时间
+ mBounceDuration = getDurationByVelocity(bounceVelocity, mFrictionCoeff)
+ //根据回弹时间计算回弹位移
+ mBounceDistance =
+ getDistanceByDuration(mBounceDuration / 2, mFrictionCoeff) * Math.signum(
+ bounceVelocity
+ )
+ //总的动画时间 = 原本动画时间 - 界外时间 + 回弹时间
+ mDuration =
+ mOriginalDuration - getDurationByDistance(finalValue - mFinalValue) + mBounceDuration
+ } else { //禁用回弹效果,按未越界处理。当越界达到边界值时会提前结束动画
+ isOutside = true
+ mDistance = distance
+ //计算动画时间
+ mDuration = getDurationByDistance(distance)
+ }
+ } else { //终点值在界内
+ //确定终点值、位移距离和动画时间
+ mFinalValue = finalValue
+ mDistance = distance
+ mDuration = getDurationByDistance(mDistance)
+ }
+ startAnimator()
+ }
+
+ /**
+ * 指定位移距离,开始减速动画。
+ * 无边界
+ *
+ * @param startValue 起始位置
+ * @param distance 位移距离
+ * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
+ */
+ fun startAnimator_Distance(startValue: Float, distance: Float, modulus: Float) {
+ startAnimator_Distance(startValue, 0f, 0f, distance, modulus)
+ }
+
+ /**
+ * 指定位移距离,开始减速动画。
+ * 当极大值大于极小值时有边界
+ *
+ * @param startValue 起始位置
+ * @param minValue 极小值
+ * @param maxValue 极大值
+ * @param distance 位移距离
+ * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
+ */
+ fun startAnimator_Distance(
+ startValue: Float,
+ minValue: Float,
+ maxValue: Float,
+ distance: Float,
+ modulus: Float
+ ) {
+ reset()
+ mInitialValue = startValue
+ // 1.先校正位移
+ mDistance = reviseDistance(distance, startValue, modulus)
+ if (mDistance == 0f) {
+ return
+ }
+ mFinalValue = startValue + mDistance
+ // 2.极值处理
+ if (maxValue > minValue && (mFinalValue < minValue || mFinalValue > maxValue)) {
+ return
+ }
+ // 3.计算时间
+ mDuration = getDurationByDistance(mDistance)
+ startAnimator()
+ }
+
+ private fun reset() {
+ isOutside = false
+ mFrictionCoeff = 1f
+ mBounceDuration = 0
+ mBounceDistance = 0f
+ mOriginalDuration = 0
+ mOriginalDistance = 0f
+ mFlingFriction = ViewConfiguration.getScrollFriction() * mFlingFrictionRatio
+ }
+
+ private fun startAnimator() {
+ // 1.设置起止值
+ setFloatValues(mInitialValue, mFinalValue)
+ // 2.设置估值器
+ setEvaluator(mDecelerateEvaluator)
+ // 3.设置持续时间
+ duration = mDuration
+ // 4.开始动画
+ start()
+ }
+
+ /**
+ * 校正位移,确保终点值是模的整数倍
+ *
+ * @param distance 位移距离
+ * @param startValue 起始位置
+ * @param modulus 终点值的模,会对滑动距离进行微调,以保证终点位置一定是modulus的整数倍
+ */
+ fun reviseDistance(distance: Float, startValue: Float, modulus: Float): Float {
+ if (modulus != 0f) {
+ val multiple = ((startValue + distance) / modulus).toInt()
+ val remainder = startValue + distance - multiple * modulus
+ if (remainder != 0f) {
+ return if (remainder * 2 < -modulus) {
+ distance - remainder - modulus
+ } else if (remainder * 2 < modulus) {
+ distance - remainder
+ } else {
+ distance - remainder + modulus
+ }
+ }
+ }
+ return distance
+ }
+
+ /**
+ * 根据位移计算初速度
+ *
+ * @param distance 位移距离
+ */
+ fun getVelocityByDistance(distance: Float): Float {
+ return getVelocityByDistance(distance, 1f)
+ }
+
+ /**
+ * 根据位移计算初速度
+ *
+ * @param distance 位移距离
+ * @param frictionCoeff 摩擦系数
+ */
+ fun getVelocityByDistance(distance: Float, frictionCoeff: Float): Float {
+ var velocity = 0f
+ if (distance != 0f) {
+ val decelMinusOne = DECELERATION_RATE - 1.0
+ val l = Math.pow(
+ (Math.abs(distance) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
+ decelMinusOne / DECELERATION_RATE
+ )
+ velocity =
+ (l * mFlingFriction * frictionCoeff * mPhysicalCoeff / INFLEXION * 4 * Math.signum(
+ distance
+ )).toFloat()
+ }
+ return velocity
+ }
+
+ /**
+ * 根据初速度计算位移
+ *
+ * @param velocity 初速度
+ */
+ fun getDistanceByVelocity(velocity: Float): Float {
+ return getDistanceByVelocity(velocity, 1f)
+ }
+
+ /**
+ * 根据初速度计算位移
+ *
+ * @param velocity 初速度
+ * @param frictionCoeff 摩擦系数
+ */
+ fun getDistanceByVelocity(velocity: Float, frictionCoeff: Float): Float {
+ var distance = 0f
+ if (velocity != 0f) {
+ val decelMinusOne = DECELERATION_RATE - 1.0
+ val l = Math.pow(
+ (INFLEXION * Math.abs(velocity / 4) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
+ DECELERATION_RATE / decelMinusOne
+ )
+ distance =
+ (l * mFlingFriction * frictionCoeff * mPhysicalCoeff * Math.signum(velocity)).toFloat()
+ }
+ return distance
+ }
+
+ /**
+ * 根据时间计算位移距离,无方向性
+ *
+ * @param duration 动画时间
+ */
+ fun getDistanceByDuration(duration: Long): Float {
+ return getDistanceByDuration(duration, 1f)
+ }
+
+ /**
+ * 根据时间计算位移距离,无方向性
+ *
+ * @param duration 动画时间
+ * @param frictionCoeff 摩擦系数
+ */
+ fun getDistanceByDuration(duration: Long, frictionCoeff: Float): Float {
+ var distance = 0f
+ if (duration > 0) {
+ val base = Math.pow((duration / 1000f).toDouble(), DECELERATION_RATE.toDouble())
+ distance = (base * mFlingFriction * frictionCoeff * mPhysicalCoeff).toFloat()
+ }
+ return distance
+ }
+
+ /**
+ * 根据初速度计算持续时间
+ *
+ * @param velocity 初速度
+ */
+ fun getDurationByVelocity(velocity: Float): Long {
+ return getDurationByVelocity(velocity, 1f)
+ }
+
+ /**
+ * 根据初速度计算持续时间
+ *
+ * @param velocity 初速度
+ * @param frictionCoeff 摩擦系数
+ */
+ fun getDurationByVelocity(velocity: Float, frictionCoeff: Float): Long {
+ var duration: Long = 0
+ if (velocity != 0f) {
+ val decelMinusOne = DECELERATION_RATE - 1.0
+ duration = (1000 * Math.pow(
+ (INFLEXION * Math.abs(velocity / 4) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble(),
+ 1 / decelMinusOne
+ )).toLong()
+ }
+ return duration
+ }
+
+ /**
+ * 根据位移距离计算持续时间
+ *
+ * @param distance 位移距离
+ */
+ fun getDurationByDistance(distance: Float): Long {
+ return getDurationByDistance(distance, 1f)
+ }
+
+ /**
+ * 根据位移距离计算持续时间
+ *
+ * @param distance 位移距离
+ * @param frictionCoeff 摩擦系数
+ */
+ fun getDurationByDistance(distance: Float, frictionCoeff: Float): Long {
+ var duration: Long = 0
+ if (distance != 0f) {
+ val base =
+ (Math.abs(distance) / (mFlingFriction * frictionCoeff * mPhysicalCoeff)).toDouble()
+ duration = (1000 * Math.pow(base, (1 / DECELERATION_RATE).toDouble())).toLong()
+ }
+ return duration
+ }
+
+ /**
+ * 根据位移距离和时间重置动摩擦系数
+ *
+ * @param distance 位移距离
+ */
+ private fun resetFlingFriction(distance: Float, duration: Long) {
+ val base = Math.pow((duration / 1000f).toDouble(), DECELERATION_RATE.toDouble())
+ mFlingFriction = Math.abs(distance / (base * mPhysicalCoeff)).toFloat()
+ }
+
+ /**
+ * 设置动摩擦系数倍率
+ */
+ fun setFlingFrictionRatio(ratio: Float) {
+ if (ratio > 0) {
+ mFlingFrictionRatio = ratio
+ }
+ }
+
+ private inner class DecelerateEvaluator :
+ TypeEvaluator {
+ override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
+ var fraction = fraction
+ return if (!isBouncing) { //禁用回弹效果(可能越界,需要提前结束动画)
+ val distance = getDistance(fraction, duration, mDistance, mFrictionCoeff)
+ if (isOutside && (distance - endValue + startValue) * mDistance > 0) { //越界了
+ if (fraction > 0 && fraction < 1) { //动画还将继续,提前结束
+ end()
+ }
+ return endValue
+ }
+ startValue + distance
+ } else if (isOutside) { //回弹效果触发(发生越界)
+ val bounceFraction = 1f * mBounceDuration / duration
+ if (fraction <= 1f - bounceFraction) { //第一阶段,按原本位移距离和动画时间进行计算
+ //校正进度值
+ fraction = fraction * duration / mOriginalDuration
+ val distance = getDistance(fraction, mOriginalDuration, mOriginalDistance, 1f)
+ startValue + distance
+ } else if (fraction <= 1f - bounceFraction / 2f) { //第二阶段,越过边界开始减速
+ //校正进度值
+ fraction = 2f * (fraction + bounceFraction - 1f) / bounceFraction
+ val distance =
+ getDistance(fraction, mBounceDuration / 2, mBounceDistance, mFrictionCoeff)
+ endValue + distance
+ } else { //第三阶段,加速回归边界
+ //校正进度值
+ fraction = 2f * (1f - fraction) / bounceFraction
+ val distance =
+ getDistance(fraction, mBounceDuration / 2, mBounceDistance, mFrictionCoeff)
+ endValue + distance
+ }
+ } else { //回弹效果未触发(未越界)
+ val distance = getDistance(fraction, duration, mDistance, mFrictionCoeff)
+ startValue + distance
+ }
+ }
+
+ /**
+ * 计算位移距离
+ *
+ * @param fraction 动画进度
+ * @param duration 动画时间
+ * @param distance 动画总距离
+ * @param frictionCoeff 摩擦系数
+ */
+ private fun getDistance(
+ fraction: Float,
+ duration: Long,
+ distance: Float,
+ frictionCoeff: Float
+ ): Float {
+ //获取剩余动画时间
+ val surplusDuration = ((1f - fraction) * duration).toLong()
+ //计算剩余位移距离
+ val surplusDistance =
+ getDistanceByDuration(surplusDuration, frictionCoeff) * Math.signum(distance)
+ //计算位移距离
+ return distance - surplusDistance
+ }
+ }
+
+ /**
+ * 减速动画
+ *
+ * @param context 上下文
+ * @param bounceCoeff 回弹系数
+ * @param bouncing 是否开启回弹效果
+ */
+ init {
+ isBouncing = isBouncing
+ mDecelerateEvaluator = DecelerateEvaluator()
+ mPhysicalCoeff = (context.resources.displayMetrics.density
+ * SensorManager.GRAVITY_EARTH * 5291.328f) // = 160.0f * 39.37f * 0.84f
+ interpolator = LinearInterpolator()
+ }
+}
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/OutsideClickDialog.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/OutsideClickDialog.kt
new file mode 100644
index 0000000..0ac3774
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/OutsideClickDialog.kt
@@ -0,0 +1,51 @@
+package com.gredicer.datetimepicker
+
+import android.app.Dialog
+import android.content.Context
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+
+/**
+ * 提供返回事件,外部点击事件
+ */
+class OutsideClickDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) {
+
+ private val mCancelable = true
+
+ var onBackClickListener: (() -> Boolean)? = null
+ var onOutsideClickListener: (() -> Boolean)? = null
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ val consume = onBackClickListener?.invoke()
+ if (consume == true) {
+ return true
+ }
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (mCancelable && isShowing &&
+ (event.action == MotionEvent.ACTION_UP && isOutOfBounds(context, event) ||
+ event.action == MotionEvent.ACTION_OUTSIDE)
+ ) {
+ val consume = onOutsideClickListener?.invoke()
+ if (consume == true) {
+ return true
+ }
+ }
+ return super.onTouchEvent(event)
+ }
+
+ private fun isOutOfBounds(context: Context, event: MotionEvent): Boolean {
+ val x = event.x.toInt()
+ val y = event.y.toInt()
+ val slop = ViewConfiguration.get(context).scaledWindowTouchSlop
+ val decorView = window?.decorView
+ return (x < -slop || y < -slop
+ || x > decorView?.width!! + slop
+ || y > decorView.height + slop)
+ }
+}
\ No newline at end of file
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/PickAdapter.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/PickAdapter.kt
new file mode 100644
index 0000000..a946e78
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/PickAdapter.kt
@@ -0,0 +1,19 @@
+package com.gredicer.datetimepicker
+
+/**
+ * @author Simon Lee
+ * @e-mail jmlixiaomeng@163.com
+ * @github https://github.com/Simon-Leeeeeeeee/SLWidget
+ * @createdTime 2018-05-17
+ */
+interface PickAdapter {
+ /**
+ * 返回数据总个数
+ */
+ val count: Int
+
+ /**
+ * 返回一条对应index的数据
+ */
+ fun getItem(position: Int): String?
+}
\ No newline at end of file
diff --git a/datetimepicker/src/main/java/com/gredicer/datetimepicker/ScrollPickerView.kt b/datetimepicker/src/main/java/com/gredicer/datetimepicker/ScrollPickerView.kt
new file mode 100644
index 0000000..9643bea
--- /dev/null
+++ b/datetimepicker/src/main/java/com/gredicer/datetimepicker/ScrollPickerView.kt
@@ -0,0 +1,829 @@
+package com.gredicer.datetimepicker
+
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.*
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.view.*
+import kotlin.math.abs
+
+
+/**
+ * @author Simon Lee
+ * @e-mail jmlixiaomeng@163.com
+ * @github https://github.com/Simon-Leeeeeeeee/SLWidget
+ * @createdTime 2018-05-11
+ */
+class ScrollPickerView : View, AnimatorUpdateListener {
+ /**
+ * dp&sp转px的系数
+ */
+ private var mDensityDP = 0f
+ private var mDensitySP = 0f
+
+ /**
+ * LayoutParams宽度
+ */
+ private var mLayoutWidth = 0
+
+ /**
+ * LayoutParams高度
+ */
+ private var mLayoutHeight = 0
+
+ /**
+ * 显示行数,仅高度为wrap_content时有效。默认值5
+ */
+ private var mTextRows = 0
+
+ /**
+ * 文本的行高
+ */
+ private var mRowHeight = 0f
+
+ /**
+ * 文本的行距。默认4dp
+ */
+ private var mRowSpacing = 0f
+
+ /**
+ * item的高度,等于mRowHeight+mRowSpacing
+ */
+ private var mItemHeight = 0f
+
+ /**
+ * 字体大小。默认16sp
+ */
+ private var mTextSize = 0f
+
+ /**
+ * 选中项的缩放比例。默认2
+ */
+ private var mTextRatio = 0f
+
+ /**
+ * 文本格式,当宽为wrap_content时用于计算宽度
+ */
+ private var mTextFormat: String? = null
+
+ /**
+ * 中部字体颜色
+ */
+ private var mTextColor_Center = 0
+
+ /**
+ * 外部字体颜色
+ */
+ private var mTextColor_Outside = 0
+
+ /**
+ * 是否开启循环
+ */
+ private var mLoopEnable = false
+
+ /**
+ * 中部item的position
+ */
+ private var mMiddleItemPostion = 0
+
+ /**
+ * 中部item的偏移量,取值范围( -mItenHeight/2F , mItenHeight/2F ]
+ */
+ private var mMiddleItemOffset = 0f
+
+ /**
+ * 绘制区域中点的Y坐标
+ */
+ private var mCenterY = 0f
+
+ /**
+ * 总的累计偏移量,指针上移,position增大,偏移量增加
+ */
+ private var mTotalOffset = 0f
+
+ /**
+ * 文本对齐方式
+ */
+ private var mGravity = 0
+
+ /**
+ * 文本绘制起始点的X坐标
+ */
+ private var mDrawingOriginX = 0f
+
+ /**
+ * 存储每行文本边界值,用于计算文本的高度
+ */
+ private var mTextBounds: Rect? = null
+
+ /**
+ * 记录触摸事件的Y坐标
+ */
+ private var mStartY = 0f
+
+ /**
+ * 触摸移动最小距离
+ */
+ private var mTouchSlop = 0
+
+ /**
+ * 触摸点的ID
+ */
+ private var mTouchPointerId = 0
+
+ /**
+ * 是否触摸移动(手指在屏幕上拖动)
+ */
+ private var isMoveAction = false
+
+ /**
+ * 是否切换了触摸点(多点触摸中的手指切换)
+ */
+ private var isSwitchTouchPointer = false
+
+ /**
+ * 用于记录指定的position
+ */
+ private var mSpecifyPosition: Int? = null
+ private var mMatrix: Matrix? = null
+
+ /**
+ * 减速动画
+ */
+ private var mDecelerateAnimator: DecelerateAnimator? = null
+
+ /**
+ * 线性颜色选择器
+ */
+ private var mLinearShader: LinearGradient? = null
+
+ /**
+ * 速度追踪器,结束触摸事件时计算手势速度,用于减速动画
+ */
+ private var mVelocityTracker: VelocityTracker? = null
+ private var mTextPaint: TextPaint? = null
+ private var mAdapter: PickAdapter? = null
+ private var mItemSelectedListener: OnItemSelectedListener? = null
+
+ interface OnItemSelectedListener {
+ /**
+ * 选中时的回调
+ */
+ fun onItemSelected(view: View?, position: Int)
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+ initView(context, attrs)
+ }
+
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ ) {
+ initView(context, attrs)
+ }
+
+ private fun initView(context: Context, attributeSet: AttributeSet?) {
+ mDensityDP = context.resources.displayMetrics.density //DP密度
+ mDensitySP = context.resources.displayMetrics.scaledDensity //SP密度
+ val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ScrollPickerView)
+ mTextRows = typedArray.getInteger(R.styleable.ScrollPickerView_scrollpicker_rows, 5)
+ mTextSize = typedArray.getDimension(
+ R.styleable.ScrollPickerView_scrollpicker_textSize,
+ 16 * mDensitySP
+ )
+ mTextRatio = typedArray.getFloat(R.styleable.ScrollPickerView_scrollpicker_textRatio, 2f)
+ mRowSpacing = typedArray.getDimension(R.styleable.ScrollPickerView_scrollpicker_spacing, 0f)
+ mTextFormat = typedArray.getString(R.styleable.ScrollPickerView_scrollpicker_textFormat)
+ mTextColor_Center = typedArray.getColor(
+ R.styleable.ScrollPickerView_scrollpicker_textColor_center,
+ -0x2277de
+ )
+ mTextColor_Outside = typedArray.getColor(
+ R.styleable.ScrollPickerView_scrollpicker_textColor_outside,
+ -0x2267
+ )
+ mLoopEnable = typedArray.getBoolean(R.styleable.ScrollPickerView_scrollpicker_loop, true)
+ mGravity =
+ typedArray.getInt(R.styleable.ScrollPickerView_scrollpicker_gravity, GRAVITY_LEFT)
+ mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
+ typedArray.recycle()
+
+ //初始化画笔工具
+ initTextPaint()
+ //计算行高
+ measureTextHeight()
+ mMatrix = Matrix() //用户记录偏移量并设置给颜色渐变工具
+ mTextBounds = Rect() //用于计算每行文本边界区域
+ //减速动画
+ mDecelerateAnimator = DecelerateAnimator(context)
+ mDecelerateAnimator!!.addUpdateListener(this)
+ }
+
+ /**
+ * 初始化画笔工具
+ */
+ private fun initTextPaint() {
+ mTextPaint = TextPaint()
+ //防抖动
+ mTextPaint!!.isDither = true
+ //抗锯齿
+ mTextPaint!!.isAntiAlias = true
+ //不要文本缓存
+ mTextPaint!!.isLinearText = true
+ //设置亚像素
+ mTextPaint!!.isSubpixelText = true
+ //字体加粗
+ mTextPaint!!.isFakeBoldText = true
+
+ //设置字体大小
+ mTextPaint!!.textSize = mTextSize
+ //等宽字体
+ mTextPaint!!.typeface = Typeface.MONOSPACE
+ when (mGravity) {
+ GRAVITY_LEFT -> {
+ mTextPaint!!.textAlign = Paint.Align.LEFT
+ }
+ GRAVITY_CENTER -> {
+ mTextPaint!!.textAlign = Paint.Align.CENTER
+ }
+ GRAVITY_RIGHT -> {
+ mTextPaint!!.textAlign = Paint.Align.RIGHT
+ }
+ }
+ }
+
+ /**
+ * 计算行高
+ */
+ private fun measureTextHeight() {
+ val fontMetrics = mTextPaint!!.fontMetrics
+ //确定行高
+ mRowHeight =
+ abs(fontMetrics.descent - fontMetrics.ascent) * if (mTextRatio > 1) mTextRatio else 1f
+ //行距不得小于负行高的一半
+ if (mRowSpacing < -mRowHeight / 2f) {
+ mRowSpacing = -mRowHeight / 2f
+ }
+ mItemHeight = mRowHeight + mRowSpacing
+ }
+
+ fun setOnItemSelectedListener(itemSelectedListener: OnItemSelectedListener?) {
+ mItemSelectedListener = itemSelectedListener
+ }
+
+ override fun setLayoutParams(params: ViewGroup.LayoutParams) {
+ mLayoutWidth = params.width
+ mLayoutHeight = params.height
+ super.setLayoutParams(params)
+ }
+
+ /**
+ * 计算PickerView的高宽,会多次调用,包括隐藏导航键也会调用
+ */
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
+ var widthSize = MeasureSpec.getSize(widthMeasureSpec)
+ val heightMode = MeasureSpec.getMode(heightMeasureSpec)
+ var heightSize = MeasureSpec.getSize(heightMeasureSpec)
+ if (mLayoutWidth == ViewGroup.LayoutParams.WRAP_CONTENT && widthMode != MeasureSpec.EXACTLY) { //宽为WRAP
+ widthSize = if (mTextFormat != null) {
+ Math.ceil((mTextPaint!!.measureText(mTextFormat) * if (mTextRatio > 1) mTextRatio else 1f).toDouble())
+ .toInt() + paddingLeft + paddingRight
+ } else {
+ paddingLeft + paddingRight
+ }
+ }
+ if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT && heightMode != MeasureSpec.EXACTLY) { //高为WRAP
+ heightSize =
+ Math.ceil((mRowHeight * mTextRows + mRowSpacing * (mTextRows - mTextRows % 2)).toDouble())
+ .toInt() + paddingTop + paddingBottom
+ }
+ setMeasuredDimension(
+ resolveSize(widthSize, widthMeasureSpec),
+ resolveSize(heightSize, heightMeasureSpec)
+ )
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ measureOriginal() //计算中心位置、绘制起点
+ setPaintShader() //设置颜色线性渐变
+ }
+
+ /**
+ * 计算中心位置、绘制起点
+ */
+ private fun measureOriginal() {
+ //计算绘制区域高度
+ val drawHeight = height - paddingTop - paddingBottom
+ //计算中心的Y值
+ mCenterY = drawHeight / 2f + paddingTop
+ when (mGravity) {
+ GRAVITY_LEFT -> {
+ mDrawingOriginX = paddingLeft.toFloat()
+ }
+ GRAVITY_CENTER -> {
+ mDrawingOriginX = (width + paddingLeft - paddingRight) / 2f
+ }
+ GRAVITY_RIGHT -> {
+ mDrawingOriginX = (width - paddingRight).toFloat()
+ }
+ }
+ }
+
+ /**
+ * 设置颜色线性渐变
+ */
+ private fun setPaintShader() {
+ mLinearShader = LinearGradient(
+ 0f,
+ mCenterY - (0.5f * mRowHeight + mItemHeight),
+ 0f,
+ mCenterY + (0.5f * mRowHeight + mItemHeight),
+ intArrayOf(mTextColor_Outside, mTextColor_Center, mTextColor_Outside),
+ floatArrayOf(0f, 0.5f, 1f),
+ Shader.TileMode.CLAMP
+ )
+ mTextPaint!!.shader = mLinearShader
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (mAdapter == null) {
+ return super.onTouchEvent(event)
+ }
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain()
+ }
+ mVelocityTracker!!.addMovement(event)
+ val actionIndex = event.actionIndex
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ isSwitchTouchPointer = false
+ //当前有减速动画未结束,则取消该动画,并直接进入滑动状态
+ if (mDecelerateAnimator!!.isStarted) {
+ isMoveAction = true
+ mDecelerateAnimator!!.cancel()
+ } else {
+ isMoveAction = false
+ }
+ //记录偏移坐标
+ mStartY = event.getY(actionIndex)
+ //记录当前控制指针ID
+ mTouchPointerId = event.getPointerId(actionIndex)
+ }
+ MotionEvent.ACTION_POINTER_UP -> {
+
+ //如果抬起的指针是当前控制指针,则进行切换
+ if (event.getPointerId(actionIndex) == mTouchPointerId) {
+ mVelocityTracker!!.clear()
+ //从列表中选择一个指针(非当前抬起的指针)作为下一个控制指针
+ var index = 0
+ while (index < event.pointerCount) {
+ if (index != actionIndex) {
+ //重置偏移坐标
+ mStartY = event.getY(index)
+ //重置触摸ID
+ mTouchPointerId = event.getPointerId(index)
+ //标记进行过手指切换
+ isSwitchTouchPointer = true
+ break
+ }
+ index++
+ }
+ }
+ }
+ MotionEvent.ACTION_MOVE -> {
+
+ //只响应当前控制指针的移动操作
+ var index = 0
+ while (index < event.pointerCount) {
+ if (event.getPointerId(index) == mTouchPointerId) {
+ //计算偏移量,指针上移偏移量为正
+ val offset = mStartY - event.getY(index)
+ if (isMoveAction) {
+ //已是滑动状态,累加偏移量,记录偏移坐标,请求重绘
+ mTotalOffset += offset
+ mStartY = event.getY(index)
+ super.invalidate()
+ } else if (Math.abs(offset) >= mTouchSlop) {
+ //进入滑动状态,重置偏移坐标,标记当前为滑动状态
+ mStartY = event.getY(index)
+ isMoveAction = true
+ }
+ break
+ }
+ index++
+ }
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+
+ //计算偏移量,指针上移偏移量为正
+ val offset = mStartY - event.getY(actionIndex)
+ if (isMoveAction) {
+ isMoveAction = false
+ //计算手势速度
+ mVelocityTracker!!.computeCurrentVelocity(1500)
+ val velocityY = -mVelocityTracker!!.getYVelocity(mTouchPointerId)
+ //累加偏移量
+ mTotalOffset += offset
+ //开启减速动画
+ startDecelerateAnimator(mTotalOffset, velocityY, 0f, mItemHeight)
+ } else if (!isSwitchTouchPointer && Math.abs(offset) < mTouchSlop) {
+ //计算触摸点相对于中心位置的偏移距离
+ val distance = event.getY(actionIndex) - mCenterY
+ //开启减速动画
+ startDecelerateAnimator(mTotalOffset, 0f, distance, mItemHeight)
+ }
+ if (mVelocityTracker != null) {
+ mVelocityTracker!!.recycle()
+ mVelocityTracker = null
+ }
+ }
+ }
+ return true
+ }
+
+ /**
+ * 开始减速动画
+ *
+ * @param startValue 初始位移值
+ * @param velocity 初始速度
+ * @param distance 移动距离
+ * @param modulus 距离的模
+ */
+ private fun startDecelerateAnimator(
+ startValue: Float,
+ velocity: Float,
+ distance: Float,
+ modulus: Float
+ ) {
+ val minValue = -1f
+ val maxValue: Float = if (mLoopEnable) -1f else (mAdapter!!.count - 1) * mItemHeight + 1
+ if (distance != 0f) {
+ mDecelerateAnimator!!.startAnimator_Distance(
+ startValue,
+ minValue,
+ maxValue,
+ distance,
+ modulus
+ )
+ } else {
+ mDecelerateAnimator!!.startAnimator_Velocity(
+ startValue,
+ minValue,
+ maxValue,
+ velocity,
+ modulus
+ )
+ }
+ }
+
+ override fun onAnimationUpdate(animation: ValueAnimator) {
+ mTotalOffset = animation.animatedValue as Float
+ super.invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (!isInEditMode && mAdapter == null) {
+ return
+ }
+ val measuredWidth = width
+ val measuredHeight = height
+ val paddingLeft = paddingLeft
+ val paddingRight = paddingRight
+ val paddingTop = paddingTop
+ val paddingBottom = paddingBottom
+
+ //根据padding限定绘制区域
+ canvas.clipRect(
+ paddingLeft,
+ paddingTop,
+ measuredWidth - paddingRight,
+ measuredHeight - paddingBottom
+ )
+
+ //计算中部item的position及偏移量
+ calculateMiddleItem()
+ //绘制上半部分的item
+ var curPosition = mMiddleItemPostion - 1
+ var curOffset = mCenterY + mMiddleItemOffset - mRowHeight / 2f - mItemHeight
+ while (curOffset > paddingTop - mRowHeight) {
+ //绘制文本
+ drawText(canvas, curPosition, curOffset)
+ curOffset -= mItemHeight
+ curPosition--
+ }
+
+ //绘制中部及下半部分的item
+ curPosition = mMiddleItemPostion
+ curOffset = mCenterY + mMiddleItemOffset - mRowHeight / 2f
+ while (curOffset < measuredHeight - paddingBottom) {
+ //绘制文本
+ drawText(canvas, curPosition, curOffset)
+ //下一个
+ curOffset += mItemHeight
+ curPosition++
+ }
+ //动画结束,进行选中回调
+ if (!isMoveAction && !mDecelerateAnimator!!.isStarted && mItemSelectedListener != null) {
+ //回调监听
+ mItemSelectedListener!!.onItemSelected(this, mMiddleItemPostion)
+ }
+ }
+
+ /**
+ * 根据总偏移量计算中部item的偏移量及position
+ * 偏移量的取值范围为(-mItenHeight/2F , mItenHeight/2F]
+ */
+ private fun calculateMiddleItem() {
+ //计算偏移了多少个完整item
+ var count =
+ if (mSpecifyPosition != null) mSpecifyPosition!! else (mTotalOffset / mItemHeight).toInt()
+ if (mSpecifyPosition != null) {
+ if (mDecelerateAnimator!!.isStarted) {
+ mDecelerateAnimator!!.cancel()
+ }
+ mTotalOffset = mSpecifyPosition!! * mItemHeight
+ mMiddleItemOffset = 0f
+ mSpecifyPosition = null
+ } else {
+ //对偏移量取余,注意这里不用取余运算符,因为可能造成严重错误!
+ val offsetRem = mTotalOffset - mItemHeight * count //取值范围( -mItenHeight , mItenHeight )
+ mMiddleItemOffset = if (offsetRem >= mItemHeight / 2f) {
+ count++
+ mItemHeight - offsetRem
+ } else if (offsetRem >= -mItemHeight / 2f) {
+ -offsetRem
+ } else {
+ count--
+ -mItemHeight - offsetRem
+ }
+ }
+ //对position取模
+ mMiddleItemPostion = getRealPosition(count)
+ //如果停止触摸且动画结束,对最终值和偏移量进行校正
+ if (!isMoveAction && !mDecelerateAnimator!!.isStarted) {
+ if (mMiddleItemPostion < 0 || mAdapter == null || mAdapter!!.count < 1) {
+ mMiddleItemPostion = 0
+ } else if (mMiddleItemPostion >= mAdapter!!.count) {
+ mMiddleItemPostion = mAdapter!!.count - 1
+ }
+ mTotalOffset = mMiddleItemPostion * mItemHeight
+ }
+ }
+
+ /**
+ * 绘制文本
+ */
+ private fun drawText(canvas: Canvas, position: Int, offsetY: Float) {
+ //对position取模
+ var position = position
+ position = getRealPosition(position)
+ //position未越界
+ if (isInEditMode || position >= 0 && position < mAdapter!!.count) {
+ //获取文本
+ val text = getDrawingText(position)
+ if (text != null) {
+ canvas.save()
+ //平移画布
+ canvas.translate(0f, offsetY)
+ //操作线性颜色渐变
+ mMatrix!!.setTranslate(0f, -offsetY)
+ mLinearShader!!.setLocalMatrix(mMatrix)
+ //计算缩放比例
+ val scaling = getScaling(offsetY)
+ canvas.scale(scaling, scaling, mDrawingOriginX, mRowHeight / 2f)
+ //获取文本尺寸
+ mTextPaint!!.getTextBounds(text, 0, text.length, mTextBounds)
+ //根据文本尺寸计算基线位置
+ val baseLineY = (mRowHeight - mTextBounds!!.top - mTextBounds!!.bottom) / 2f
+ //绘制文本
+ canvas.drawText(text, mDrawingOriginX, baseLineY, mTextPaint!!)
+ canvas.restore()
+ }
+ }
+ }
+
+ /**
+ * 循环模式下对position取模
+ */
+ private fun getRealPosition(position: Int): Int {
+ var position = position
+ if (mLoopEnable && mAdapter != null && mAdapter!!.count > 0) {
+ position %= mAdapter!!.count
+ if (position < 0) {
+ position += mAdapter!!.count
+ }
+ }
+ return position
+ }
+
+ /**
+ * 根据获取要绘制的文本内容
+ */
+ private fun getDrawingText(position: Int): String? {
+ if (isInEditMode) {
+ return if (mTextFormat != null) mTextFormat else ("item$position").toString()
+ }
+ return if (position >= 0 && position < mAdapter!!.count) {
+ mAdapter!!.getItem(position)
+ } else null
+ }
+
+ /**
+ * 根据偏移量计算缩放比例
+ */
+ private fun getScaling(offsetY: Float): Float {
+ val abs = Math.abs(offsetY + mRowHeight / 2f - mCenterY)
+ return if (abs < mItemHeight) {
+ (1 - abs / mItemHeight) * (mTextRatio - 1f) + 1f
+ } else {
+ 1f
+ }
+ }
+
+ /**
+ * 设置适配器
+ */
+ fun setAdapter(adapter: PickAdapter?) {
+ mAdapter = adapter
+ super.invalidate()
+ }
+
+ /**
+ * 设置当前选中项
+ */
+ fun setSelectedPosition(position: Int) {
+ if (mAdapter == null) return
+ if (position < 0 || position >= mAdapter!!.count) {
+ throw ArrayIndexOutOfBoundsException()
+ }
+ if (mDecelerateAnimator!!.isStarted) {
+ mDecelerateAnimator!!.cancel()
+ }
+ // 如果在onMeasure之前设置选中项,mItemHeight为0,无法得到正确偏移量,因此这里不能直接计算mTotalOffset
+ mSpecifyPosition = position
+ super.invalidate()
+ }
+
+ /**
+ * 获取当前选中项
+ */
+ fun getSelectedPosition(): Int {
+ return if (isMoveAction || mAdapter == null || mDecelerateAnimator!!.isStarted) {
+ -1
+ } else mMiddleItemPostion
+ }
+
+
+ /**
+ * 设置文本对齐方式,计算文本绘制起始点的X坐标
+ */
+ fun setGravity(gravity: Int) {
+ when (gravity) {
+ GRAVITY_LEFT -> {
+ mTextPaint!!.textAlign = Paint.Align.LEFT
+ mDrawingOriginX = paddingLeft.toFloat()
+ }
+ GRAVITY_CENTER -> {
+ mTextPaint!!.textAlign = Paint.Align.CENTER
+ mDrawingOriginX = (width + paddingLeft - paddingRight) / 2f
+ }
+ GRAVITY_RIGHT -> {
+ mTextPaint!!.textAlign = Paint.Align.RIGHT
+ mDrawingOriginX = (width - paddingRight).toFloat()
+ }
+ else -> return
+ }
+ mGravity = gravity
+ super.invalidate()
+ }
+
+ fun isLoopEnable(): Boolean {
+ return mLoopEnable
+ }
+
+ fun setLoopEnable(enable: Boolean) {
+ if (mLoopEnable != enable) {
+ mLoopEnable = enable
+ //循环将关闭且正在减速动画
+ if (!mLoopEnable && mDecelerateAnimator!!.isStarted && mAdapter != null) {
+ //停止减速动画,并指定position以确保item对齐
+ mDecelerateAnimator!!.cancel()
+ //防止position越界
+ mSpecifyPosition =
+ if (mMiddleItemPostion < 0) 0 else if (mMiddleItemPostion >= mAdapter!!.count) mAdapter!!.count - 1 else mMiddleItemPostion
+ }
+ super.invalidate()
+ }
+ }
+
+ /**
+ * 设置文本显示的行数,仅当高为WRAP_CONTENT时有效
+ */
+ fun setTextRows(rows: Int) {
+ if (mTextRows != rows) {
+ mTextRows = rows
+ if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ super.requestLayout()
+ }
+ }
+ }
+
+ /**
+ * 设置文本字体大小,单位px
+ *
+ * @param textSize 必须大于0
+ */
+ fun setTextSize(textSize: Float) {
+ if (textSize > 0 && mTextSize != textSize) {
+ mTextSize = textSize
+ mTextPaint!!.textSize = mTextSize
+ measureTextHeight()
+ reInvalidate()
+ }
+ }
+
+ /**
+ * 设置文本行间距,单位px
+ */
+ fun setRowSpacing(rowSpacing: Float) {
+ if (mRowSpacing != rowSpacing) {
+ mRowSpacing = rowSpacing
+ measureTextHeight()
+ reInvalidate()
+ }
+ }
+
+ /**
+ * 设置放大倍数
+ */
+ fun setTextRatio(textRatio: Float) {
+ if (mTextRatio != textRatio) {
+ mTextRatio = textRatio
+ measureTextHeight()
+ reInvalidate()
+ }
+ }
+
+ /**
+ * 设置中部字体颜色
+ */
+ fun setCenterTextColor(color: Int) {
+ if (mTextColor_Center != color) {
+ mTextColor_Center = color
+ setPaintShader() //设置颜色线性渐变
+ invalidate()
+ }
+ }
+
+ /**
+ * 设置外部字体颜色
+ */
+ fun setOutsideTextColor(color: Int) {
+ if (mTextColor_Outside != color) {
+ mTextColor_Outside = color
+ setPaintShader() //设置颜色线性渐变
+ invalidate()
+ }
+ }
+
+ private fun reInvalidate() {
+ if (mDecelerateAnimator!!.isStarted) {
+ mDecelerateAnimator!!.cancel()
+ }
+ mSpecifyPosition = mMiddleItemPostion
+ if (mLayoutHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ super.requestLayout()
+ } else {
+ super.invalidate()
+ }
+ }
+
+ override fun canScrollVertically(direction: Int): Boolean {
+ return true
+ }
+
+ companion object {
+ /**
+ * 文本对齐方式,居左
+ */
+ const val GRAVITY_LEFT = 3
+
+ /**
+ * 文本对齐方式,居右
+ */
+ const val GRAVITY_RIGHT = 5
+
+ /**
+ * 文本对齐方式,居中
+ */
+ const val GRAVITY_CENTER = 17
+ }
+}
diff --git a/datetimepicker/src/main/res/drawable/selector_bg_4_round_corner.xml b/datetimepicker/src/main/res/drawable/selector_bg_4_round_corner.xml
new file mode 100644
index 0000000..ed9a163
--- /dev/null
+++ b/datetimepicker/src/main/res/drawable/selector_bg_4_round_corner.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/drawable/shape_button_corners.xml b/datetimepicker/src/main/res/drawable/shape_button_corners.xml
new file mode 100644
index 0000000..1928d69
--- /dev/null
+++ b/datetimepicker/src/main/res/drawable/shape_button_corners.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/drawable/shape_button_corners_disable.xml b/datetimepicker/src/main/res/drawable/shape_button_corners_disable.xml
new file mode 100644
index 0000000..3bec69c
--- /dev/null
+++ b/datetimepicker/src/main/res/drawable/shape_button_corners_disable.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/drawable/shape_dialog_corners.xml b/datetimepicker/src/main/res/drawable/shape_dialog_corners.xml
new file mode 100644
index 0000000..b70e2e7
--- /dev/null
+++ b/datetimepicker/src/main/res/drawable/shape_dialog_corners.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/layout/fragment_datetime_picker.xml b/datetimepicker/src/main/res/layout/fragment_datetime_picker.xml
new file mode 100644
index 0000000..f2010f9
--- /dev/null
+++ b/datetimepicker/src/main/res/layout/fragment_datetime_picker.xml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/values/attrs.xml b/datetimepicker/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..873a5f2
--- /dev/null
+++ b/datetimepicker/src/main/res/values/attrs.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/values/strings.xml b/datetimepicker/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d64d9a4
--- /dev/null
+++ b/datetimepicker/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+ 测试
+ show_datetime
+ 请选择时间
+ 年
+ 月
+ 日
+ 时
+ 分
+ 确定
+ 重置为现在
+
\ No newline at end of file
diff --git a/datetimepicker/src/main/res/values/themes.xml b/datetimepicker/src/main/res/values/themes.xml
new file mode 100644
index 0000000..41101f8
--- /dev/null
+++ b/datetimepicker/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/datetimepicker/src/test/java/com/gredicer/datetimepicker/ExampleUnitTest.kt b/datetimepicker/src/test/java/com/gredicer/datetimepicker/ExampleUnitTest.kt
new file mode 100644
index 0000000..7e56f44
--- /dev/null
+++ b/datetimepicker/src/test/java/com/gredicer/datetimepicker/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.gredicer.datetimepicker
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index d7635cb..93e8d78 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -26,3 +26,4 @@ dependencyResolutionManagement {
}
rootProject.name = "NavinfoVolvo"
include ':app'
+include ":datetimepicker"