自定义时间范围筛选

This commit is contained in:
litingting 2025-10-13 18:08:46 +08:00
parent d610ef5d53
commit 592918d6ce
52 changed files with 4597 additions and 110 deletions

View File

@ -54,4 +54,5 @@ dependencies {
kapt (libs.compiler)
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation ("com.google.android.material:material:1.13.0")
implementation(project(":pickerview"))
}

View File

@ -1,6 +1,7 @@
package com.ux.video.file.filerecovery
import android.app.Application
import org.jaaksi.pickerview.widget.BasePickerView
class App: Application() {
@ -10,5 +11,8 @@ class App: Application() {
override fun onCreate() {
super.onCreate()
mAppContext = this
BasePickerView.sDefaultItemSize = 40
// BasePickerView.sDefaultDrawIndicator = false
}
}

View File

@ -21,10 +21,12 @@ import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.BaseActivity
import com.ux.video.file.filerecovery.databinding.ActivityMainBinding
import com.ux.video.file.filerecovery.main.ScanSelectTypeActivity
import com.ux.video.file.filerecovery.photo.DateFilterPopupWindows
import com.ux.video.file.filerecovery.photo.DatePickerDialogFragment
import com.ux.video.file.filerecovery.utils.ScanManager
class MainActivity : BaseActivity<ActivityMainBinding>() {
private var dialogCustomerDateStart:DatePickerDialogFragment? = null
private var dialogPermission: PermissionDialogFragment? = null
//是否正确引导用户打开所有文件管理权限
private var isRequestPermission = false
@ -94,9 +96,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
intentCheck()
}
}
binding.btnPermission.setOnClickListener {
binding.tvTitle.setOnClickListener {
showDatePicker()
}
binding.btnScanAllPhoto.setOnClickListener {
@ -242,4 +244,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
super.onDestroy()
binding.layoutPermission.isVisible = false
}
fun showDatePicker() {
// dialogCustomerDateStart = dialogCustomerDateStart?: DateFilterPopupWindows(this){}
// dialogCustomerDateStart?.show(supportFragmentManager,"")
}
}

View File

@ -0,0 +1,171 @@
package com.ux.video.file.filerecovery.photo
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.databinding.PopwindowsFilterDateBinding
import com.ux.video.file.filerecovery.utils.Common
import com.ux.video.file.filerecovery.utils.Common.setItemSelect
import com.ux.video.file.filerecovery.utils.CustomTextView
import java.util.Date
/**
* 日期筛选弹窗
*/
class DateFilterPopupWindows(
context: Context,
var selectedPos: Int,
var onClickConfirm: (value: String, showDateDialog: Boolean) -> Unit,
var onResetDate: (isStart: Boolean,currentDisplayDate: Date?) -> Unit,
var onClickDismiss: () -> Unit
) :
PopupWindow(context) {
private val viewBinding: PopwindowsFilterDateBinding =
PopwindowsFilterDateBinding.inflate(LayoutInflater.from(context))
private lateinit var startDate: Date
private lateinit var endDate: Date
init {
setContentView(viewBinding.root)
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
isFocusable = true
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
viewBinding.viewMask.setOnClickListener {
dismiss()
}
viewBinding.run {
tvStartDate.setOnClickListener { onResetDate(true, Common.getDateFromFormat(tvStartDate.text.toString())) }
tvEndDate.setOnClickListener { onResetDate(false,Common.getDateFromFormat(tvEndDate.text.toString())) }
}
setData(context.resources.getStringArray(R.array.filter_date), selectedPos)
Common.showLog("--------------FilterPopupWindows init")
}
fun updateSelectPos(index: Int, start: Date, end: Date) {
selectedPos = index
setPosSelect(selectedPos)
if (index == 4) {
startDate = start
endDate = end
viewBinding.run {
layoutStartEnd.isVisible = true
tvStartDate.text = Common.getChineseFormatDate(start)
tvEndDate.text = Common.getChineseFormatDate(end)
Common.setItemSelect(layoutStartEnd, true)
}
}
}
fun updateStartEndDate(start: Date? = null, end: Date? = null) {
start?.let { viewBinding.tvStartDate.text = Common.getChineseFormatDate(it) }
end?.let { viewBinding.tvEndDate.text = Common.getChineseFormatDate(it) }
}
private fun setData(
list: Array<String>,
selectedPos: Int,
) {
list.forEachIndexed { index, item ->
viewBinding.run {
when (index) {
0 -> tvAll.text = item
1 -> tv1.text = item
2 -> tv6.text = item
3 -> tv24.text = item
4 -> tvCustomize.text = item
}
}
}
setMutualExclusion()
setPosSelect(selectedPos)
}
private fun setMutualExclusion() {
viewBinding.run {
for (i in 0 until parentLayout.childCount) {
val child = parentLayout.getChildAt(i)
if (child is RelativeLayout) {
child.setOnClickListener {
//回调选中的文字
for (j in 0 until child.childCount) {
val other = child.getChildAt(j)
if (other is CustomTextView) {
other.text.toString().let {
if (it == "Customize" && layoutStartEnd.isVisible == false) {
//自定义日期
onClickConfirm.invoke(it, true)
} else {
setPosSelect(i)
dismiss()
onClickConfirm.invoke(it, false)
}
}
}
}
}
}
}
}
}
private fun setPosSelect(selected: Int) {
viewBinding.run {
for (i in 0 until parentLayout.childCount) {
val child = parentLayout.getChildAt(i)
if (child is RelativeLayout) {
if (i == selected) {
setItemSelect(child, true)
} else {
setItemSelect(child, false)
}
}
}
}
}
fun show(anchor: View, xOff: Int = 0, yOff: Int = 0) {
showAsDropDown(anchor, xOff, yOff)
viewBinding.viewMask.apply {
alpha = 0f
animate().alpha(1f).setDuration(200).start()
visibility = View.VISIBLE
}
}
override fun dismiss() {
Log.d("------------", "--------------dismiss")
viewBinding.viewMask.animate().alpha(0f).setDuration(200)
.withEndAction {
super.dismiss()
onClickDismiss.invoke()
}.start()
}
}

View File

@ -1,26 +1,41 @@
package com.ux.video.file.filerecovery.photo
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.graphics.toColorInt
import androidx.fragment.app.DialogFragment
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.databinding.CommonLayoutSortItemBinding
import com.ux.video.file.filerecovery.databinding.DialogSortBinding
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointForward
import java.text.SimpleDateFormat
import java.util.*
import com.ux.video.file.filerecovery.databinding.DialogFilterCustomerDateBinding
import com.ux.video.file.filerecovery.utils.Common
import com.ux.video.file.filerecovery.utils.CustomTextView
import org.jaaksi.pickerview.picker.TimePicker
import org.jaaksi.pickerview.picker.TimePicker.Companion.TYPE_DATE
import org.jaaksi.pickerview.picker.TimePicker.OnTimeSelectListener
import org.jaaksi.pickerview.widget.DefaultCenterDecoration
import java.util.Date
class DatePickerDialogFragment(val onClickSort: (type: Int) -> Unit) : DialogFragment() {
private lateinit var binding: DialogSortBinding
class DatePickerDialogFragment(
var mContext: Context,
var title: String,
var onPickerChooseListener: (selectedDate: Date)-> Unit,
var onClickCancel:()-> Unit
) :
DialogFragment(), OnTimeSelectListener {
private lateinit var binding: DialogFilterCustomerDateBinding
private lateinit var timerPicker: TimePicker
private var defaultSelectedDate = System.currentTimeMillis()
private var defaultStartDate = 1540361760000L
private var defaultEndDate = System.currentTimeMillis()
override fun onStart() {
super.onStart()
dialog?.window?.apply {
@ -28,7 +43,7 @@ class DatePickerDialogFragment(val onClickSort: (type: Int) -> Unit) : DialogFra
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setGravity(Gravity.BOTTOM)
setGravity(Gravity.CENTER)
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
}
}
@ -37,10 +52,114 @@ class DatePickerDialogFragment(val onClickSort: (type: Int) -> Unit) : DialogFra
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DialogSortBinding.inflate(inflater)
binding = DialogFilterCustomerDateBinding.inflate(inflater)
initDatePickerView()
binding.run {
textTitle.text =title
tvCancel.setOnClickListener {
if (!timerPicker.canSelected()) return@setOnClickListener
dismiss()
onClickCancel.invoke()
}
tvOk.setOnClickListener {
if (!timerPicker.canSelected()) return@setOnClickListener
dismiss()
timerPicker.onConfirm()
}
}
return binding.root
}
fun setCurrentSelectedDate(dateTimer: Date){
defaultSelectedDate = dateTimer.time
}
fun setRangeDate(start: Date){
defaultStartDate = start.time
// defaultEndDate = end
}
private fun initDatePickerView() {
var index = 0
var centerDrawable: Drawable? = null
timerPicker = TimePicker.Builder(requireContext(), TYPE_DATE, this)
.setRangDate(defaultStartDate, defaultEndDate)
// .setContainsStarDate(true)
//.setTimeMinuteOffset(10)
// 设置选中时间
.setSelectedDate(defaultSelectedDate)
.setInterceptor { pickerView, params ->
pickerView.run {
visibleItemCount = 5
setTextSize(15, 19)
setFont(CustomTextView.bold)
setColor(
Common.getColorInt(requireContext(), R.color.main_title),
Common.getColorInt(requireContext(), R.color.main_sub_title)
)
centerDrawable = when (index) {
0 -> ContextCompat.getDrawable(
mContext,
R.drawable.bg_rectangle_15787880_left_13
)
1 -> ContextCompat.getDrawable(
mContext,
R.drawable.bg_rectangle_15787880_center
)
else -> ContextCompat.getDrawable(
mContext,
R.drawable.bg_rectangle_15787880_right_13
)
}
index = index + 1
val centerDecoration =
DefaultCenterDecoration(requireContext())
.setLineColor("#00000000".toColorInt())
//.setMargin(margin, -margin, margin, -margin)
.setLineWidth(0f)
.setDrawable(centerDrawable)
setCenterDecoration(centerDecoration)
}
}
.setFormatter(object : TimePicker.DefaultFormatter() {
// 自定义Formatter显示去年今年明年
@SuppressLint("DefaultLocale")
override fun format(
picker: TimePicker,
type: Int,
position: Int,
value: Long
): CharSequence {
if (type == TimePicker.TYPE_YEAR) {
return String.format("%d", value)
} else if (type == TimePicker.TYPE_MONTH) {
return String.format("%d", value)
} else if (type == TimePicker.TYPE_DAY) {
return String.format("%d", value)
}
return super.format(picker, type, position, value)
}
}).create()
timerPicker.view().let {
(it.parent as? ViewGroup)?.removeView(it)
binding.pickerContainer.addView(it)
}
}
override fun onTimeSelect(
picker: TimePicker,
date: Date
) {
defaultSelectedDate = date.time
onPickerChooseListener(date)
Common.showLog("--------onTimeSelect=${Common.getChineseFormatDate(date)}")
}
}

View File

