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 @@ + + + +