fix: 修改日期选择器按钮样式
This commit is contained in:
parent
cdc5e8a95f
commit
2eddd942d2
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -12,6 +12,7 @@
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/datetimepicker" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
@ -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'
|
||||
|
@ -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
1
datetimepicker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
50
datetimepicker/build.gradle
Normal file
50
datetimepicker/build.gradle
Normal 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'
|
||||
|
||||
}
|
0
datetimepicker/consumer-rules.pro
Normal file
0
datetimepicker/consumer-rules.pro
Normal file
21
datetimepicker/proguard-rules.pro
vendored
Normal file
21
datetimepicker/proguard-rules.pro
vendored
Normal 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
|
@ -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)
|
||||
}
|
||||
}
|
4
datetimepicker/src/main/AndroidManifest.xml
Normal file
4
datetimepicker/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.gredicer.datetimepicker">
|
||||
|
||||
</manifest>
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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?
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
221
datetimepicker/src/main/res/layout/fragment_datetime_picker.xml
Normal file
221
datetimepicker/src/main/res/layout/fragment_datetime_picker.xml
Normal 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>
|
51
datetimepicker/src/main/res/values/attrs.xml
Normal file
51
datetimepicker/src/main/res/values/attrs.xml
Normal 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>
|
12
datetimepicker/src/main/res/values/strings.xml
Normal file
12
datetimepicker/src/main/res/values/strings.xml
Normal 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>
|
5
datetimepicker/src/main/res/values/themes.xml
Normal file
5
datetimepicker/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme" parent="Theme.MaterialComponents.Light" />
|
||||
</resources>
|
@ -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)
|
||||
}
|
||||
}
|
@ -26,3 +26,4 @@ dependencyResolutionManagement {
|
||||
}
|
||||
rootProject.name = "NavinfoVolvo"
|
||||
include ':app'
|
||||
include ":datetimepicker"
|
||||
|
Loading…
x
Reference in New Issue
Block a user