@ -12,12 +12,13 @@ import android.widget.RelativeLayout
import androidx.core.view.forEach
import com.ux.video.file.filerecovery.databinding.CommonLayoutFilterItemBinding
import com.ux.video.file.filerecovery.databinding.PopwindowsFilterBinding
import com.ux.video.file.filerecovery.utils.Common
import com.ux.video.file.filerecovery.utils.Common.setItemSelect
import com.ux.video.file.filerecovery.utils.CustomTextView
class FilterPopupWindows(
context: Context, list: Array<String>,
selectedPos: Int,
var selectedPos: Int,
onClickConfirm: (value: String) -> Unit,
var onClickDismiss: () -> Unit
) :
@ -46,6 +47,12 @@ class FilterPopupWindows(
}
setData(list, selectedPos, onClickConfirm)
Common.showLog("--------------FilterPopupWindows init")
}
fun updateSelectPos(index: Int) {
selectedPos = index
setPosSelect(selectedPos)
}
private fun setData(
@ -83,21 +90,18 @@ class FilterPopupWindows(
val child = parentLayout.getChildAt(i)
if (child is RelativeLayout) {
child.setOnClickListener {
for (j in 0 until parentLayout.childCount) {
val other = parentLayout.getChildAt(j)
if (other is RelativeLayout) {
setItemSelect(other, false)
}
}
setItemSelect(child, true)
//回调选中的文字
for (j in 0 until child.childCount) {
val other = child.getChildAt(j)
if (other is CustomTextView) {
other.text.toString().let {
setPosSelect(i)
dismiss()
}
onClickConfirm.invoke(other.text.toString())
}
}
dismiss()
}
}
}
@ -105,6 +109,21 @@ class FilterPopupWindows(
}
private fun setPosSelect(selected: Int) {
viewBinding.run {
for (i in 0 until parentLayout.childCount) {
val child = parentLayout.getChildAt(i)
if (child is RelativeLayout) {
if (i == selected) {
setItemSelect(child, true)
} else {
setItemSelect(child, false)
}
}
}
}
}
fun show(anchor: View, xOff: Int = 0, yOff: Int = 0) {
showAsDropDown(anchor, xOff, yOff)

View File

@ -1,18 +1,13 @@
package com.ux.video.file.filerecovery.photo
import android.content.Intent
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.base.BaseActivity
import com.ux.video.file.filerecovery.databinding.ActivityPhotoSortingBinding
@ -22,20 +17,18 @@ import com.ux.video.file.filerecovery.utils.Common.setItemSelect
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySize
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySizeList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonths
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonthsList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRange
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRangeList
//import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonths
//import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonthsList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
import com.ux.video.file.filerecovery.utils.ExtendFunctions.mbToBytes
import com.ux.video.file.filerecovery.utils.ExtendFunctions.resetItemDecorationOnce
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
import com.ux.video.file.filerecovery.utils.ScanManager
import com.ux.video.file.filerecovery.utils.ScanManager.copySelectedFilesAsync
import com.ux.video.file.filerecovery.utils.ScanManager.deleteFilesAsync
import com.ux.video.file.filerecovery.utils.ScanRepository
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
@ -47,6 +40,7 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
val FILTER_DATE_1 = 1
val FILTER_DATE_6 = 6
val FILTER_DATE_24 = 24
val FILTER_DATE_CUSTOMER = 0
val FILTER_SIZE_ALL = -1
val FILTER_SIZE_1 = 1
@ -70,6 +64,9 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
private var dialogDeleting: DeletingDialogFragment? = null
private var dialogCustomerDateStart: DatePickerDialogFragment? = null
private var dialogCustomerDateEnd: DatePickerDialogFragment? = null
//默认倒序排序
private var sortReverse = true
@ -80,7 +77,10 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
//筛选大小,默认全部-1
private var filterSize = FILTER_SIZE_ALL
private var filterDatePopupWindows: FilterPopupWindows? = null
private var filterDatePopupWindows: DateFilterPopupWindows? = null
private var filterStartDate: Date? = null
private var filterEndDate: Date? = null
private var filterSizePopupWindows: FilterPopupWindows? = null
private var filterLayoutPopupWindows: FilterPopupWindows? = null
@ -282,9 +282,7 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
val bPx = 6.dpToPx(context)
setPadding(aPx, 0, bPx, 0)
clipToPadding = false
layoutManager = GridLayoutManager(context, columns)
// resetItemDecorationOnce(mItemDecoration)
adapter = sizeSortAdapter
}
@ -299,20 +297,52 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
filterDateLayout.setOnClickListener {
setItemSelect(it as LinearLayout, true)
resources.getStringArray(R.array.filter_date).let { data ->
filterDatePopupWindows = filterDatePopupWindows ?: FilterPopupWindows(
filterDatePopupWindows = filterDatePopupWindows ?: DateFilterPopupWindows(
this@PhotoSortingActivity,
data,
0,
{ clickValue ->
{ clickValue, showDialog ->
when (clickValue) {
data[0] -> filterDate = FILTER_DATE_ALL
data[1] -> filterDate = FILTER_DATE_1
data[2] -> filterDate = FILTER_DATE_6
data[3] -> filterDate = FILTER_DATE_24
data[4] -> showDatePicker()
}
data[0] -> {
filterDate = FILTER_DATE_ALL
startFilter()
}
data[1] -> {
filterDate = FILTER_DATE_1
startFilter()
}
data[2] -> {
filterDate = FILTER_DATE_6
startFilter()
}
data[3] -> {
filterDate = FILTER_DATE_24
startFilter()
}
data[4] -> {
filterDate = FILTER_DATE_CUSTOMER
if (showDialog)
showStartDateDialog(true,null)
else {
startFilter()
}
}
}
}, onResetDate = { isStart,currentDate ->
if (isStart) {
showStartDateDialog(false,currentDate)
} else {
showEndDateDialog(false,currentDate)
}
}) {
//dismiss
setItemSelect(it, false)
}
@ -408,13 +438,18 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
* 执行筛选结果
*/
private fun startFilter() {
Common.showLog("--------------开始筛选")
when (binding.recyclerView.adapter) {
//当前是时间排序
is PhotoDisplayDateAdapter -> {
//确定当前排序
val list = if (sortReverse) sortByDateReverse else sortedByDatePositive
val filterSizeCovert = filterSizeCovert(filterSize)
list.filterWithinMonths(filterDate)
list.filterWithinDateRange(
filterDate,
startDate = if (filterDate == FILTER_DATE_CUSTOMER) filterStartDate else null,
endDate = if (filterDate == FILTER_DATE_CUSTOMER) filterEndDate else null
)
.filterBySize(filterSizeCovert.first, filterSizeCovert.second)
.let { currentList ->
//对筛选后的数据与实际选中集合对比 ,得出筛选后显示的数据中的选中数据
@ -434,7 +469,11 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
is PhotoDisplayDateChildAdapter -> {
val list = if (sortReverse) sortBySizeBigToSmall else sortBySizeSmallToBig
val filterSizeCovert = filterSizeCovert(filterSize)
list.filterWithinMonthsList(filterDate)
list.filterWithinDateRangeList(
filterDate,
startDate = if (filterDate == FILTER_DATE_CUSTOMER) filterStartDate else null,
endDate = if (filterDate == FILTER_DATE_CUSTOMER) filterEndDate else null
)
.filterBySizeList(filterSizeCovert.first, filterSizeCovert.second)
.let { currentList ->
//对筛选后的数据与实际选中集合对比 ,得出筛选后显示的数据中的选中数据
@ -461,33 +500,75 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
}
fun showDatePicker() {
// 创建日期选择器构建器
val builder = MaterialDatePicker.Builder.datePicker()
builder.setTitleText("选择日期")
private fun showStartDateDialog(isNeedSetSelected: Boolean,currentDate: Date?) {
// 可选:限制可选日期,比如只能选择今天之后的日期
val constraintsBuilder = CalendarConstraints.Builder()
constraintsBuilder.setValidator(DateValidatorPointForward.now()) // 今天之后
builder.setCalendarConstraints(constraintsBuilder.build())
dialogCustomerDateStart = dialogCustomerDateStart ?: DatePickerDialogFragment(
this,
getString(R.string.start_date),
onPickerChooseListener = {}, onClickCancel = {})
dialogCustomerDateStart?.run {
currentDate?.let {
setCurrentSelectedDate(it)
}
onPickerChooseListener = { selectedDate ->
filterStartDate = selectedDate
// filterDatePopupWindows?.updateStartEndDate(start = selectedDate)
Log.d("showStartDateDialog", "isFirst=${isNeedSetSelected}--------")
showEndDateDialog(isNeedSetSelected,null)
val datePicker = builder.build()
// if (isFirst) {
// showEndDateDialog(true)
// } else {
// if (filterDate == FILTER_DATE_CUSTOMER) {
// startFilter()
// filterDatePopupWindows?.dismiss()
// }
// }
}
onClickCancel = {
// 显示日期选择器
datePicker.show(supportFragmentManager, "MATERIAL_DATE_PICKER")
}
show(supportFragmentManager, "")
// 监听用户选择
datePicker.addOnPositiveButtonClickListener { selection ->
// selection 是 Long 类型的时间戳UTC 毫秒)
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateString = sdf.format(Date(selection))
println("用户选择的日期:$dateString")
}
}
private fun showEndDateDialog(isNeedSetSelected: Boolean,currentDate: Date?) {
dialogCustomerDateEnd = dialogCustomerDateEnd ?: DatePickerDialogFragment(
this,
getString(R.string.end_date),
onPickerChooseListener = {}, onClickCancel = {})
dialogCustomerDateEnd?.run {
currentDate?.let {
setCurrentSelectedDate(it)
}
filterStartDate?.let { setRangeDate(it) }
onPickerChooseListener = { selectedDate ->
filterEndDate = selectedDate
if(isNeedSetSelected){
filterDatePopupWindows?.updateSelectPos(4, filterStartDate!!, filterEndDate!!)
startFilter()
filterDatePopupWindows?.dismiss()
}else{
filterDatePopupWindows?.updateStartEndDate(filterStartDate,filterEndDate)
if (filterDate == FILTER_DATE_CUSTOMER) {
startFilter()
filterDatePopupWindows?.dismiss()
}
}
}
onClickCancel = {
// filterDatePopupWindows?.dismiss()
}
show(supportFragmentManager, "")
}
}
private fun showRecoveringDialog() {
dialogRecovering = dialogRecovering ?: RecoveringDialogFragment(filterSelectedSetList.size){
dialogRecovering =
dialogRecovering ?: RecoveringDialogFragment(filterSelectedSetList.size) {
complete()
}
dialogRecovering?.show(supportFragmentManager, "")
@ -500,6 +581,9 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
dialogDeleting?.show(supportFragmentManager, "")
}
/**
* 删除或者恢复完成
*/
private fun complete() {
dialogDeleting?.dismiss()
dialogRecovering?.dismiss()

View File

@ -5,7 +5,9 @@ import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import android.os.Environment
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.ux.video.file.filerecovery.App
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
@ -32,6 +34,7 @@ object Common {
val rootDir = Environment.getExternalStorageDirectory()
val dateFormat = SimpleDateFormat("MMMM d,yyyy", Locale.ENGLISH)
val chineseFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINESE)
val recoveryPhotoDir = "MyAllRecovery/Photo"
/**
@ -127,14 +130,21 @@ object Common {
/**
* 设置全部子View的选中
*/
fun setItemSelect(view: ViewGroup, boolean: Boolean) {
// fun setItemSelect(view: ViewGroup, boolean: Boolean) {
// for (i in 0 until view.childCount) {
// val child = view.getChildAt(i)
// child.isSelected = boolean
// }
// }
fun setItemSelect(view: View, selected: Boolean) {
view.isSelected = selected
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
child.isSelected = boolean
setItemSelect(child, selected)
}
}
}
/**
@ -161,7 +171,10 @@ object Common {
}
fun checkSelectListContainSize(list: List<ResultPhotosFiles>, selected: Set<String>): Pair<Int, MutableSet<String>> {
fun checkSelectListContainSize(
list: List<ResultPhotosFiles>,
selected: Set<String>
): Pair<Int, MutableSet<String>> {
val currentSelected = mutableSetOf<String>()
val totalSelectedCount = list.count {
val isSelected = it.path in selected
@ -176,6 +189,19 @@ object Common {
return dateFormat.format(Date(time))
}
fun getChineseFormatDate(date: Date): String {
return chineseFormat.format(date)
}
fun getDateFromFormat(dateStr: String): Date? {
return chineseFormat.parse(dateStr)
}
fun getColorInt(context: Context, colorId: Int): Int {
return ContextCompat.getColor(context, colorId)
}
fun showLog(msg: String) {
Log.d("============", msg)
}

View File

@ -12,8 +12,8 @@ class CustomTextView @JvmOverloads constructor(
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
companion object {
private var regular: Typeface? = null
private var bold: Typeface? = null
private var regular: Typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
var bold: Typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
private var alimama: Typeface? = null
}
@ -27,25 +27,20 @@ class CustomTextView @JvmOverloads constructor(
selectedText = typedArray.getString(R.styleable.CustomTextView_selected_text)
normalText = typedArray.getString(R.styleable.CustomTextView_normal_text)
typedArray.recycle()
Typeface.create("sans-serif-light", Typeface.NORMAL) // Roboto Light
Typeface.create("sans-serif-thin", Typeface.ITALIC) // Roboto Thin Italic
Typeface.create("sans-serif-medium", Typeface.BOLD) // Roboto Medium Bold
Typeface.create("sans-serif-condensed", Typeface.NORMAL) // Roboto Condensed
if (alimama == null) {
alimama = Typeface.createFromAsset(context.assets, "fonts/alimama.ttf")
}
when (type) {
0 -> {
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
typeface = regular
}
1 -> {
typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
typeface = bold
}
2 -> {
if (alimama == null) {
alimama = Typeface.createFromAsset(context.assets, "fonts/alimama.ttf")
}
typeface = alimama
}

View File

@ -8,6 +8,7 @@ import android.os.Parcelable
import android.util.TypedValue
import androidx.recyclerview.widget.RecyclerView
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import java.util.Date
object ExtendFunctions {
@ -40,17 +41,57 @@ object ExtendFunctions {
/**
* 按时间筛选最近 N 个月
*/
fun List<ResultPhotosFiles>.filterWithinMonthsList(months: Int): List<ResultPhotosFiles> {
if (months == -1) return this
// fun List<ResultPhotosFiles>.filterWithinMonthsList(months: Int): List<ResultPhotosFiles> {
// if (months == -1) return this
// val today = Calendar.getInstance()
// val monthsAgo = Calendar.getInstance().apply {
// add(Calendar.MONTH, -months)
// }
// return this.filter {
// val cal = Calendar.getInstance().apply { timeInMillis = it.lastModified }
// !cal.before(monthsAgo) && !cal.after(today)
// }
// }
fun List<ResultPhotosFiles>.filterWithinDateRangeList(
months: Int = -1,
startDate: Date? = null,
endDate: Date? = null
): List<ResultPhotosFiles> {
val today = Calendar.getInstance()
return when {
// ✅ 1. -1 表示不过滤,返回全部
months == -1 -> this
// ✅ 2. 0 表示仅根据 startDate / endDate 筛选
months == 0 -> this.filter { file ->
val date = Date(file.lastModified)
when {
startDate != null && endDate != null -> date in startDate..endDate
// startDate != null -> !date.before(startDate)
// endDate != null -> !date.after(endDate)
else -> true // 没传日期则默认不过滤
}
}
// ✅ 3. 其他情况:按“最近 N 个月”筛选
else -> {
val monthsAgo = Calendar.getInstance().apply {
add(Calendar.MONTH, -months)
}
return this.filter {
val cal = Calendar.getInstance().apply { timeInMillis = it.lastModified }
this.filter { file ->
val cal = Calendar.getInstance().apply { timeInMillis = file.lastModified }
!cal.before(monthsAgo) && !cal.after(today)
}
}
}
}
/**
* 按文件大小筛选区间 [minSize, maxSize]
@ -66,19 +107,61 @@ object ExtendFunctions {
/**
* 按时间筛选最近 N 个月
*/
fun List<Pair<String, List<ResultPhotosFiles>>>.filterWithinMonths(months: Int): List<Pair<String, List<ResultPhotosFiles>>> {
if (months == -1) return this
// fun List<Pair<String, List<ResultPhotosFiles>>>.filterWithinMonths(months: Int): List<Pair<String, List<ResultPhotosFiles>>> {
// if (months == -1) return this
// val sdf = Common.dateFormat
// val today = Calendar.getInstance()
// val monthsAgo = Calendar.getInstance().apply {
// add(Calendar.MONTH, -months)
// }
// return this.filter { (dayStr, _) ->
// val day = sdf.parse(dayStr)
// day != null && !day.before(monthsAgo.time) && !day.after(today.time)
// }
// }
fun List<Pair<String, List<ResultPhotosFiles>>>.filterWithinDateRange(
months: Int = -1,
startDate: Date? = null,
endDate: Date? = null
): List<Pair<String, List<ResultPhotosFiles>>> {
val sdf = Common.dateFormat
val today = Calendar.getInstance()
// 计算“几个月前”的时间
val monthsAgo = Calendar.getInstance().apply {
add(Calendar.MONTH, -months)
}
return this.filter { (dayStr, _) ->
val day = sdf.parse(dayStr)
day != null && !day.before(monthsAgo.time) && !day.after(today.time)
return when {
// -1 表示不过滤,返回全部
months == -1 -> this
// 0 表示只用日期范围过滤
months == 0 -> this.filter { (dayStr, _) ->
val day = sdf.parse(dayStr) ?: return@filter false
when {
startDate != null && endDate != null -> day in startDate..endDate
// startDate != null -> !day.before(startDate)
// endDate != null -> !day.after(endDate)
else -> true // 没传 start/end 则默认不过滤
}
}
// 其他情况:按“最近 N 个月”过滤
else -> this.filter { (dayStr, _) ->
val day = sdf.parse(dayStr) ?: return@filter false
!day.before(monthsAgo.time) && !day.after(today.time)
}
}
}
/**
* 分组数据按大小筛选
*/

View File

@ -41,7 +41,7 @@ class ScanRepository : ViewModel() {
_selectedLiveData.value = current.toSet()
_selectedDisplayLiveData.value = currentDisplay.toSet()
Log.d("CallTrace", Log.getStackTraceString(Exception("Call trace")))
// Log.d("CallTrace", Log.getStackTraceString(Exception("Call trace")))
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/date_dialog_center_bg" />
</shape>

View File

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

View File

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

View File

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

View File

@ -247,6 +247,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_wchat">

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@drawable/bg_rectangle_white_20"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingVertical="30dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_date"
android:id="@+id/text_title"
android:textColor="@color/main_title"
android:textSize="20sp"
app:fontType="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Year"
android:textColor="@color/main_title"
android:textSize="15sp"
app:fontType="bold" />
<com.ux.video.file.filerecovery.utils.CustomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Month"
android:textColor="@color/main_title"
android:textSize="15sp"
app:fontType="bold" />
<com.ux.video.file.filerecovery.utils.CustomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Day"
android:textColor="@color/main_title"
android:textSize="15sp"
app:fontType="bold" />
</LinearLayout>
<FrameLayout
android:id="@+id/pickerContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_height="150dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginTop="26dp"
android:orientation="horizontal">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_cancel"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_weight="1"
android:background="@drawable/bg_dialog_btn_cancel_stoke_8"
android:gravity="center"
android:text="@string/cancel"
android:textColor="@color/main_text_blue"
android:textSize="14sp"
app:fontType="bold" />
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_ok"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:background="@drawable/bg_dialog_btn_allow_solid_8"
android:gravity="center"
android:text="@string/ok"
android:textColor="@color/white"
android:textSize="14sp"
app:fontType="bold" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view_mask"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
android:background="@color/popup_windows_mask"
android:visibility="gone" />
<LinearLayout
android:id="@+id/parent_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_rectangle_white_bottom_20"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/layout_all"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingHorizontal="16dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<ImageView
android:layout_width="16dp"
android:layout_height="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/selector_filter_image_view" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_1_month"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingHorizontal="16dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<ImageView
android:layout_width="16dp"
android:layout_height="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/selector_filter_image_view" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_6_month"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingHorizontal="16dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<ImageView
android:layout_width="16dp"
android:layout_height="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/selector_filter_image_view" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_24_month"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingHorizontal="16dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_24"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<ImageView
android:layout_width="16dp"
android:layout_height="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/selector_filter_image_view" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_customize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_customize"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:gravity="center"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<ImageView
android:layout_width="16dp"
android:layout_height="12dp"
android:layout_alignTop="@id/tv_customize"
android:layout_alignBottom="@id/tv_customize"
android:layout_alignParentEnd="true"
android:src="@drawable/selector_filter_image_view"
tools:src="@drawable/icon_checkmark_filter" />
<LinearLayout
android:id="@+id/layout_start_end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_below="@id/tv_customize"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="14dp">
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_start_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_filter_date"
android:gravity="center"
android:paddingHorizontal="10dp"
android:paddingVertical="6dp"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
<View
android:layout_width="12dp"
android:layout_height="1dp"
android:layout_marginHorizontal="11dp"
android:background="@color/view_div_color" />
<com.ux.video.file.filerecovery.utils.CustomTextView
android:id="@+id/tv_end_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_filter_date"
android:gravity="center"
android:paddingHorizontal="10dp"
android:paddingVertical="6dp"
android:textColor="@color/selector_c7c7cc_blue"
android:textSize="16sp"
app:fontType="bold"
tools:ignore="RelativeOverlap"
tools:text="@string/allow" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</FrameLayout>

View File

@ -21,5 +21,9 @@
<color name="dialog_shape">#0048FD</color>
<color name="dialog_shape_delete">#fdad00</color>
<color name="dialog_progress_color">#6BFFFFFF</color>
<color name="date_dialog_center_bg">#15787880</color>
<color name="date_dialog_bg_unselected">#F2F2F7</color>
<color name="view_div_color">#D9D9D9</color>
</resources>

View File

@ -73,6 +73,8 @@ wait..</string>
<string name="recovered_success">Recovered successfully!</string>
<string name="deleted_success">Deleted successfully!</string>
<string name="text_continue">Continue</string>
<string name="start_date">Start date</string>
<string name="end_date">End date</string>
<string-array name="filter_date">
<item>All</item>

1
pickerview/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

59
pickerview/build.gradle Normal file
View File

@ -0,0 +1,59 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
//apply plugin: 'com.novoda.bintray-release'
//apply from: 'publish.gradle'
android {
compileSdkVersion 36
namespace = "org.jaaksi.pickerview"
defaultConfig {
minSdkVersion 15
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
// build.gradle中的all加入格式
tasks.withType(Javadoc) {
options {
encoding "UTF-8"
charSet 'UTF-8'
links "http://docs.oracle.com/javase/7/docs/api"
}
// Javadoc Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting):
options.addStringOption('Xdoclint:none', '-quiet')
}
}
dependencies {
implementation "androidx.core:core-ktx:1.6.0"
// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

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

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

72
pickerview/publish.gradle Normal file
View File

@ -0,0 +1,72 @@
def getLocalProperties(String key, Object defValue) {
try {
Properties properties = new Properties()
properties.load(new File(rootDir.absolutePath + "/local.properties").newDataInputStream())
def value = properties.getProperty(key, defValue)
return value
} catch (Exception e) {
return defValue
}
}
def bintrayUser = getLocalProperties("bintrayUser", "")
def bintrayKey = getLocalProperties("bintrayKey", "")
def LibVersion = '3.0.2'
publish {
userOrg = bintrayUser //bintray注册的用户名所属组织名
groupId = 'org.jaaksi' //compile引用时的第1部分groupId
artifactId = 'pickerview' //compile引用时的第2部分项目名
publishVersion = LibVersion //compile引用时的第3部分版本号
desc = 'This is a pickerView library.' //d项目描述
repoName = "maven" //maven
// website = 'https://github.com/jaaksi/maven.git' //
}
afterEvaluate { project ->
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
/*task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
javadoc {
options {
encoding 'UTF-8'
charSet 'UTF-8'
author true
}
}*/
artifacts {
// archives javadocJar
archives sourcesJar
}
}
// jcenter时需要pom文件generatePomFile taskclean之后主动执行以下generatePomFile
task push {
doLast {
exec {
try {
executable 'bash'
args "-c",
"gradle clean generatePomFileForReleasePublication build bintrayUpload -PbintrayUser=$bintrayUser -PbintrayKey=$bintrayKey -PdryRun=false"
println commandType
} catch (Exception e) {
println e.message
}
}
}
}

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jaaksi.pickerview" />

View File

@ -0,0 +1,17 @@
package org.jaaksi.pickerview.adapter
/**
* The simple Array wheel adapter
* 数据实现 [PickerDataSet]即可
*
* @param <T> the element type
*/
class ArrayWheelAdapter<T>(val data: List<T>?) : WheelAdapter<T> {
override fun getItem(index: Int): T? {
return data?.getOrNull(index)
}
override val itemCount: Int
get() = data?.size ?: 0
}

View File

@ -0,0 +1,17 @@
package org.jaaksi.pickerview.adapter
/**
* 数字adapter
*
* @param minValue the wheel min value
* @param maxValue the wheel max value
*/
class NumericWheelAdapter(private val minValue: Int, private val maxValue: Int) :
WheelAdapter<Int> {
override val itemCount: Int
get() = maxValue - minValue + 1
override fun getItem(index: Int): Int {
return if (index in 0 until itemCount) minValue + index else 0
}
}

View File

@ -0,0 +1,18 @@
package org.jaaksi.pickerview.adapter
interface WheelAdapter<T> {
/**
* Gets items count
*
* @return the count of wheel items
*/
val itemCount: Int
/**
* Gets a wheel item by index.
*
* @param index the item index
* @return the wheel item text or null
*/
fun getItem(index: Int): T?
}

View File

@ -0,0 +1,12 @@
package org.jaaksi.pickerview.dataset
/**
* Created by fuchaoyang on 2018/2/11.<br></br>
* description[OptionPicker]专用数据集
*/
interface OptionDataSet : PickerDataSet {
/**
* @return 下一级的数据集
*/
fun getSubs(): List<OptionDataSet>?
}

View File

@ -0,0 +1,15 @@
package org.jaaksi.pickerview.dataset
/**
* 创建时间2018年01月31日16:34 <br></br>
* 作者fuchaoyang <br></br>
* 描述数据实现接口用户显示文案
*/
interface PickerDataSet {
fun getCharSequence(): CharSequence?
/**
* @return 上传的value用于匹配初始化选中的下标
*/
fun getValue(): String?
}

View File

@ -0,0 +1,69 @@
package org.jaaksi.pickerview.dialog
import android.content.Context
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.jaaksi.pickerview.R
import org.jaaksi.pickerview.databinding.DialogPickerviewDefaultBinding
import org.jaaksi.pickerview.picker.BasePicker
/**
* picker.dialog(IPickerDialog接口)自定义Dialog提供DefaultPickerDialog支持全局设定
*/
class DefaultPickerDialog(context: Context) :
BottomSheetDialog(context, R.style.BottomSheetDialog),
IPickerDialog {
lateinit var picker: BasePicker
private set
protected var onPickerChooseListener: OnPickerChooseListener? = null
val binding = DialogPickerviewDefaultBinding.inflate(layoutInflater)
init {
setContentView(binding.root)
}
override fun onStart() {
super.onStart()
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.isDraggable = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setCanceledOnTouchOutside(sDefaultCanceledOnTouchOutside)
binding.btnCancel.setOnClickListener {
if (!picker.canSelected()) return@setOnClickListener // 滑动未停止不响应点击事件
dismiss()
onPickerChooseListener?.onCancel()
}
binding.btnConfirm.setOnClickListener {
if (!picker.canSelected()) return@setOnClickListener
// 给用户拦截
if (onPickerChooseListener == null || onPickerChooseListener!!.onConfirm()) {
// 抛给picker去处理
dismiss()
picker.onConfirm()
}
}
}
/**
* 先于onCreate(Bundle savedInstanceState)执行
*/
override fun onCreate(picker: BasePicker) {
this.picker = picker
binding.root.addView(picker.view())
}
override fun showDialog() {
show()
}
companion object {
/** Canceled dialog OnTouch Outside */
var sDefaultCanceledOnTouchOutside = true
}
}

View File

@ -0,0 +1,10 @@
package org.jaaksi.pickerview.dialog
import android.content.Context
interface IGlobalDialogCreator {
/**
* 创建IPickerDialog
*/
fun create(context: Context): IPickerDialog
}

View File

@ -0,0 +1,16 @@
package org.jaaksi.pickerview.dialog
import org.jaaksi.pickerview.picker.BasePicker
interface IPickerDialog {
/**
* picker create 时回调
* @see BasePicker.BasePicker
*/
fun onCreate(picker: BasePicker)
/**
* 其实可以不提供这个方法为了方便在[BasePicker.show]
*/
fun showDialog()
}

View File

@ -0,0 +1,9 @@
package org.jaaksi.pickerview.dialog
interface OnPickerChooseListener {
/**
* @return 是否回调选中关闭dialog
*/
fun onConfirm(): Boolean
fun onCancel()
}

View File

@ -0,0 +1,239 @@
package org.jaaksi.pickerview.picker
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.util.SparseArray
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import org.jaaksi.pickerview.dialog.DefaultPickerDialog
import org.jaaksi.pickerview.dialog.IGlobalDialogCreator
import org.jaaksi.pickerview.dialog.IPickerDialog
import org.jaaksi.pickerview.widget.PickerView
/**
* 创建时间2018年01月31日18:28 <br></br>
* 作者fuchaoyang <br></br>
* BasePicker中并不提供对pickerview的设置方法而是通过接口PickerHandler转交PickerView处理
* 三个picker的的思路有部分是不一样的如reset调用地方看看是不是可以优化
*/
abstract class BasePicker(protected var mContext: Context) {
/** 是否启用dialog */
@JvmField
protected var needDialog = true
@JvmField
protected var iPickerDialog: IPickerDialog? = null
protected lateinit var mPickerContainer: LinearLayout
private var mInterceptor: Interceptor? = null
/**
* setTag用法同[View.setTag]
*
* @param tag tag
*/
var tag: Any? = null
private var mKeyedTags: SparseArray<Any>? = null
private val mPickerViews: MutableList<PickerView<*>> = ArrayList()
/**
* 设置拦截器用于用于在pickerview创建时拦截设置pickerview的属性Picker内部并不提供对PickerView的设置方法
* 而是通过Interceptor实现实现Picker和PickerView的属性设置解耦
* 必须在调用 [.createPickerView]之前设置
* 子类应该在Builder中提供该方法
*/
protected fun setInterceptor(interceptor: Interceptor?) {
mInterceptor = interceptor
}
/**
* 设置picker背景
*
* @param color color
*/
fun setPickerBackgroundColor(@ColorInt color: Int) {
mPickerContainer.setBackgroundColor(color)
}
/**
* 设置pickerview父容器padding 单位:px
*/
fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
mPickerContainer.setPadding(left, top, right, bottom)
}
fun getTag(key: Int): Any? {
return mKeyedTags?.get(key)
}
protected fun initPickerView() {
mPickerContainer = LinearLayout(mContext)
mPickerContainer.orientation = LinearLayout.HORIZONTAL
mPickerContainer.layoutParams = LinearLayout.LayoutParams(-1, -2)
if (sDefaultPaddingRect != null) {
setPadding(
sDefaultPaddingRect!!.left, sDefaultPaddingRect!!.top, sDefaultPaddingRect!!.right,
sDefaultPaddingRect!!.bottom
)
}
if (sDefaultPickerBackgroundColor != Color.TRANSPARENT) {
setPickerBackgroundColor(sDefaultPickerBackgroundColor)
}
if (needDialog) { // 是否使用弹窗
// 弹窗优先级:自定义的 > 全局的 > 默认的
if (iPickerDialog == null) { // 如果没有自定义dialog
iPickerDialog = if (sDefaultDialogCreator != null) { // 如果定义了全局的dialog
sDefaultDialogCreator!!.create(mContext)
} else { // 使用默认的
DefaultPickerDialog(mContext)
}
}
iPickerDialog?.onCreate(this)
}
}
/**
* [.createPickerView]
*
* @return Picker中所有的pickerview集合
*/
val pickerViews: List<PickerView<*>>
get() = mPickerViews
/**
* 如果使用[.createPickerView]创建pickerview就不需要手动添加
*
* @param pickerView pickerView
*/
protected fun addPicker(pickerView: PickerView<*>) {
mPickerViews.add(pickerView)
}
/**
* 创建pickerview
*
* @param tag settag
* @param weight 权重
*/
protected fun <T> createPickerView(tag: Any?, weight: Float): PickerView<T> {
val pickerView: PickerView<T> = PickerView(mContext)
pickerView.tag = tag
// 这里是竖直方向的如果要设置横向的则自己再设置LayoutParams
val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT)
params.weight = weight
// do it
if (mInterceptor != null) {
mInterceptor!!.intercept(pickerView, params)
}
pickerView.layoutParams = params
mPickerContainer.addView(pickerView)
addPicker(pickerView)
return pickerView
}
/**
* 通过tag找到对应的pickerview
*
* @param tag tag
* @return 对应tag的pickerview找不到返回null
*/
fun findPickerViewByTag(tag: Any): PickerView<*>? {
for (pickerView in mPickerViews) {
if (checkIsSamePickerView(tag, pickerView.tag)) return pickerView
}
return null
}
/**
* 通过两个tag判断是否是同一个pickerview
*/
protected fun checkIsSamePickerView(tag: Any, pickerViewTag: Any): Boolean {
return tag == pickerViewTag
}
/**
* 是否滚动未停止
*/
fun canSelected(): Boolean {
for (i in mPickerViews.indices.reversed()) {
val pickerView = mPickerViews[i]
if (!pickerView.canSelected()) {
return false
}
}
return true
}
/**
* setTag 用法同[View.setTag]
*
* @param key key R.id.xxx
* @param tag tag
*/
fun setTag(key: Int, tag: Any) {
// If the package id is 0x00 or 0x01, it's either an undefined package
// or a framework id
require(key ushr 24 >= 2) { "The key must be an application-specific " + "resource id." }
setKeyedTag(key, tag)
}
private fun setKeyedTag(key: Int, tag: Any) {
if (mKeyedTags == null) {
mKeyedTags = SparseArray(2)
}
mKeyedTags!!.put(key, tag)
}
/**
* @return 获取IPickerDialog
*/
fun dialog(): IPickerDialog? {
return iPickerDialog
}
/**
* @return 获取picker的view用于非弹窗情况
*/
fun view(): LinearLayout {
return mPickerContainer
}
/**
* 显示picker弹窗
*/
fun show() {
iPickerDialog?.showDialog()
}
/**
* 点击确定按钮的回调
*/
abstract fun onConfirm()
/**
* 用于子类修改设置PickerView属性
*/
fun interface Interceptor {
/**
* 拦截pickerview的创建我们可以自定义
*
* @param pickerView 增加layoutparams参数方便设置weight
*/
fun intercept(pickerView: PickerView<*>, params: LinearLayout.LayoutParams)
}
companion object {
/** pickerView父容器的 default padding */
var sDefaultPaddingRect: Rect? = null
/** default picker background color */
var sDefaultPickerBackgroundColor = Color.WHITE
/** Canceled dialog OnTouch Outside */
var sDefaultCanceledOnTouchOutside = true
/** 用于构建全局的DefaultDialog的接口 */
var sDefaultDialogCreator: IGlobalDialogCreator? = null
}
}

View File

@ -0,0 +1,238 @@
package org.jaaksi.pickerview.picker
import android.content.Context
import org.jaaksi.pickerview.dataset.OptionDataSet
import org.jaaksi.pickerview.dialog.IPickerDialog
import org.jaaksi.pickerview.picker.option.ForeignOptionDelegate
import org.jaaksi.pickerview.picker.option.IOptionDelegate
import org.jaaksi.pickerview.picker.option.OptionDelegate
import org.jaaksi.pickerview.widget.BasePickerView
import org.jaaksi.pickerview.widget.BasePickerView.OnSelectedListener
import org.jaaksi.pickerview.widget.PickerView
/**
* Created by fuchaoyang on 2018/2/11.<br></br>
* description多级别的的选项picker支持联动与非联动
* 强大点
* 与https://github.com/Bigkoo/Android-PickerView对比
* 1.支持设置层级
* 2.构造数据源简单只需要实现OptionDataSet接口
* 3.支持联动及不联动
* 3.支持通过选中的value设置选中项内部处理选中项逻辑避免用户麻烦的遍历处理
*
*
*/
class OptionPicker private constructor(
context: Context,
private val hierarchy: Int,
private val onOptionSelectListener: OnOptionSelectListener
) : BasePicker(context), OnSelectedListener, BasePickerView.Formatter {
// 层级有几层add几个pickerview
// 选中的下标。如果为-1表示当前选中的index列没有数据
/**
* 获取选中的下标
*
* @return 选中的下标数组size=mHierarchy如果为-1表示该列没有数据
*/
val selectedPosition: IntArray = IntArray(this.hierarchy)
/** 是否无关连 */
private var mIsForeign = false
private var mFormatter: Formatter? = null
private var mDelegate: IOptionDelegate? = null
private fun initPicker() {
for (i in 0 until this.hierarchy) {
val pickerView: PickerView<*> = createPickerView<Any>(i, 1f)
pickerView.setOnSelectedListener(this)
pickerView.formatter = this
}
}
fun setFormatter(formatter: Formatter?) {
mFormatter = formatter
}
private fun initForeign(foreign: Boolean) {
mIsForeign = foreign
mDelegate = if (mIsForeign) { // 不关联的
ForeignOptionDelegate()
} else {
OptionDelegate()
}
mDelegate!!.init(object : Delegate {
override val hierarchy: Int
get() = this@OptionPicker.hierarchy
override val selectedPosition: IntArray
get() = this@OptionPicker.selectedPosition
override fun getPickerViews(): List<PickerView<OptionDataSet>> {
return pickerViews as List<PickerView<OptionDataSet>>
}
})
}
/**
* 根据选中的values初始化选中的position并初始化pickerview数据
*
* @param options data
*/
fun setData(vararg options: List<OptionDataSet>) {
// 初始化是否关联
initForeign(options.size > 1)
mDelegate!!.setData(*options)
}
/**
* 根据选中的values初始化选中的position
*
* @param values 选中数据的value[OptionDataSet.getValue]如果values[0]==null则进行默认选中其他为null认为没有该列
*/
fun setSelectedWithValues(vararg values: String?) {
mDelegate!!.setSelectedWithValues(*values)
}
private fun reset() {
mDelegate!!.reset()
}
val selectedOptions: Array<OptionDataSet?>
/**
* 获取选中的选项
*
* @return 选中的选项如果指定index为null则表示该列没有数据
*/
get() = mDelegate!!.selectedOptions
override fun onConfirm() {
onOptionSelectListener.onOptionSelect(this, this.selectedPosition, selectedOptions)
}
// 重置选中的position
private fun resetPosition(index: Int, position: Int) {
for (i in index until selectedPosition.size) {
if (i == index) {
selectedPosition[i] = position
} else {
if (!mIsForeign) {
// 如果是无关的则不需要处理后面的index关联的则直接重置为0
selectedPosition[i] = 0
}
}
}
}
override fun onSelected(pickerView: BasePickerView<*>, position: Int) {
// 1联动2联动3...当前选中position后面的都重置为0更改mSelectedPosition,然后直接reset
val index = pickerView.tag as Int
resetPosition(index, position)
reset()
if (!needDialog){
onConfirm()
}
}
override fun format(
pickerView: BasePickerView<*>,
position: Int,
charSequence: CharSequence?
): CharSequence? {
return if (mFormatter == null) charSequence else mFormatter!!.format(
this,
pickerView.tag as Int,
position,
charSequence
)
}
/**
* 强制设置的属性直接在构造方法中设置
*
* @param hierarchy 层级有几层add几个pickerview
* @param listener listener
*/
class Builder(
private val context: Context,
private val hierarchy: Int,
private val onOptionSelectListener: OnOptionSelectListener
) {
private var mInterceptor: Interceptor? = null
private var mFormatter: Formatter? = null
private var needDialog = true
private var iPickerDialog: IPickerDialog? = null
/**
* 设置内容 Formatter
*
* @param formatter formatter
*/
fun setFormatter(formatter: Formatter?): Builder {
mFormatter = formatter
return this
}
/**
* 设置拦截器
*
* @param interceptor 拦截器
*/
fun setInterceptor(interceptor: Interceptor?): Builder {
mInterceptor = interceptor
return this
}
/**
* 自定义弹窗如果为null表示不需要弹窗
* @param iPickerDialog
*/
fun dialog(iPickerDialog: IPickerDialog?): Builder {
needDialog = iPickerDialog != null
this.iPickerDialog = iPickerDialog
return this
}
fun create(): OptionPicker {
val picker = OptionPicker(context, hierarchy, onOptionSelectListener)
picker.needDialog = needDialog
picker.iPickerDialog = iPickerDialog
picker.initPickerView()
picker.setFormatter(mFormatter)
picker.setInterceptor(mInterceptor)
picker.initPicker()
return picker
}
}
interface Formatter {
/**
* @param level 级别 0 ~ mHierarchy - 1
* @param charSequence charSequence
*/
fun format(
picker: OptionPicker,
level: Int,
position: Int,
charSequence: CharSequence?
): CharSequence?
}
interface OnOptionSelectListener {
/**
* @param selectedPosition length = mHierarchy选中的下标:如果指定index为-1表示当前选中的index列没有数据
* @param selectedOptions length = mHierarchy选中的选项如果指定index为null则表示该列没有数据
*/
fun onOptionSelect(
picker: OptionPicker, selectedPosition: IntArray,
selectedOptions: Array<OptionDataSet?>
)
}
interface Delegate {
val hierarchy: Int
val selectedPosition: IntArray
fun getPickerViews(): List<PickerView<OptionDataSet>>
}
}

View File

@ -0,0 +1,934 @@
package org.jaaksi.pickerview.picker
import android.content.Context
import org.jaaksi.pickerview.adapter.ArrayWheelAdapter
import org.jaaksi.pickerview.adapter.NumericWheelAdapter
import org.jaaksi.pickerview.dialog.IPickerDialog
import org.jaaksi.pickerview.util.DateUtil
import org.jaaksi.pickerview.util.DateUtil.createDateFormat
import org.jaaksi.pickerview.util.DateUtil.getDayOfMonth
import org.jaaksi.pickerview.widget.BasePickerView
import org.jaaksi.pickerview.widget.BasePickerView.OnSelectedListener
import org.jaaksi.pickerview.widget.PickerView
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
/**
* 创建时间2018年08月02日15:42 <br></br>
* 作者fuchaoyang <br></br>
* 描述时间选择器
* 强大点
* 1.type的设计自由组合
* 2.支持时间区间设置以及选中联动
* 3.支持混合模式支持日期时间混合
* 4.支持自定义日期时间格式
* 5.time支持设置时间间隔如30分钟也就是00:00 00:30 01:00 01:30
* 无法被60整除的如设置13分钟认为是无效设置会被忽略如13 26 39 52,只能选到52
*/
class TimePicker private constructor(
context: Context,
private val type: Int,
private val onTimeSelectListener: OnTimeSelectListener
) : BasePicker(context), OnSelectedListener, BasePickerView.Formatter {
private var mDatePicker: PickerView<Int>? = null
private var mYearPicker: PickerView<Int>? = null
private var mMonthPicker: PickerView<Int>? = null
private var mDayPicker: PickerView<Int>? = null
private var mTimePicker: PickerView<Int>? = null
private var mHourPicker: PickerView<Int>? = null
private var mNoonPicker: PickerView<Int>? = null
private var mMinutePicker: PickerView<Int>? = null
// 初始设置选中的时间如果不设置为startDate
private var mSelectedDate: Calendar? = null
private lateinit var mStartDate: Calendar //开始时间
private lateinit var mEndDate: Calendar //终止时间
// 聚合的日期模式
private var mDayOffset = -1
private var mStartYear = 0
private var mEndYear = 0
private var mStartMonth = 0
private var mEndMonth = 0
private var mStartDay = 0
private var mEndDay = 0
private var mStartHour = 0
private var mEndHour = 0
private var mStartMinute = 0
private var mEndMinute = 0
// 时间分钟间隔
private var mTimeMinuteOffset = 0
// 设置offset时是否包含起止时间
private var mContainsStarDate = false
private var mContainsEndDate = false
var formatter: Formatter? = null
/**
* @param calendar 指定时间
* @param isStart 是否是起始时间
* @return 指定时间的有效分钟偏移量
*/
private fun getValidTimeOffset(
calendar: Calendar, isStart: Boolean
): Int {
val timeMinutes = calendar[Calendar.MINUTE]
var validOffset: Int
val offset = timeMinutes % mTimeMinuteOffset
if (offset == 0) {
validOffset = 0
} else {
validOffset = -offset
if (isStart) {
if (!mContainsStarDate) {
validOffset += mTimeMinuteOffset
}
} else {
if (mContainsEndDate) {
validOffset += mTimeMinuteOffset
}
}
}
return validOffset
}
private fun ignoreSecond(calendar: Calendar) {
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
}
private fun setRangDate(startDate: Long, endDate: Long) {
// 重新计算时间区间bugfix由于由于起始时间没有考虑时间间隔而导致可能会引起bug
var calendar = Calendar.getInstance()
calendar.timeInMillis = startDate
ignoreSecond(calendar)
calendar.add(Calendar.MINUTE, getValidTimeOffset(calendar, true))
mStartDate = calendar
calendar = Calendar.getInstance()
calendar.timeInMillis = endDate
ignoreSecond(calendar)
calendar.add(Calendar.MINUTE, getValidTimeOffset(calendar, false))
mEndDate = calendar
}
/**
* 设置选中的时间如果不设置默认为起始时间应该伴随着show方法使用
*
* @param millis 选中的时间戳 单位ms
*/
fun setSelectedDate(millis: Long) {
updateSelectedDate(millis)
reset()
}
private fun updateSelectedDate(millis: Long) {
if (mSelectedDate == null) {
mSelectedDate = Calendar.getInstance()
}
mSelectedDate!!.timeInMillis = millis
ignoreSecond(mSelectedDate!!)
}
/**
* @return 是否有上下午并且是下午
*/
private val isAfterNoon: Boolean
private get() = hasType(TYPE_12_HOUR) && mNoonPicker!!.selectedItem == 1
val selectedDates: Date
get() {
val calendar = Calendar.getInstance()
if (hasType(TYPE_MIXED_DATE)) {
calendar.timeInMillis = mStartDate.timeInMillis
calendar.add(Calendar.DAY_OF_YEAR, mDatePicker!!.selectedPosition)
} else {
calendar.time = mSelectedDate!!.time
if (hasType(TYPE_YEAR)) {
calendar[Calendar.YEAR] = mYearPicker!!.selectedItem!!
}
if (hasType(TYPE_MONTH)) {
calendar[Calendar.MONTH] = mMonthPicker!!.selectedItem!! - 1
}
if (hasType(TYPE_DAY)) {
calendar[Calendar.DAY_OF_MONTH] = mDayPicker!!.selectedItem!!
}
}
if (hasType(TYPE_MIXED_TIME)) {
var hour = mTimePicker!!.selectedItem!! * mTimeMinuteOffset / 60
if (isAfterNoon) { // 下午
hour += 12
}
calendar[Calendar.HOUR_OF_DAY] = hour
val minute = mTimePicker!!.selectedItem!! * mTimeMinuteOffset % 60
calendar[Calendar.MINUTE] = minute
} else {
if (hasType(TYPE_HOUR)) {
val hour =
if (isAfterNoon) mHourPicker!!.selectedItem!! + 12 else mHourPicker!!.selectedItem!!
calendar[Calendar.HOUR_OF_DAY] = hour
}
if (hasType(TYPE_MINUTE)) {
calendar[Calendar.MINUTE] = getRealMinute(mMinutePicker!!.selectedPosition)
}
}
return calendar.time
}
/**
* @param type type
* @return 是否包含类型 type
*/
fun hasType(type: Int): Boolean {
return this.type and type == type
}
/**
* createPickerView在init中执行那么[.setInterceptor]就必须在构造该方法之前执行才有效采用Builder
*/
private fun initPicker() {
if (hasType(TYPE_MIXED_DATE)) {
mDatePicker = createPickerView(TYPE_MIXED_DATE, 2.5f)
mDatePicker!!.setOnSelectedListener(this)
mDatePicker!!.formatter = this
} else {
if (hasType(TYPE_YEAR)) {
mYearPicker = createPickerView(TYPE_YEAR, 1.2f)
mYearPicker!!.setOnSelectedListener(this)
mYearPicker!!.formatter = this
}
if (hasType(TYPE_MONTH)) {
mMonthPicker = createPickerView(TYPE_MONTH, 1f)
mMonthPicker!!.setOnSelectedListener(this)
mMonthPicker!!.formatter = this
}
if (hasType(TYPE_DAY)) {
mDayPicker = createPickerView(TYPE_DAY, 1f)
mDayPicker!!.setOnSelectedListener(this)
mDayPicker!!.formatter = this
}
}
if (hasType(TYPE_12_HOUR)) { // 上下午
mNoonPicker = createPickerView(TYPE_12_HOUR, 1f)
mNoonPicker!!.setOnSelectedListener(this)
mNoonPicker!!.formatter = this
}
if (hasType(TYPE_MIXED_TIME)) { // 包含Time
mTimePicker = createPickerView(TYPE_MIXED_TIME, 2f)
mTimePicker!!.formatter = this
mTimePicker!!.setOnSelectedListener(this)
} else {
if (hasType(TYPE_HOUR)) {
mHourPicker = createPickerView(TYPE_HOUR, 1f)
mHourPicker!!.setOnSelectedListener(this)
mHourPicker!!.formatter = this
if (hasType(TYPE_12_HOUR)) { // 如果是12小时制将小时设置为循环的
mHourPicker!!.setIsCirculation(true)
}
}
if (hasType(TYPE_MINUTE)) {
mMinutePicker = createPickerView(TYPE_MINUTE, 1f)
mMinutePicker!!.formatter = this
mMinutePicker!!.setOnSelectedListener(this)
}
}
}
private fun handleData() {
if (mSelectedDate == null || mSelectedDate!!.timeInMillis < mStartDate.timeInMillis) {
updateSelectedDate(mStartDate.timeInMillis)
} else if (mSelectedDate!!.timeInMillis > mEndDate.timeInMillis) {
updateSelectedDate(mEndDate.timeInMillis)
}
if (mTimeMinuteOffset < 1) {
mTimeMinuteOffset = 1
}
// 因为区间不能改变,所以这里只进行一次初始化操作
if (mDayOffset == -1 || mStartYear == 0) {
if (hasType(TYPE_MIXED_DATE)) {
mDayOffset = offsetStart(mEndDate)
} else {
mStartYear = mStartDate[Calendar.YEAR]
mEndYear = mEndDate[Calendar.YEAR]
mStartMonth = mStartDate[Calendar.MONTH] + 1
mEndMonth = mEndDate[Calendar.MONTH] + 1
mStartDay = mStartDate[Calendar.DAY_OF_MONTH]
mEndDay = mEndDate[Calendar.DAY_OF_MONTH]
}
mStartHour = mStartDate[Calendar.HOUR_OF_DAY]
mEndHour = mEndDate[Calendar.HOUR_OF_DAY]
mStartMinute = mStartDate[Calendar.MINUTE]
mEndMinute = mEndDate[Calendar.MINUTE]
}
}
private fun reset() {
handleData()
// 处理数据,根据当前选中的时间及设置的日期范围处理数据
if (hasType(TYPE_MIXED_DATE)) {
if (mDatePicker!!.adapter == null) {
mDatePicker!!.adapter = NumericWheelAdapter(0, mDayOffset)
}
mDatePicker!!.setSelectedPosition(offsetStart(mSelectedDate!!), false)
if (hasType(TYPE_12_HOUR)) {
resetNoonAdapter(true)
}
if (hasType(TYPE_MIXED_TIME)) {
// 时间需要考虑起始日期对应的起始时间
resetTimeAdapter(true)
} else {
resetHourAdapter(true)
}
} else {
if (hasType(TYPE_YEAR)) {
if (mYearPicker!!.adapter == null) { // 年不会发生变化,不需要重复设置
mYearPicker!!.adapter =
NumericWheelAdapter(mStartDate[Calendar.YEAR], mEndDate[Calendar.YEAR])
}
mYearPicker!!.setSelectedPosition(
mSelectedDate!![Calendar.YEAR] - mYearPicker!!.adapter!!.getItem(0)!!, false
)
}
resetMonthAdapter(true)
}
}
private fun resetMonthAdapter(isInit: Boolean) {
// 1.根据当前选中的年份,以及起止时间,设置对应的月份。然后再设置对应的日
if (hasType(TYPE_MONTH)) {
val year =
if (hasType(TYPE_YEAR)) mYearPicker!!.selectedItem!! else mSelectedDate!![Calendar.YEAR]
val start: Int
val end: Int
// 这里要计算 selectedItem 而不是selectedPosition
val last =
if (isInit) mSelectedDate!![Calendar.MONTH] + 1 else mMonthPicker!!.selectedItem!!
start = if (year == mStartYear) mStartMonth else 1
end = if (year == mEndYear) mEndMonth else 12
mMonthPicker!!.adapter = NumericWheelAdapter(start, end)
// 2.设置选中的月份
mMonthPicker!!.setSelectedPosition(last - mMonthPicker!!.adapter!!.getItem(0)!!, false)
}
// 3.月份要联动日
resetDayAdapter(isInit)
}
private fun resetDayAdapter(isInit: Boolean) {
if (hasType(TYPE_DAY)) {
val year =
if (hasType(TYPE_YEAR)) mYearPicker!!.selectedItem!! else mSelectedDate!![Calendar.YEAR]
// 3.根据当前选中的年月设置日期。联动同月份。如果和起始或截止时同一年月,则比较对应日期
// 有年,有日,则强制认为有月。
val month =
if (hasType(TYPE_MONTH)) mMonthPicker!!.selectedItem!! else mSelectedDate!![Calendar.MONTH] + 1
val last =
if (isInit) mSelectedDate!![Calendar.DAY_OF_MONTH] else mDayPicker!!.selectedItem!!
val start = if (year == mStartYear && month == mStartMonth) mStartDay else 1
val end =
if (year == mEndYear && month == mEndMonth) mEndDay else getDayOfMonth(year, month)
mDayPicker!!.adapter = NumericWheelAdapter(start, end)
mDayPicker!!.setSelectedPosition(last - mDayPicker!!.adapter!!.getItem(0)!!, false)
}
resetNoonAdapter(isInit)
}
/**
* 选择上下午的时候如果选中的是起止时间要重置下一级hour or timeadapter)
* 比如起始时间是 9:30如果是上午则hour是9-11如果是下午则是0-11非起止时间都是0-11
* 如果结束时间
*/
private fun resetNoonAdapter(isInit: Boolean) {
if (hasType(TYPE_12_HOUR)) {
val isSameStartDay = isSameDay(true)
val isSameEndDay = isSameDay(false)
val noons: MutableList<Int> = ArrayList()
if (!isSameStartDay || mStartHour < 12) { // 如果是起始的那天,且时间>11点则不包含上午
noons.add(0)
}
if (!isSameEndDay || mEndHour >= 12) { // 如果是结束的那天,且时间<12点则不包含下午
noons.add(1)
}
val last: Int = if (isInit) {
if (mSelectedDate!![Calendar.HOUR_OF_DAY] < 12) 0 else 1
} else {
mNoonPicker!!.selectedItem!!
}
mNoonPicker!!.adapter = ArrayWheelAdapter(noons)
mNoonPicker!!.setSelectedPosition(last, false)
}
if (hasType(TYPE_MIXED_TIME)) {
resetTimeAdapter(isInit)
} else {
// 日联动小时
resetHourAdapter(isInit)
}
}
private fun resetTimeAdapter(isInit: Boolean) { // 如果是聚合日期+12小时制+聚合时间就crash了。因为没有初始化上下午adapter
val isSameStartDay = isSameDay(true)
val isSameEndDay = isSameDay(false)
val start: Int
val end: Int
if (!hasType(TYPE_12_HOUR)) {
start = if (isSameStartDay) getValidTimeMinutes(mStartDate, true) else 0
end = if (isSameEndDay) getValidTimeMinutes(
mEndDate, false
) else getValidTimeMinutes(24 * 60 - mTimeMinuteOffset, false)
} else {
if (isSameStartDay) {
// 如果起始时间是上午并且选择的是下午start=0,否则start=get12Hour(start)
start = if (mStartHour < 12 && mNoonPicker!!.selectedItem == 1) {
0
} else {
if (mStartHour >= 12) getValidTimeMinutes(
mStartDate, true
) - 12 * 60 else getValidTimeMinutes(mStartDate, true)
}
end = if (isSameEndDay && mEndHour >= 12 && mNoonPicker!!.selectedItem == 1) {
// 如果 > 12 需要减去12小时
if (mEndHour >= 12) getValidTimeMinutes(
mEndDate, false
) - 12 * 60 else getValidTimeMinutes(
mEndDate, false
)
} else {
getValidTimeMinutes(12 * 60 - mTimeMinuteOffset, false)
}
} else if (isSameEndDay) {
start = 0
end = if (mEndHour >= 12 && mNoonPicker!!.selectedItem == 1) {
// 如果 > 12 需要减去12小时
if (mEndHour >= 12) getValidTimeMinutes(
mEndDate, false
) - 12 * 60 else getValidTimeMinutes(
mEndDate, false
)
} else {
getValidTimeMinutes(12 * 60 - mTimeMinuteOffset, false)
}
} else {
start = 0
end = getValidTimeMinutes(12 * 60 - mTimeMinuteOffset, false)
}
}
val last: Int = if (isInit) {
if (hasType(TYPE_12_HOUR)) {
val timeMinutes = getValidTimeMinutes(mSelectedDate, true)
if (timeMinutes >= 12 * 60) getValidTimeMinutes(
mSelectedDate, true
) - 12 * 60 else getValidTimeMinutes(mSelectedDate, true)
} else {
getValidTimeMinutes(mSelectedDate, true)
}
} else {
mTimePicker!!.selectedItem!! * mTimeMinuteOffset
}
// adapter 的item设置的是 有效分钟数/mTimeMinuteOffset
mTimePicker!!.adapter =
NumericWheelAdapter(getValidTimesValue(start), getValidTimesValue(end))
mTimePicker!!.setSelectedPosition(findPositionByValidTimes(last), false)
}
private fun resetHourAdapter(isInit: Boolean) {
if (hasType(TYPE_HOUR)) {
val isSameStartDay = isSameDay(true)
val isSameEndDay = isSameDay(false)
val start: Int
val end: Int
if (!hasType(TYPE_12_HOUR)) {
start = if (isSameStartDay) mStartHour else 0
end = if (isSameEndDay) mEndHour else 23
} else {
if (isSameStartDay) {
// 如果起始时间是上午并且选择的是下午start=0,否则start=get12Hour(start)
start = if (mStartHour < 12 && mNoonPicker!!.selectedItem == 1) {
0
} else {
get12Hour(mStartHour)
}
end =
if (isSameEndDay && mEndHour >= 12 && mNoonPicker!!.selectedItem == 1) { // 如果开始和结束时间是同一天
get12Hour(mEndHour)
} else {
11
}
} else if (isSameEndDay) {
start = 0
// 如果截止时间是下午如果选择的是上午end=11如果选择的下午end=get12Hour(mEndHour)
// 如果截止时间是上午选择的是上午end=get12Hour
end = if (mEndHour >= 12 && mNoonPicker!!.selectedItem == 1) {
get12Hour(mEndHour)
} else {
11
}
} else {
start = 0
end = 11
}
}
val last: Int = if (isInit) {
if (hasType(TYPE_12_HOUR)) {
get12Hour(mSelectedDate!![Calendar.HOUR_OF_DAY])
} else {
mSelectedDate!![Calendar.HOUR_OF_DAY]
}
} else {
mHourPicker!!.selectedItem!!
}
mHourPicker!!.adapter = NumericWheelAdapter(start, end)
mHourPicker!!.setSelectedPosition(last - mHourPicker!!.adapter!!.getItem(0)!!, false)
}
resetMinuteAdapter(isInit)
}
private fun get12Hour(hour: Int): Int {
return if (hour >= 12) {
hour - 12
} else hour
}
private fun isSameDay(isStart: Boolean): Boolean {
return if (hasType(TYPE_MIXED_DATE)) {
if (isStart) {
DateUtil.isSameDay(mStartDate.timeInMillis,selectedDate.time)
} else {
DateUtil.isSameDay(mEndDate.timeInMillis, selectedDate.time)
}
} else {
val year =
if (hasType(TYPE_YEAR)) mYearPicker!!.selectedItem!! else mSelectedDate!![Calendar.YEAR]
val month =
if (hasType(TYPE_MONTH)) mMonthPicker!!.selectedItem!! else mSelectedDate!![Calendar.MONTH] + 1
val day =
if (hasType(TYPE_DAY)) mDayPicker!!.selectedItem!! else mSelectedDate!![Calendar.DAY_OF_MONTH]
if (isStart) {
year == mStartYear && month == mStartMonth && day == mStartDay
} else {
year == mEndYear && month == mEndMonth && day == mEndDay
}
}
}
private fun resetMinuteAdapter(isInit: Boolean) {
if (hasType(TYPE_MINUTE)) {
val isSameStartDay: Boolean
val isSameEndDay: Boolean
if (hasType(TYPE_MIXED_DATE)) {
isSameStartDay = DateUtil.isSameDay(selectedDate.time, mStartDate.timeInMillis)
isSameEndDay = DateUtil.isSameDay(selectedDate.time, mEndDate.timeInMillis)
} else {
val year =
if (hasType(TYPE_YEAR)) mYearPicker!!.selectedItem!! else mSelectedDate!![Calendar.YEAR]
val month =
if (hasType(TYPE_MONTH)) mMonthPicker!!.selectedItem!! else mSelectedDate!![Calendar.MONTH] + 1
val day =
if (hasType(TYPE_DAY)) mDayPicker!!.selectedItem!! else mSelectedDate!![Calendar.DAY_OF_MONTH]
isSameStartDay = year == mStartYear && month == mStartMonth && day == mStartDay
isSameEndDay = year == mEndYear && month == mEndMonth && day == mEndDay
}
val hour: Int = if (hasType(TYPE_HOUR)) {
if (hasType(TYPE_12_HOUR) && mNoonPicker!!.selectedItem == 1) {
mHourPicker!!.selectedItem!! + 12
} else {
mHourPicker!!.selectedItem!!
}
} else {
mSelectedDate!![Calendar.HOUR_OF_DAY]
}
val last = if (isInit) mSelectedDate!![Calendar.MINUTE] else getRealMinute(
mMinutePicker!!.selectedPosition
)
val start = if (isSameStartDay && hour == mStartHour) mStartMinute else 0
val end = if (isSameEndDay && hour == mEndHour) mEndMinute else 60 - mTimeMinuteOffset
mMinutePicker!!.adapter =
NumericWheelAdapter(getValidMinuteValue(start), getValidMinuteValue(end))
mMinutePicker!!.setSelectedPosition(findPositionByValidTimes(last), false)
}
}
// 获取有效分钟数对应的item的数值
private fun getValidMinuteValue(validTimeMinutes: Int): Int {
return validTimeMinutes / mTimeMinuteOffset
}
// 通过有效分钟数找到在adapter中的position
private fun findPositionByValidTimes(validTimeMinutes: Int): Int {
val timesValue = getValidMinuteValue(validTimeMinutes)
return if (mMinutePicker != null) {
timesValue - mMinutePicker!!.adapter!!.getItem(0)!!
} else timesValue - mTimePicker!!.adapter!!.getItem(0)!!
}
/**
* 获取对应position的真实分钟数注意这里必须使用position
*
* @param position [BasePickerView.getSelectedPosition]
*/
// 获指定position的分钟item对应的真实的分钟数
private fun getRealMinute(position: Int): Int {
// bugfix:这个position是下标要拿对应item的数值来计算
return mMinutePicker!!.adapter!!.getItem(position)!! * mTimeMinuteOffset
}
// 获取指定position对应的有效的分钟数
private fun getPositionValidMinutes(position: Int): Int {
return mTimePicker!!.adapter!!.getItem(position)!! * mTimeMinuteOffset
}
// 获取有效分钟数对应的item的数值
private fun getValidTimesValue(validTimeMinutes: Int): Int {
return validTimeMinutes / mTimeMinuteOffset
}
/**
* 获取根据mTimeMinuteOffset处理后的有效分钟数
* 默认为 start <= X <= end 即都不包含在内
*/
private fun getValidTimeMinutes(timeMinutes: Int, isStart: Boolean): Int {
var validTimeMinutes: Int
val offset = timeMinutes % mTimeMinuteOffset
if (offset == 0) {
validTimeMinutes = timeMinutes
} else {
if (isStart) {
validTimeMinutes = timeMinutes - offset
if (!mContainsStarDate) {
validTimeMinutes += mTimeMinuteOffset
}
} else {
validTimeMinutes = timeMinutes - offset
if (mContainsEndDate) {
validTimeMinutes += mTimeMinuteOffset
}
}
}
return validTimeMinutes
}
/**
* 获取时间的分钟数
*/
private fun getValidTimeMinutes(calendar: Calendar?, isStart: Boolean): Int {
if (calendar == null) return 0
val hour = calendar[Calendar.HOUR_OF_DAY]
val minute = calendar[Calendar.MINUTE]
val minutes = hour * 60 + minute
return getValidTimeMinutes(minutes, isStart)
}
/**
* 获取指定日期距离第0个的offset
*/
private fun offsetStart(calendar: Calendar): Int {
return DateUtil.getIntervalDay(mStartDate.timeInMillis, calendar.timeInMillis)
}
private val selectedDate: Date
get() = getPositionDate(mDatePicker!!.selectedPosition)
// 获取对应position的日期
private fun getPositionDate(position: Int): Date {
val calendar = Calendar.getInstance()
calendar.timeInMillis = mStartDate.timeInMillis
calendar.add(Calendar.DAY_OF_YEAR, position)
return calendar.time
}
// 获取对应position的时间
private fun getPositionTime(position: Int): Date {
val calendar = Calendar.getInstance()
// 计算出position对应的hour & minute
val minutes = mTimePicker!!.adapter!!.getItem(position)!! * mTimeMinuteOffset
val hour = minutes / 60
val minute = minutes % 60
calendar[Calendar.HOUR_OF_DAY] = hour
calendar[Calendar.MINUTE] = minute
return calendar.time
}
override fun onSelected(pickerView: BasePickerView<*>, position: Int) {
// 联动,年份、月份是固定的,使用日历,获取指定指定某年某月的日期
when (pickerView.tag as Int) {
TYPE_YEAR -> resetMonthAdapter(false)
TYPE_MONTH -> resetDayAdapter(false)
TYPE_MIXED_DATE, TYPE_DAY -> resetNoonAdapter(false)
TYPE_12_HOUR -> if (hasType(TYPE_MIXED_TIME)) {
resetTimeAdapter(false)
} else {
resetHourAdapter(false)
}
TYPE_HOUR -> resetMinuteAdapter(false)
}
if (!needDialog){
onConfirm()
}
}
override fun onConfirm() {
onTimeSelectListener.onTimeSelect(this, selectedDates)
}
/**
* @param position 这个是adapter的position但是起始时间如果不从0开始就不对了
*/
override fun format(
pickerView: BasePickerView<*>, position: Int, charSequence: CharSequence?
): CharSequence? {
if (formatter == null) return charSequence
val type = pickerView.tag as Int
val value: Long = when (type) {
TYPE_MIXED_DATE -> {
getPositionDate(position).time
}
TYPE_MIXED_TIME -> {
getPositionTime(position).time
}
TYPE_MINUTE -> {
getRealMinute(position).toLong()
}
else -> {
charSequence.toString().toInt().toLong()
}
}
return formatter!!.format(this, type, position, value)
}
/**
* 强制设置的属性直接在构造方法中设置
*
* @param listener listener
*/
class Builder(
private val context: Context,
private val type: Int,
private val onTimeSelectListener: OnTimeSelectListener
) {
// 都应该设置起止时间的,哪怕是只有时间格式,因为真实回调的是时间戳
private var mStartDate: Long = 0 // 默认起始为1970/1/1 8:0:0
private var mEndDate = 4133865600000L // 默认截止为2100/12/31 0:0:0
private var mSelectedDate: Long = -1
private var mFormatter: Formatter? = null
private var mInterceptor: Interceptor? = null
// 时间分钟间隔
private var mTimeMinuteOffset = 1
// 设置mTimeMinuteOffset时是否包含起止时间
private var mContainsStarDate = false
private var mContainsEndDate = false
private var needDialog = true
private var iPickerDialog: IPickerDialog? = null
/**
* 设置起止时间
*
* @param startDate 起始时间
* @param endDate 截止时间
*/
fun setRangDate(startDate: Long, endDate: Long): Builder {
mEndDate = endDate
mStartDate = if (endDate < startDate) {
endDate
} else {
startDate
}
return this
}
/**
* 设置选中时间戳
*
* @param millis 选中时间戳
*/
fun setSelectedDate(millis: Long): Builder {
mSelectedDate = millis
return this
}
/**
* 设置时间间隔分钟数以0为起始边界
*
* @param timeMinuteOffset 60%offset==0才有效
*/
fun setTimeMinuteOffset(timeMinuteOffset: Int): Builder {
mTimeMinuteOffset = timeMinuteOffset
return this
}
/**
* 设置mTimeMinuteOffset作用时是否包含超出的startDate
*
* @param containsStarDate 是否包含startDate
*/
fun setContainsStarDate(containsStarDate: Boolean): Builder {
mContainsStarDate = containsStarDate
return this
}
/**
* 设置mTimeMinuteOffset作用时是否包含超出的endDate
*
* @param containsEndDate 是否包含endDate
*/
fun setContainsEndDate(containsEndDate: Boolean): Builder {
mContainsEndDate = containsEndDate
return this
}
fun setFormatter(formatter: Formatter?): Builder {
mFormatter = formatter
return this
}
fun setInterceptor(interceptor: Interceptor?): Builder {
mInterceptor = interceptor
return this
}
/**
* 自定义弹窗
*
* @param iPickerDialog 如果为null表示不需要弹窗
*/
fun dialog(iPickerDialog: IPickerDialog?): Builder {
needDialog = iPickerDialog != null
this.iPickerDialog = iPickerDialog
return this
}
fun create(): TimePicker {
val picker = TimePicker(context, type, onTimeSelectListener)
// 不支持重复设置的都在builder中控制一次性行为
picker.needDialog = needDialog
picker.iPickerDialog = iPickerDialog
picker.initPickerView()
picker.setInterceptor(mInterceptor)
picker.mTimeMinuteOffset = mTimeMinuteOffset
picker.mContainsStarDate = mContainsStarDate
picker.mContainsEndDate = mContainsEndDate
picker.setRangDate(mStartDate, mEndDate)
if (mFormatter == null) {
mFormatter = DefaultFormatter()
}
picker.formatter = mFormatter
picker.initPicker()
if (mSelectedDate < 0) {
picker.reset()
} else {
picker.setSelectedDate(mSelectedDate)
}
return picker
}
}
open class DefaultFormatter : Formatter {
override fun format(
picker: TimePicker, type: Int, position: Int, value: Long
): CharSequence {
when (type) {
TYPE_YEAR -> {
return value.toString() + ""
}
TYPE_MONTH -> {
return String.format("%02d月", value)
}
TYPE_DAY -> {
return String.format("%02d日", value)
}
TYPE_12_HOUR -> {
return if (value == 0L) "上午" else "下午"
}
TYPE_HOUR -> {
if (picker.hasType(TYPE_12_HOUR)) {
if (value == 0L) {
return "12时"
}
}
return String.format("%2d时", value)
}
TYPE_MINUTE -> {
return String.format("%2d分", value)
}
TYPE_MIXED_DATE -> {
// 如果是TYPE_MIXED_,则value表示时间戳
return sDefaultDateFormat.format(Date(value))
}
TYPE_MIXED_TIME -> {
val time = sDefaultTimeFormat.format(Date(value))
return if (picker.hasType(TYPE_12_HOUR)) {
time.replace("00:", "12:") // 12小时
} else {
time
}
}
else -> return value.toString()
}
}
}
fun interface Formatter {
/**
* 根据type和num格式化时间
*
* @param picker picker
* @param type 并不是模式而是当前item所属的type如年
* @param position position
* @param value position item对应的value如果是TYPE_MIXED_DATE表示日期时间戳否则表示显示的数字
*/
fun format(
picker: TimePicker, type: Int, position: Int, value: Long
): CharSequence
}
fun interface OnTimeSelectListener {
/**
* 点击确定按钮选择时间后回调
*
* @param date 选择的时间
*/
fun onTimeSelect(picker: TimePicker, date: Date)
}
companion object {
const val TYPE_YEAR = 0x01
const val TYPE_MONTH = 0x02
const val TYPE_DAY = 0x04
const val TYPE_HOUR = 0x08
const val TYPE_MINUTE = 0x10
/** 日期聚合 */
const val TYPE_MIXED_DATE = 0x20
/** 时间聚合 */
const val TYPE_MIXED_TIME = 0x40
/** 上午、下午12小时制,默认24小时制不显示上午下午 */
const val TYPE_12_HOUR = 0x80
// 日期:年月日
const val TYPE_DATE = TYPE_YEAR or TYPE_MONTH or TYPE_DAY
// 时间:小时、分钟
const val TYPE_TIME = TYPE_HOUR or TYPE_MINUTE
// 全部
const val TYPE_ALL = TYPE_DATE or TYPE_TIME
var sDefaultDateFormat: DateFormat = createDateFormat("yyyy年MM月dd日")
var sDefaultTimeFormat: DateFormat = createDateFormat("HH:mm")
}
}

View File

@ -0,0 +1,72 @@
package org.jaaksi.pickerview.picker.option
import org.jaaksi.pickerview.adapter.ArrayWheelAdapter
import org.jaaksi.pickerview.dataset.OptionDataSet
import org.jaaksi.pickerview.picker.OptionPicker
/**
* Created by fuchaoyang on 2018/7/6.<br></br>
* description无关联的 OptionPicker Delegate
*/
class ForeignOptionDelegate : IOptionDelegate {
private var mDelegate: OptionPicker.Delegate? = null
private var mOptions: Array<out List<OptionDataSet>>? = null
override fun init(delegate: OptionPicker.Delegate) {
mDelegate = delegate
}
override fun setData(vararg options: List<OptionDataSet>) {
mOptions = options
for (i in 0 until mDelegate!!.hierarchy) {
val pickerView = mDelegate!!.getPickerViews()[i]
pickerView.adapter = ArrayWheelAdapter(mOptions!![i])
}
}
override fun setSelectedWithValues(vararg values: String?) {
for (i in 0 until mDelegate!!.hierarchy) {
if (mOptions == null || mOptions!!.isEmpty()) { // 数据源无效
mDelegate!!.selectedPosition[i] = -1
} else if (values.size <= i || values[i] == null) { // 选中默认项0...
mDelegate!!.selectedPosition[i] = 0
} else {
val options = mOptions!![i]
for (j in 0..options.size) {
// 遍历找到选中的下标如果没有找到则将下标置为0
if (j == options.size) {
mDelegate!!.selectedPosition[i] = 0
break
}
if (values[i] == options[j].getValue()) {
mDelegate!!.selectedPosition[i] = j
break
}
}
}
if (mDelegate!!.selectedPosition[i] != -1) {
mDelegate!!.getPickerViews()[i]
.setSelectedPosition(mDelegate!!.selectedPosition[i], false)
}
}
}
override val selectedOptions: Array<OptionDataSet?>
get() {
val optionDataSets = arrayOfNulls<OptionDataSet>(
mDelegate!!.hierarchy
)
for (i in 0 until mDelegate!!.hierarchy) {
val selectedPosition = mDelegate!!.selectedPosition[i]
if (selectedPosition == -1) break
optionDataSets[i] = mOptions!![i]!![selectedPosition]
}
return optionDataSets
}
override fun reset() {
for (i in 0 until mDelegate!!.hierarchy) {
val pickerView = mDelegate!!.getPickerViews()[i]
pickerView.setSelectedPosition(mDelegate!!.selectedPosition[i], false)
}
}
}

View File

@ -0,0 +1,25 @@
package org.jaaksi.pickerview.picker.option
import org.jaaksi.pickerview.dataset.OptionDataSet
import org.jaaksi.pickerview.picker.OptionPicker
/**
* Created by fuchaoyang on 2018/7/6.<br></br>
* description
*/
interface IOptionDelegate {
fun init(delegate: OptionPicker.Delegate)
fun setData(vararg options: List<OptionDataSet>)
/**
* 根据选中的values初始化选中的position
*/
fun setSelectedWithValues(vararg values: String?)
/**
* 获取选中的选项如果指定index为null则表示该列没有数据
*/
val selectedOptions: Array<OptionDataSet?>
fun reset()
}

View File

@ -0,0 +1,106 @@
package org.jaaksi.pickerview.picker.option
import org.jaaksi.pickerview.adapter.ArrayWheelAdapter
import org.jaaksi.pickerview.dataset.OptionDataSet
import org.jaaksi.pickerview.picker.OptionPicker
/**
* Created by fuchaoyang on 2018/7/6.<br></br>
* description关联的Option Picker Delegate
*/
class OptionDelegate : IOptionDelegate {
private var mDelegate: OptionPicker.Delegate? = null
private var mOptions: List<OptionDataSet>? = null
override fun init(delegate: OptionPicker.Delegate) {
mDelegate = delegate
}
override fun setData(vararg options: List<OptionDataSet>) {
mOptions = options[0]
setSelectedWithValues()
}
/**
* 根据选中的values初始化选中的position
*
* @param values 选中数据的value[OptionDataSet.getValue]如果values[i]==null如果该列有数据则进行默认选中否则认为没有该列
*/
override fun setSelectedWithValues(vararg values: String?) {
var temp = mOptions
for (i in 0 until mDelegate!!.hierarchy) {
val pickerView = mDelegate!!.getPickerViews()[i]
val adapter = pickerView.adapter as ArrayWheelAdapter<*>?
if (adapter == null || adapter.data !== temp) {
pickerView.adapter = ArrayWheelAdapter(temp)
}
if (temp == null || temp.size == 0) { // 数据源无效
mDelegate!!.selectedPosition[i] = -1
} else if (values.size <= i || values[i] == null) { // 选中默认项0...
mDelegate!!.selectedPosition[i] = 0
} else { // 遍历找到选中的下标如果没有找到则将下标置为0
for (j in temp.indices) {
val dataSet = temp[j]
if (values[i] == dataSet.getValue()) {
mDelegate!!.selectedPosition[i] = j
break
}
if (j == temp.size) {
mDelegate!!.selectedPosition[i] = 0
}
}
}
if (mDelegate!!.selectedPosition[i] == -1) {
temp = null
} else {
pickerView.setSelectedPosition(mDelegate!!.selectedPosition[i], false)
val dataSet = temp!![mDelegate!!.selectedPosition[i]]
temp = dataSet.getSubs()
}
}
}
override fun reset() {
var temp = mOptions
for (i in mDelegate!!.getPickerViews().indices) {
val pickerView = mDelegate!!.getPickerViews()[i]
val adapter = pickerView.adapter as ArrayWheelAdapter<*>?
if (adapter == null || adapter.data !== temp) {
pickerView.adapter = ArrayWheelAdapter(temp)
}
// 重置下标
pickerView.setSelectedPosition(mDelegate!!.selectedPosition[i], false)
if (temp.isNullOrEmpty()) {
mDelegate!!.selectedPosition[i] = -1 // 下标置为-1表示选中的第i列没有
} else if (temp.size <= mDelegate!!.selectedPosition[i]) { // 下标超过范围取默认值0
mDelegate!!.selectedPosition[i] = 0
}
if (mDelegate!!.selectedPosition[i] == -1) {
temp = null
} else {
val dataSet = temp!![mDelegate!!.selectedPosition[i]]
temp = dataSet.getSubs()
}
}
}
override val selectedOptions: Array<OptionDataSet?>
/**
* 获取选中的选项
*
* @return 选中的选项如果指定index为null则表示该列没有数据
*/
get() {
val optionDataSets = arrayOfNulls<OptionDataSet>(
mDelegate!!.hierarchy
)
var temp = mOptions
for (i in 0 until mDelegate!!.hierarchy) {
if (mDelegate!!.selectedPosition[i] == -1) break
// !=-1则一定会有数据所以不需要判断temp是否为空也不用担心会下标越界
optionDataSets[i] = temp!![mDelegate!!.selectedPosition[i]]
temp = optionDataSets[i]!!.getSubs()
}
return optionDataSets
}
}

View File

@ -0,0 +1,85 @@
package org.jaaksi.pickerview.util
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
/**
* 创建时间2018年02月02日12:00 <br></br>
* 作者fuchaoyang <br></br>
* 描述时间工具类
*/
object DateUtil {
@JvmStatic
fun createDateFormat(format: String): SimpleDateFormat {
return SimpleDateFormat(format, Locale.getDefault())
}
/**
* 获取某年某月有多少天
*/
@JvmStatic
fun getDayOfMonth(year: Int, month: Int): Int {
val c = Calendar.getInstance()
c[year, month] = 0 //输入类型为int类型
return c[Calendar.DAY_OF_MONTH]
}
// 判断两个时间戳是否是同一天
fun isSameDay(time1: Long, time2: Long): Boolean{
return time1.dayStart() == time2.dayStart()
}
/**
* 不能用时间戳差值 / 86400000, 夏令时会有误差
* @return endTime - startTime 相差的天数
*/
fun getIntervalDay(time1: Long, time2: Long): Int {
val cal1 = Calendar.getInstance().apply { timeInMillis = min(time1,time2) }
val cal2 = Calendar.getInstance().apply { timeInMillis = max(time1, time2) }
// 如果是同一年,直接计算 dayOfYear 差值
if (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)) {
return if (time1 > time2)
-(cal2.get(Calendar.DAY_OF_YEAR) - cal1.get(Calendar.DAY_OF_YEAR))
else
cal2.get(Calendar.DAY_OF_YEAR) - cal1.get(Calendar.DAY_OF_YEAR)
}
// 跨年计算
var daysBetween = 0
// 1. 计算起始年剩余的天数
val daysLeftInYear1 = cal1.getActualMaximum(Calendar.DAY_OF_YEAR) - cal1.get(Calendar.DAY_OF_YEAR)
daysBetween += daysLeftInYear1
// 2. 计算中间完整年份的天数
var year = cal1.get(Calendar.YEAR) + 1
while (year < cal2.get(Calendar.YEAR)) {
val tempCal = Calendar.getInstance().apply { set(Calendar.YEAR, year) }
daysBetween += tempCal.getActualMaximum(Calendar.DAY_OF_YEAR)
year++
}
// 3. 计算结束年已过的天数
daysBetween += cal2.get(Calendar.DAY_OF_YEAR)
return if (time1 > time2) -daysBetween else daysBetween
}
fun Long.toCalendar(): Calendar {
return Calendar.getInstance().apply { timeInMillis = this@toCalendar }
}
fun Long.dayStart(): Long {
val calendar = this.toCalendar()
calendar[Calendar.HOUR_OF_DAY] = 0
calendar[Calendar.MINUTE] = 0
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
return calendar.timeInMillis
}
}

View File

@ -0,0 +1,48 @@
package org.jaaksi.pickerview.util
import android.content.Context
import android.graphics.Color
object Util {
/**
* dip转换px
*
* @param context 上下文
* @param dpValue dip值
* @return px值
*/
@JvmStatic
fun dip2px(context: Context, dpValue: Float): Int {
val scale = context.resources.displayMetrics.density
return (dpValue * scale + 0.5f).toInt()
}
/**
* 计算渐变后的颜色
*
* @param startColor 开始颜色
* @param endColor 结束颜色
* @param rate 渐变率0,1
* @return 渐变后的颜色当rate=0返回startColor当rate=1时返回endColor
*/
@JvmStatic
fun computeGradientColor(startColor: Int, endColor: Int, rate: Float): Int {
var rate = rate
if (rate < 0) {
rate = 0f
}
if (rate > 1) {
rate = 1f
}
val alpha = Color.alpha(endColor) - Color.alpha(startColor)
val red = Color.red(endColor) - Color.red(startColor)
val green = Color.green(endColor) - Color.green(startColor)
val blue = Color.blue(endColor) - Color.blue(startColor)
return Color.argb(
Math.round(Color.alpha(startColor) + alpha * rate),
Math.round(Color.red(startColor) + red * rate),
Math.round(Color.green(startColor) + green * rate),
Math.round(Color.blue(startColor) + blue * rate)
)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,157 @@
package org.jaaksi.pickerview.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import org.jaaksi.pickerview.util.Util.dip2px
import org.jaaksi.pickerview.widget.BasePickerView.CenterDecoration
/**
* 创建时间2018年02月17日10:55 <br></br>
* 作者fuchaoyang <br></br>
* 描述default centerdecoration
* 样式背景图上下两条线支持设置线条颜色宽度margin
*/
class DefaultCenterDecoration(private val mContext: Context) : CenterDecoration {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mDrawable: Drawable? = null
private var marginRect: Rect? = null
private val mRect = Rect()
init {
paint.style = Paint.Style.FILL
setLineWidth(sDefaultLineWidth)
setLineColor(sDefaultLineColor)
setDrawable(sDefaultDrawable)
setMargin(sDefaultMarginRect)
}
/**
* 设置linecolor
*
* @param lineColor line color 如果设置为Color.TRANSPARENT就不绘制线
*/
fun setLineColor(@ColorInt lineColor: Int): DefaultCenterDecoration {
paint.color = lineColor
return this
}
/**
* 设置装饰线宽度
*
* @param lineWidth 装饰线宽度 单位dp
*/
fun setLineWidth(lineWidth: Float): DefaultCenterDecoration {
paint.strokeWidth = dip2px(mContext, lineWidth).toFloat()
return this
}
/**
* 设置CenterDecoration drawable
*/
fun setDrawable(drawable: Drawable?): DefaultCenterDecoration {
mDrawable = drawable
return this
}
fun setDrawable(@ColorInt color: Int): DefaultCenterDecoration {
mDrawable = ColorDrawable(color)
return this
}
/**
* 设置装饰线的margin 单位px
* 水平方向认为left=topmargin,top为rightmargin,right=botommargin,botom=leftmargin
*/
fun setMargin(left: Int, top: Int, right: Int, bottom: Int): DefaultCenterDecoration {
marginRect = Rect(left, top, right, bottom)
return this
}
/**
* 设置装饰线的margin
* 水平方向认为left=topmargin,top为rightmargin,right=botommargin,botom=leftmargin
*/
fun setMargin(marginRect: Rect?): DefaultCenterDecoration {
this.marginRect = marginRect
return this
}
override fun drawIndicator(
pickerView: BasePickerView<*>,
canvas: Canvas,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
if (marginRect == null) {
marginRect = Rect()
}
val isHorizontal = pickerView.isHorizontal
if (mDrawable != null) {
if (!isHorizontal) {
mRect[left + marginRect!!.left, top + marginRect!!.top + (paint.strokeWidth / 2).toInt(), right - marginRect!!.right] =
bottom - marginRect!!.bottom - (paint.strokeWidth / 2).toInt()
} else {
mRect[left + marginRect!!.top + (paint.strokeWidth / 2).toInt(), top + marginRect!!.right, right - marginRect!!.bottom - (paint.strokeWidth / 2).toInt()] =
bottom - marginRect!!.left
}
mDrawable!!.bounds = mRect
mDrawable!!.draw(canvas)
}
if (paint.color == Color.TRANSPARENT) return
if (!isHorizontal) {
canvas.drawLine(
(left + marginRect!!.left).toFloat(),
(top + marginRect!!.top).toFloat(),
(right - marginRect!!.right).toFloat(),
(top + marginRect!!.top).toFloat(),
paint
)
canvas.drawLine(
(left + marginRect!!.left).toFloat(),
(bottom - marginRect!!.bottom).toFloat(),
(right - marginRect!!.right).toFloat(),
(bottom - marginRect!!.bottom).toFloat(),
paint
)
} else {
// 水平方向认为。left=topmargin,top为rightmargin,right=botommargin,botom=leftmargin
canvas.drawLine(
(left + marginRect!!.top).toFloat(),
(top + marginRect!!.right).toFloat(),
(left + marginRect!!.top).toFloat(),
(bottom - marginRect!!.left).toFloat(),
paint
)
canvas.drawLine(
(right - marginRect!!.bottom).toFloat(),
(top + marginRect!!.right).toFloat(),
(right - marginRect!!.bottom).toFloat(),
(bottom - marginRect!!.left).toFloat(),
paint
)
}
}
companion object {
/** default line color */
var sDefaultLineColor = "#ECECEE".toColorInt()
/** default line width */
var sDefaultLineWidth = 1f
/** default item background drawable */
var sDefaultDrawable: Drawable? = null
/** default line margin */
var sDefaultMarginRect: Rect? = null
}
}

View File

@ -0,0 +1,267 @@
package org.jaaksi.pickerview.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.core.graphics.toColorInt
import org.jaaksi.pickerview.R
import org.jaaksi.pickerview.dataset.PickerDataSet
import org.jaaksi.pickerview.util.Util.computeGradientColor
import org.jaaksi.pickerview.util.Util.dip2px
/**
* 字符串滚动选择器
* https://github.com/1993hzw/Androids/blob/master/androids/src/cn/forward/androids/views/StringScrollPicker.java
* 做以下修改
* 1.数据不仅仅支持String支持任意数据方便设置数据
* 2.绘制文字不使用StaticLayout只绘制一行且居中
*
* 其实View不关心泛型需要关心的是Adapter但是我们的adapter是通用的并不需要用户再定义所以无法指明泛型所以不得以view这里依然用泛型
* 这里为了兼容支持直接设置String,int数据
*
* @see PickerDataSet 如果是自定义数据类型请实现该接口
*/
class PickerView<T> @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BasePickerView<T>(context, attrs, defStyleAttr) {
private val mPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
private var outTextSize = 0 // 最小的字体
private var centerTextSize = 0 // 最大的字体
// 字体渐变颜色
private var centerColor = sCenterColor // 中间选中item的颜色
private var outColor = sOutColor // 上下两边的颜色
/**
* 设置对其方式
*
* @param alignment 对齐方式
*/
var alignment = Layout.Alignment.ALIGN_CENTER // 对齐方式,默认居中
private var mShadowColors: IntArray? = sShadowColors
// Shadows drawables
private var mStartShadow: GradientDrawable? = null
private var mEndShadow: GradientDrawable? = null
init {
mPaint.style = Paint.Style.FILL
mPaint.color = Color.BLACK
init(attrs)
}
fun setFont(tf: Typeface){
mPaint.typeface = tf
}
private fun init(attrs: AttributeSet?) {
if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PickerView)
outTextSize =
typedArray.getDimensionPixelSize(R.styleable.PickerView_pv_out_text_size, 0)
centerTextSize =
typedArray.getDimensionPixelSize(R.styleable.PickerView_pv_center_text_size, 0)
centerColor = typedArray.getColor(R.styleable.PickerView_pv_start_color, centerColor)
outColor = typedArray.getColor(R.styleable.PickerView_pv_end_color, outColor)
val align = typedArray.getInt(R.styleable.PickerView_pv_alignment, 1)
if (align == 2) {
alignment = Layout.Alignment.ALIGN_NORMAL
} else if (align == 3) {
alignment = Layout.Alignment.ALIGN_OPPOSITE
} else {
alignment = Layout.Alignment.ALIGN_CENTER
}
typedArray.recycle()
}
if (outTextSize <= 0) {
outTextSize = dip2px(context, sOutTextSize.toFloat())
}
if (centerTextSize <= 0) {
centerTextSize = dip2px(context, sCenterTextSize.toFloat())
}
resetShadow()
}
private fun resetShadow() {
if (mShadowColors == null) {
mStartShadow = null
mEndShadow = null
} else {
if (isHorizontal) {
mStartShadow =
GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mShadowColors)
mEndShadow =
GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mShadowColors)
} else {
mStartShadow =
GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, mShadowColors)
mEndShadow =
GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, mShadowColors)
}
}
}
/**
* 设置蒙版
*/
fun setShadowsColors(@ColorInt colors: IntArray?) {
mShadowColors = colors
resetShadow()
}
/**
* 设置center out 文字 color
*
* @param centerColor 正中间的颜色
* @param outColor 上下两边的颜色
*/
fun setColor(@ColorInt centerColor: Int, @ColorInt outColor: Int) {
this.centerColor = centerColor
this.outColor = outColor
invalidate()
}
/**
* 设置item文字大小单位dp
*
* @param minText 沒有被选中时的最小文字
* @param maxText 被选中时的最大文字
*/
fun setTextSize(minText: Int, maxText: Int) {
outTextSize = dip2px(context, minText.toFloat())
centerTextSize = dip2px(context, maxText.toFloat())
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mShadowColors != null) {
drawShadows(canvas)
}
}
override fun drawItem(
canvas: Canvas?, data: T?, position: Int, relative: Int, moveLength: Float,
top: Float
) {
// 添加一层装饰器
var text: CharSequence? = if (data is PickerDataSet) {
(data as PickerDataSet).getCharSequence()
} else {
data.toString()
}
text = if (formatter == null) text else formatter!!.format(this, position, text)
if (text == null) return
val itemSize = itemSize
// 设置文字大小
if (relative == -1) { // 上一个
if (moveLength < 0) { // 向上滑动
mPaint.textSize = outTextSize.toFloat()
} else { // 向下滑动
mPaint.textSize =
outTextSize + (centerTextSize - outTextSize) * moveLength / itemSize
}
} else if (relative == 0) { // 中间item,当前选中
mPaint.textSize = (outTextSize
+ (centerTextSize - outTextSize) * (itemSize - Math.abs(moveLength)) / itemSize)
} else if (relative == 1) { // 下一个
if (moveLength > 0) { // 向下滑动
mPaint.textSize = outTextSize.toFloat()
} else { // 向上滑动
mPaint.textSize =
outTextSize + (centerTextSize - outTextSize) * -moveLength / itemSize
}
} else { // 其他
mPaint.textSize = outTextSize.toFloat()
}
// 不换行
val layout = StaticLayout(
text, 0, text.length, mPaint, dip2px(
context, 1000f
), alignment,
1.0f, 0.0f, true, null, 0
)
var x = 0f
var y = 0f
val lineWidth = layout.width.toFloat()
if (isHorizontal) { // 水平滚动
x = top + (itemWidth - lineWidth) / 2
y = ((itemHeight - layout.height) / 2).toFloat()
} else { // 垂直滚动
x = (itemWidth - lineWidth) / 2
y = top + (itemHeight - layout.height) / 2
}
// 计算渐变颜色
computeColor(relative, itemSize, moveLength)
canvas!!.save()
canvas.translate(x, y)
layout.draw(canvas)
canvas.restore()
}
/**
* Draws shadows on top and bottom of control
*/
private fun drawShadows(canvas: Canvas) {
val height = itemHeight
mStartShadow!!.setBounds(0, 0, width, height)
mStartShadow!!.draw(canvas)
mEndShadow!!.setBounds(0, getHeight() - height, width, getHeight())
mEndShadow!!.draw(canvas)
}
/**
* 计算字体颜色渐变
* 1.中间区域为 centerColor其他未 outColor 参考AndroidPickers
* 2.如果再当前位置松开手后应该选中的那个item的文字颜色为centerColor,其他为outColor
* 把这个做成接口提供默认实现
*
* @param relative  相对中间item的位置
*/
private fun computeColor(relative: Int, itemSize: Int, moveLength: Float) {
var color = outColor //  其他默认为 mOutColor
if (relative == -1 || relative == 1) { // 上一个或下一个
// 处理上一个item且向上滑动 或者 处理下一个item且向下滑动 颜色为 mOutColor
color = if (relative == -1 && moveLength < 0 || relative == 1 && moveLength > 0) {
outColor
} else { // 计算渐变的颜色
val rate = (itemSize - Math.abs(moveLength)) / itemSize
computeGradientColor(centerColor, outColor, rate)
}
} else if (relative == 0) { // 中间item
val rate = Math.abs(moveLength) / itemSize
color = computeGradientColor(centerColor, outColor, rate)
}
mPaint.color = color
}
companion object {
/** default out text size 18dp */
var sOutTextSize = 18 // dp
/** default center text size 20dp */
var sCenterTextSize = 20 // dp
/** default center text color */
var sCenterColor = "#41bc6a".toColorInt()
/** default out text color */
var sOutColor = "#666666".toColorInt()
/** Top and bottom shadows colors */
var sShadowColors =
intArrayOf(Color.WHITE, "#88ffffff".toColorInt(), "#00FFFFFF".toColorInt())
}
}

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/white">
<TextView
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="取消"
android:textColor="#888888"
android:textSize="16dp" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#253238"
android:textSize="18dp"
tools:text="title" />
<TextView
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right|center_vertical"
android:gravity="center"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="确定"
android:textColor="#41bc6a"
android:textSize="16dp" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_gravity="bottom"
android:background="#e5e5e5" />
</FrameLayout>
</LinearLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 添加线模式 -->
<declare-styleable name="BasePickerView">
<attr name="pv_visible_item_count" format="integer" />
<!-- item尺寸 -->
<attr name="pv_item_size" format="dimension" />
<!-- 中间item的位置,默认为0-->
<attr name="pv_center_item_position" format="integer" />
<!-- 是否循环滚动默认为true开启-->
<attr name="pv_is_circulation" format="boolean" />
<!-- 不允许父组件拦截触摸事件设置为true为不允许拦截此时该设置才生效 -->
<attr name="pv_disallow_intercept_touch" format="boolean" />
<!-- 滚动的方向-->
<attr name="pv_orientation" format="string">
<enum name="horizontal" value="1" />
<enum name="vertical" value="2" />
</attr>
</declare-styleable>
<declare-styleable name="PickerView">
<!-- out min text size -->
<attr name="pv_out_text_size" format="dimension" />
<!-- center max text size -->
<attr name="pv_center_text_size" format="dimension" />
<!-- out text color -->
<attr name="pv_start_color" format="color" />
<!-- center text color -->
<attr name="pv_end_color" format="color" />
<!-- alignment -->
<attr name="pv_alignment" format="enum">
<enum name="center" value="1" />
<enum name="left" value="2" />
<enum name="right" value="3" />
</attr>
</declare-styleable>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">PickerView</string>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="BottomSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
<item name="android:colorBackground">@android:color/transparent</item>
</style>
</resources>

View File

@ -16,9 +16,10 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://maven.aliyun.com/repository/public/") }
}
}
rootProject.name = "FileRecovery"
include(":app")
include(":app",":pickerview")