fix: 修改日期选择器按钮样式

This commit is contained in:
xiaoyan 2023-01-09 17:21:23 +08:00
parent cdc5e8a95f
commit 2eddd942d2
25 changed files with 2473 additions and 4 deletions

1
.idea/gradle.xml generated
View File

@ -12,6 +12,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/datetimepicker" />
</set>
</option>
</GradleProjectSettings>

View File

@ -102,8 +102,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'

View File

@ -12,6 +12,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
@ -90,7 +91,8 @@ class ObtainMessageFragment: Fragment() {
_binding = FragmentObtainMessageBinding.inflate(inflater, container, false)
val root: View = binding.root
obtainMessageViewModel.setCurrentMessage(GreetingMessage())
val greetingMessage = GreetingMessage()
obtainMessageViewModel.setCurrentMessage(greetingMessage)
obtainMessageViewModel?.getMessageLiveData()?.observe(
viewLifecycleOwner, Observer {
@ -164,6 +166,21 @@ class ObtainMessageFragment: Fragment() {
return root
}
override fun onResume() {
super.onResume()
if (obtainMessageViewModel.getMessageLiveData().value!=null&&"已发送".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
}
}
fun initView() {
// 设置问候信息提示的红色星号
binding.tiLayoutTitle.markRequiredInRed()
@ -207,7 +224,6 @@ class ObtainMessageFragment: Fragment() {
obtainMessageViewModel.updateMessageSendTime(DateUtils.date2Str(sendDate, dateSendFormat))
}
}
}
dialog.show(parentFragmentManager, "SelectSendTime")
}

1
datetimepicker/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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'
}

View File

21
datetimepicker/proguard-rules.pro vendored Normal file
View File

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

View File

@ -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)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.gredicer.datetimepicker">
</manifest>

View File

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

View File

@ -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]
}
}

View File

@ -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<Float> {
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()
}
}

View File

@ -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)
}
}

View File

@ -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?
}

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/shape_button_corners_disable" android:state_pressed="true"></item>
<item android:drawable="@drawable/shape_button_corners_disable" android:state_selected="true"></item>
<item android:drawable="@drawable/shape_button_corners_disable" android:state_checked="true"></item>
<item android:drawable="@drawable/shape_button_corners_disable" android:state_enabled="false"></item>
<item android:drawable="@drawable/shape_button_corners" ></item>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#eeeeee"/>
<corners android:radius="100dp"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#888888"/>
<corners android:radius="100dp"/>
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#fff"/>
<corners
android:topLeftRadius="15dp"
android:topRightRadius="15dp" />
</shape>

