Merge branch 'master' of gitlab.navinfo.com:vivo/navinfovivo
Conflicts: app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt
This commit is contained in:
commit
1d48ff0cce
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -12,6 +12,7 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/datetimepicker" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
|
@ -21,7 +21,7 @@ android {
|
|||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 32
|
targetSdk 32
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@ -104,8 +104,9 @@ dependencies {
|
|||||||
|
|
||||||
// 文件选择器 https://github.com/rosuH/AndroidFilePicker/blob/master/README_CN.md
|
// 文件选择器 https://github.com/rosuH/AndroidFilePicker/blob/master/README_CN.md
|
||||||
implementation 'me.rosuh:AndroidFilePicker:0.8.2'
|
implementation 'me.rosuh:AndroidFilePicker:0.8.2'
|
||||||
// 时间选择器 https://github.com/Gredicer/datetimepicker
|
// // 时间选择器 https://github.com/Gredicer/datetimepicker
|
||||||
implementation 'com.github.Gredicer:datetimepicker:V1.0.0'
|
// implementation 'com.github.Gredicer:datetimepicker:V1.0.0'
|
||||||
|
implementation project(path: ':datetimepicker')
|
||||||
|
|
||||||
//带侧滑的自定义列表
|
//带侧滑的自定义列表
|
||||||
implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
|
implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
|
||||||
|
Binary file not shown.
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"artifactType": {
|
|
||||||
"type": "APK",
|
|
||||||
"kind": "Directory"
|
|
||||||
},
|
|
||||||
"applicationId": "com.navinfo.volvo",
|
|
||||||
"variantName": "release",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "SINGLE",
|
|
||||||
"filters": [],
|
|
||||||
"attributes": [],
|
|
||||||
"versionCode": 1,
|
|
||||||
"versionName": "1.0",
|
|
||||||
"outputFile": "app-release.apk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"elementType": "File"
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.AdapterView.OnItemSelectedListener
|
import android.widget.AdapterView.OnItemSelectedListener
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
@ -35,6 +36,7 @@ import com.gredicer.datetimepicker.DateTimePickerFragment
|
|||||||
import com.hjq.permissions.OnPermissionCallback
|
import com.hjq.permissions.OnPermissionCallback
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import com.navinfo.volvo.Constant
|
||||||
import com.navinfo.volvo.R
|
import com.navinfo.volvo.R
|
||||||
import com.navinfo.volvo.RecorderLifecycleObserver
|
import com.navinfo.volvo.RecorderLifecycleObserver
|
||||||
import com.navinfo.volvo.database.entity.Attachment
|
import com.navinfo.volvo.database.entity.Attachment
|
||||||
@ -65,7 +67,7 @@ class ObtainMessageFragment : Fragment() {
|
|||||||
private var _binding: FragmentObtainMessageBinding? = null
|
private var _binding: FragmentObtainMessageBinding? = null
|
||||||
private val obtainMessageViewModel by viewModels<ObtainMessageViewModel>()
|
private val obtainMessageViewModel by viewModels<ObtainMessageViewModel>()
|
||||||
private val photoHelper by lazy {
|
private val photoHelper by lazy {
|
||||||
EasyMediaFile().setCrop(true)
|
EasyMediaFile().setCrop(false)
|
||||||
}
|
}
|
||||||
private val recorderLifecycleObserver by lazy {
|
private val recorderLifecycleObserver by lazy {
|
||||||
RecorderLifecycleObserver()
|
RecorderLifecycleObserver()
|
||||||
@ -179,6 +181,25 @@ class ObtainMessageFragment : Fragment() {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (obtainMessageViewModel.getMessageLiveData().value!=null&&Constant.message_status_send_over.equals((obtainMessageViewModel.getMessageLiveData().value as GreetingMessage).status)) {
|
||||||
|
binding.tvMessageTitle.isEnabled=false
|
||||||
|
binding.btnStartPhoto.isEnabled=false
|
||||||
|
binding.btnStartCamera.isEnabled=false
|
||||||
|
binding.btnStartRecord.isEnabled=false
|
||||||
|
binding.btnSelectSound.isEnabled=false
|
||||||
|
binding.edtSendFrom.isEnabled=false
|
||||||
|
binding.edtSendTo.isEnabled=false
|
||||||
|
binding.btnSendTime.isEnabled=false
|
||||||
|
binding.btnObtainMessageConfirm.isEnabled=false
|
||||||
|
binding.tvPhotoName.isEnabled = false
|
||||||
|
binding.tvAudioName.isEnabled = false
|
||||||
|
binding.imgPhotoDelete.isEnabled = false
|
||||||
|
binding.imgAudioDelete.isEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun initView() {
|
fun initView() {
|
||||||
// 设置问候信息提示的红色星号
|
// 设置问候信息提示的红色星号
|
||||||
binding.tiLayoutTitle.markRequiredInRed()
|
binding.tiLayoutTitle.markRequiredInRed()
|
||||||
@ -236,7 +257,6 @@ class ObtainMessageFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
dialog.show(parentFragmentManager, "SelectSendTime")
|
dialog.show(parentFragmentManager, "SelectSendTime")
|
||||||
}
|
}
|
||||||
@ -256,7 +276,7 @@ class ObtainMessageFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 开始启动拍照界面
|
// 开始启动拍照界面
|
||||||
photoHelper.setCrop(true).takePhoto(requireActivity())
|
photoHelper.setCrop(false).takePhoto(requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
|
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
|
||||||
@ -274,7 +294,7 @@ class ObtainMessageFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnStartPhoto.setOnClickListener {
|
binding.btnStartPhoto.setOnClickListener {
|
||||||
photoHelper.setCrop(true).selectPhoto(requireActivity())
|
photoHelper.setCrop(false).selectPhoto(requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户选择录音文件
|
// 用户选择录音文件
|
||||||
|
@ -392,7 +392,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="确认提交"></com.google.android.material.button.MaterialButton>
|
android:text="修改保存"></com.google.android.material.button.MaterialButton>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
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"
|
rootProject.name = "NavinfoVolvo"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
include ":datetimepicker"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user