View File

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="50dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/datetime_dialog_title"
android:textColor="#000"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_time_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/datetime_dialog_show"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/fl_datetimepicker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:justifyContent="space_around"
app:layout_constraintTop_toBottomOf="@+id/tv_time_show">
<LinearLayout
android:id="@+id/year_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone">
<com.gredicer.datetimepicker.ScrollPickerView
android:id="@+id/date_picker_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrollpicker_gravity="center"
app:scrollpicker_rows="3"
app:scrollpicker_spacing="20dp"
app:scrollpicker_textColor_center="#555555"
app:scrollpicker_textColor_outside="#fff"
app:scrollpicker_textFormat="0000"
app:scrollpicker_textRatio="1.4"
app:scrollpicker_textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:text="@string/year"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/month_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone">
<com.gredicer.datetimepicker.ScrollPickerView
android:id="@+id/date_picker_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrollpicker_gravity="center"
app:scrollpicker_rows="3"
app:scrollpicker_spacing="20dp"
app:scrollpicker_textColor_center="#555555"
app:scrollpicker_textColor_outside="#fff"
app:scrollpicker_textFormat="00"
app:scrollpicker_textRatio="1.4"
app:scrollpicker_textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:text="@string/month"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/day_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone">
<com.gredicer.datetimepicker.ScrollPickerView
android:id="@+id/date_picker_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrollpicker_gravity="center"
app:scrollpicker_rows="3"
app:scrollpicker_spacing="20dp"
app:scrollpicker_textColor_center="#555555"
app:scrollpicker_textColor_outside="#fff"
app:scrollpicker_textFormat="00"
app:scrollpicker_textRatio="1.4"
app:scrollpicker_textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:text="@string/day"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/hour_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone">
<com.gredicer.datetimepicker.ScrollPickerView
android:id="@+id/date_picker_hour"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrollpicker_gravity="center"
app:scrollpicker_rows="3"
app:scrollpicker_spacing="20dp"
app:scrollpicker_textColor_center="#555555"
app:scrollpicker_textColor_outside="#fff"
app:scrollpicker_textFormat="00"
app:scrollpicker_textRatio="1.4"
app:scrollpicker_textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:text="@string/hour"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/minute_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone">
<com.gredicer.datetimepicker.ScrollPickerView
android:id="@+id/date_picker_minute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrollpicker_gravity="center"
app:scrollpicker_rows="3"
app:scrollpicker_spacing="20dp"
app:scrollpicker_textColor_center="#555555"
app:scrollpicker_textColor_outside="#fff"
app:scrollpicker_textFormat="00"
app:scrollpicker_textRatio="1.4"
app:scrollpicker_textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:text="@string/minute"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_back_now"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_marginTop="50dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:gravity="center"
android:text="@string/back_now"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/btn_enter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fl_datetimepicker" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_enter"
android:layout_width="150dp"
android:layout_height="50dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:gravity="center"
android:text="@string/enter"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_back_now"
app:layout_constraintTop_toTopOf="@id/btn_back_now" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ScrollPickerView">
<!--行数-->
<attr name="scrollpicker_rows" />
<!--行间距-->
<attr name="scrollpicker_spacing" />
<!--字体大小-->
<attr name="scrollpicker_textSize" />
<!--字体格式宽度为wrap_content时用于计算宽度-->
<attr name="scrollpicker_textFormat" />
<!--字体放大倍数-->
<attr name="scrollpicker_textRatio" />
<!--选中时字体颜色-->
<attr name="scrollpicker_textColor_center" />
<!--未选中时字体颜色-->
<attr name="scrollpicker_textColor_outside" />
<!--文本对齐方式(左、中、右)-->
<attr name="scrollpicker_gravity" />
<!--是否开启循环-->
<attr name="scrollpicker_loop" />
</declare-styleable>
<!--行数-->
<attr name="scrollpicker_rows" format="integer" />
<!--行间距-->
<attr name="scrollpicker_spacing" format="reference|dimension" />
<!--字体大小-->
<attr name="scrollpicker_textSize" format="reference|dimension" />
<!--字体格式宽度为wrap_content时用于计算宽度-->
<attr name="scrollpicker_textFormat" format="reference|string" />
<!--字体放大倍数-->
<attr name="scrollpicker_textRatio" format="float" />
<!--选中时字体颜色-->
<attr name="scrollpicker_textColor_center" format="reference|color" />
<!--未选中时字体颜色-->
<attr name="scrollpicker_textColor_outside" format="reference|color" />
<!--文本对齐方式(左、中、右)-->
<attr name="scrollpicker_gravity">
<enum name="center" value="17" />
<enum name="left" value="3" />
<enum name="right" value="5" />
</attr>
<!--是否开启循环-->
<attr name="scrollpicker_loop" format="boolean" />
</resources>

View File

@ -0,0 +1,12 @@
<resources>
<string name="app_name">测试</string>
<string name="datetime_dialog_show">show_datetime</string>
<string name="datetime_dialog_title">请选择时间</string>
<string name="year"></string>
<string name="month"></string>
<string name="day"></string>
<string name="hour"></string>
<string name="minute"></string>
<string name="enter">确定</string>
<string name="back_now">重置为现在</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme" parent="Theme.MaterialComponents.Light" />
</resources>

View File

@ -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)
}
}

View File

@ -26,3 +26,4 @@ dependencyResolutionManagement {
}
rootProject.name = "NavinfoVolvo"
include ':app'
include ":datetimepicker"