From 592918d6ce697058154ae4f8a9918e26910114c1 Mon Sep 17 00:00:00 2001 From: litingting Date: Mon, 13 Oct 2025 18:08:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../com/ux/video/file/filerecovery/App.kt | 4 + .../file/filerecovery/main/MainActivity.kt | 14 +- .../photo/DateFilterPopupWindows.kt | 171 +++ .../photo/DatePickerDialogFragment.kt | 143 ++- .../filerecovery/photo/FilterPopupWindows.kt | 41 +- .../photo/PhotoSortingActivity.kt | 180 ++- .../video/file/filerecovery/utils/Common.kt | 44 +- .../file/filerecovery/utils/CustomTextView.kt | 19 +- .../filerecovery/utils/ExtendFunctions.kt | 109 +- .../file/filerecovery/utils/ScanRepository.kt | 2 +- ...og_filter_date_start_selected_d5ebff_8.xml | 7 + ..._filter_date_start_unselected_f2f2f7_8.xml | 7 + .../drawable/bg_rectangle_15787880_center.xml | 7 + .../bg_rectangle_15787880_left_13.xml | 7 + .../bg_rectangle_15787880_right_13.xml | 7 + .../res/drawable/selector_filter_date.xml | 5 + app/src/main/res/layout/activity_main.xml | 1 + .../layout/dialog_filter_customer_date_.xml | 109 ++ .../res/layout/popwindows_filter_date.xml | 203 ++++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/strings.xml | 2 + pickerview/.gitignore | 1 + pickerview/build.gradle | 59 + pickerview/proguard-rules.pro | 21 + pickerview/publish.gradle | 72 ++ pickerview/src/main/AndroidManifest.xml | 2 + .../pickerview/adapter/ArrayWheelAdapter.kt | 17 + .../pickerview/adapter/NumericWheelAdapter.kt | 17 + .../jaaksi/pickerview/adapter/WheelAdapter.kt | 18 + .../pickerview/dataset/OptionDataSet.kt | 12 + .../pickerview/dataset/PickerDataSet.kt | 15 + .../pickerview/dialog/DefaultPickerDialog.kt | 69 ++ .../pickerview/dialog/IGlobalDialogCreator.kt | 10 + .../jaaksi/pickerview/dialog/IPickerDialog.kt | 16 + .../dialog/OnPickerChooseListener.kt | 9 + .../jaaksi/pickerview/picker/BasePicker.kt | 239 ++++ .../jaaksi/pickerview/picker/OptionPicker.kt | 238 ++++ .../jaaksi/pickerview/picker/TimePicker.kt | 934 +++++++++++++++ .../picker/option/ForeignOptionDelegate.kt | 72 ++ .../picker/option/IOptionDelegate.kt | 25 + .../picker/option/OptionDelegate.kt | 106 ++ .../org/jaaksi/pickerview/util/DateUtil.kt | 85 ++ .../java/org/jaaksi/pickerview/util/Util.kt | 48 + .../pickerview/widget/BasePickerView.kt | 1004 +++++++++++++++++ .../widget/DefaultCenterDecoration.kt | 157 +++ .../jaaksi/pickerview/widget/PickerView.kt | 267 +++++ .../res/layout/dialog_pickerview_default.xml | 55 + pickerview/src/main/res/values/attrs.xml | 39 + pickerview/src/main/res/values/strings.xml | 3 + pickerview/src/main/res/values/styles.xml | 7 + settings.gradle.kts | 3 +- 52 files changed, 4597 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/com/ux/video/file/filerecovery/photo/DateFilterPopupWindows.kt create mode 100644 app/src/main/res/drawable/bg_dialog_filter_date_start_selected_d5ebff_8.xml create mode 100644 app/src/main/res/drawable/bg_dialog_filter_date_start_unselected_f2f2f7_8.xml create mode 100644 app/src/main/res/drawable/bg_rectangle_15787880_center.xml create mode 100644 app/src/main/res/drawable/bg_rectangle_15787880_left_13.xml create mode 100644 app/src/main/res/drawable/bg_rectangle_15787880_right_13.xml create mode 100644 app/src/main/res/drawable/selector_filter_date.xml create mode 100644 app/src/main/res/layout/dialog_filter_customer_date_.xml create mode 100644 app/src/main/res/layout/popwindows_filter_date.xml create mode 100644 pickerview/.gitignore create mode 100644 pickerview/build.gradle create mode 100644 pickerview/proguard-rules.pro create mode 100644 pickerview/publish.gradle create mode 100644 pickerview/src/main/AndroidManifest.xml create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/adapter/ArrayWheelAdapter.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/adapter/NumericWheelAdapter.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/adapter/WheelAdapter.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dataset/OptionDataSet.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dataset/PickerDataSet.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dialog/DefaultPickerDialog.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dialog/IGlobalDialogCreator.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dialog/IPickerDialog.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/dialog/OnPickerChooseListener.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/BasePicker.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/OptionPicker.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/TimePicker.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/option/ForeignOptionDelegate.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/option/IOptionDelegate.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/picker/option/OptionDelegate.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/util/DateUtil.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/util/Util.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/widget/BasePickerView.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/widget/DefaultCenterDecoration.kt create mode 100644 pickerview/src/main/java/org/jaaksi/pickerview/widget/PickerView.kt create mode 100644 pickerview/src/main/res/layout/dialog_pickerview_default.xml create mode 100644 pickerview/src/main/res/values/attrs.xml create mode 100644 pickerview/src/main/res/values/strings.xml create mode 100644 pickerview/src/main/res/values/styles.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 25f7dc7..1ed77ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/App.kt b/app/src/main/java/com/ux/video/file/filerecovery/App.kt index 02bab00..ebf4e4a 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/App.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/App.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt index dffc5ed..c598897 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt @@ -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() { - + private var dialogCustomerDateStart:DatePickerDialogFragment? = null private var dialogPermission: PermissionDialogFragment? = null //是否正确引导用户打开所有文件管理权限 private var isRequestPermission = false @@ -94,9 +96,9 @@ class MainActivity : BaseActivity() { intentCheck() } } - binding.btnPermission.setOnClickListener { - + binding.tvTitle.setOnClickListener { + showDatePicker() } binding.btnScanAllPhoto.setOnClickListener { @@ -242,4 +244,10 @@ class MainActivity : BaseActivity() { super.onDestroy() binding.layoutPermission.isVisible = false } + + + fun showDatePicker() { +// dialogCustomerDateStart = dialogCustomerDateStart?: DateFilterPopupWindows(this){} +// dialogCustomerDateStart?.show(supportFragmentManager,"") + } } \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/DateFilterPopupWindows.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/DateFilterPopupWindows.kt new file mode 100644 index 0000000..75cbb9d --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/DateFilterPopupWindows.kt @@ -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, + 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() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt index 63352fb..d791f76 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/DatePickerDialogFragment.kt @@ -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)}") + } + } diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/FilterPopupWindows.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/FilterPopupWindows.kt index 2dac8a4..f69fa5e 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/photo/FilterPopupWindows.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/FilterPopupWindows.kt @@ -12,14 +12,15 @@ 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, - selectedPos: Int, + var selectedPos: Int, onClickConfirm: (value: String) -> Unit, - var onClickDismiss: () -> Unit + var onClickDismiss: () -> Unit ) : PopupWindow(context) { @@ -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) diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt index 7b5a8f2..d3da35f 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt @@ -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() { @@ -47,6 +40,7 @@ class PhotoSortingActivity : BaseActivity() { 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() { 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() { //筛选大小,默认全部-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() { 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() { 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() + } + + } } - startFilter() + + }, onResetDate = { isStart,currentDate -> + if (isStart) { + showStartDateDialog(false,currentDate) + } else { + showEndDateDialog(false,currentDate) + } + }) { + //dismiss + setItemSelect(it, false) } @@ -408,13 +438,18 @@ class PhotoSortingActivity : BaseActivity() { * 执行筛选结果 */ 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() { 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,45 +500,90 @@ class PhotoSortingActivity : BaseActivity() { } - 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){ - complete() - } + dialogRecovering = + dialogRecovering ?: RecoveringDialogFragment(filterSelectedSetList.size) { + complete() + } dialogRecovering?.show(supportFragmentManager, "") } private fun showDeletingDialog() { - dialogDeleting = dialogDeleting ?: DeletingDialogFragment(filterSelectedSetList.size){ + dialogDeleting = dialogDeleting ?: DeletingDialogFragment(filterSelectedSetList.size) { complete() } dialogDeleting?.show(supportFragmentManager, "") } + /** + * 删除或者恢复完成 + */ private fun complete() { dialogDeleting?.dismiss() dialogRecovering?.dismiss() diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt index 2fdc95b..ee1d4ab 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt @@ -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,16 +130,23 @@ object Common { /** * 设置全部子View的选中 */ - fun setItemSelect(view: ViewGroup, boolean: Boolean) { - for (i in 0 until view.childCount) { - val child = view.getChildAt(i) - child.isSelected = 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) + setItemSelect(child, selected) + } } } - - /** * @param list 筛选后的显示的数据 * @param selected 实际选中的数据集合 @@ -161,7 +171,10 @@ object Common { } - fun checkSelectListContainSize(list: List, selected: Set): Pair> { + fun checkSelectListContainSize( + list: List, + selected: Set + ): Pair> { val currentSelected = mutableSetOf() val totalSelectedCount = list.count { val isSelected = it.path in selected @@ -172,8 +185,21 @@ object Common { return totalSelectedCount to currentSelected } - fun getFormatDate(time: Long): String{ - return dateFormat.format(Date(time)) + fun getFormatDate(time: Long): String { + 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) { diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt index bbeeb85..ad7768e 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt @@ -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 } diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt index 676e568..211bcee 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt @@ -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,18 +41,58 @@ object ExtendFunctions { /** * 按时间筛选:最近 N 个月 */ - fun List.filterWithinMonthsList(months: Int): List { - if (months == -1) return this +// fun List.filterWithinMonthsList(months: Int): List { +// 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.filterWithinDateRangeList( + months: Int = -1, + startDate: Date? = null, + endDate: Date? = null + ): List { + 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) + + 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) + } + + 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>>.filterWithinMonths(months: Int): List>> { - if (months == -1) return this +// fun List>>.filterWithinMonths(months: Int): List>> { +// 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>>.filterWithinDateRange( + months: Int = -1, + startDate: Date? = null, + endDate: Date? = null + ): List>> { + 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) + } } } + + + + + + /** * 分组数据:按大小筛选 */ diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt index 0cf6e24..ec2df5b 100644 --- a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt @@ -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"))) } diff --git a/app/src/main/res/drawable/bg_dialog_filter_date_start_selected_d5ebff_8.xml b/app/src/main/res/drawable/bg_dialog_filter_date_start_selected_d5ebff_8.xml new file mode 100644 index 0000000..2ac601d --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog_filter_date_start_selected_d5ebff_8.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dialog_filter_date_start_unselected_f2f2f7_8.xml b/app/src/main/res/drawable/bg_dialog_filter_date_start_unselected_f2f2f7_8.xml new file mode 100644 index 0000000..cde792a --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog_filter_date_start_unselected_f2f2f7_8.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rectangle_15787880_center.xml b/app/src/main/res/drawable/bg_rectangle_15787880_center.xml new file mode 100644 index 0000000..632c2f5 --- /dev/null +++ b/app/src/main/res/drawable/bg_rectangle_15787880_center.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rectangle_15787880_left_13.xml b/app/src/main/res/drawable/bg_rectangle_15787880_left_13.xml new file mode 100644 index 0000000..1cec049 --- /dev/null +++ b/app/src/main/res/drawable/bg_rectangle_15787880_left_13.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rectangle_15787880_right_13.xml b/app/src/main/res/drawable/bg_rectangle_15787880_right_13.xml new file mode 100644 index 0000000..0e1a502 --- /dev/null +++ b/app/src/main/res/drawable/bg_rectangle_15787880_right_13.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_filter_date.xml b/app/src/main/res/drawable/selector_filter_date.xml new file mode 100644 index 0000000..199e055 --- /dev/null +++ b/app/src/main/res/drawable/selector_filter_date.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1dc45fd..c93416c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -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"> diff --git a/app/src/main/res/layout/dialog_filter_customer_date_.xml b/app/src/main/res/layout/dialog_filter_customer_date_.xml new file mode 100644 index 0000000..443ef8f --- /dev/null +++ b/app/src/main/res/layout/dialog_filter_customer_date_.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popwindows_filter_date.xml b/app/src/main/res/layout/popwindows_filter_date.xml new file mode 100644 index 0000000..0eed862 --- /dev/null +++ b/app/src/main/res/layout/popwindows_filter_date.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 742d8fe..eb2472f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -21,5 +21,9 @@ #0048FD #fdad00 #6BFFFFFF + #15787880 + #F2F2F7 + #D9D9D9 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a2c423..b39116a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,8 @@ wait.. Recovered successfully! Deleted successfully! Continue + Start date + End date All diff --git a/pickerview/.gitignore b/pickerview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/pickerview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/pickerview/build.gradle b/pickerview/build.gradle new file mode 100644 index 0000000..a5193c9 --- /dev/null +++ b/pickerview/build.gradle @@ -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' +} diff --git a/pickerview/proguard-rules.pro b/pickerview/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/pickerview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/pickerview/publish.gradle b/pickerview/publish.gradle new file mode 100644 index 0000000..9980cca --- /dev/null +++ b/pickerview/publish.gradle @@ -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 task,所以这里执行clean之后主动执行以下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 + } + } + } +} \ No newline at end of file diff --git a/pickerview/src/main/AndroidManifest.xml b/pickerview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bf03204 --- /dev/null +++ b/pickerview/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/adapter/ArrayWheelAdapter.kt b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/ArrayWheelAdapter.kt new file mode 100644 index 0000000..c721d70 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/ArrayWheelAdapter.kt @@ -0,0 +1,17 @@ +package org.jaaksi.pickerview.adapter + +/** + * The simple Array wheel adapter + * 数据实现 [PickerDataSet]即可 + * + * @param the element type + */ +class ArrayWheelAdapter(val data: List?) : WheelAdapter { + + override fun getItem(index: Int): T? { + return data?.getOrNull(index) + } + + override val itemCount: Int + get() = data?.size ?: 0 +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/adapter/NumericWheelAdapter.kt b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/NumericWheelAdapter.kt new file mode 100644 index 0000000..6d19233 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/NumericWheelAdapter.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/adapter/WheelAdapter.kt b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/WheelAdapter.kt new file mode 100644 index 0000000..7a54553 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/adapter/WheelAdapter.kt @@ -0,0 +1,18 @@ +package org.jaaksi.pickerview.adapter + +interface WheelAdapter { + /** + * 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? +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dataset/OptionDataSet.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dataset/OptionDataSet.kt new file mode 100644 index 0000000..ad69249 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dataset/OptionDataSet.kt @@ -0,0 +1,12 @@ +package org.jaaksi.pickerview.dataset + +/** + * Created by fuchaoyang on 2018/2/11.

+ * description:[OptionPicker]专用数据集 + */ +interface OptionDataSet : PickerDataSet { + /** + * @return 下一级的数据集 + */ + fun getSubs(): List? +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dataset/PickerDataSet.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dataset/PickerDataSet.kt new file mode 100644 index 0000000..ff498b1 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dataset/PickerDataSet.kt @@ -0,0 +1,15 @@ +package org.jaaksi.pickerview.dataset + +/** + * 创建时间:2018年01月31日16:34

+ * 作者:fuchaoyang

+ * 描述:数据实现接口,用户显示文案 + */ +interface PickerDataSet { + fun getCharSequence(): CharSequence? + + /** + * @return 上传的value,用于匹配初始化选中的下标 + */ + fun getValue(): String? +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dialog/DefaultPickerDialog.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/DefaultPickerDialog.kt new file mode 100644 index 0000000..3e5ee49 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/DefaultPickerDialog.kt @@ -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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IGlobalDialogCreator.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IGlobalDialogCreator.kt new file mode 100644 index 0000000..78b0931 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IGlobalDialogCreator.kt @@ -0,0 +1,10 @@ +package org.jaaksi.pickerview.dialog + +import android.content.Context + +interface IGlobalDialogCreator { + /** + * 创建IPickerDialog + */ + fun create(context: Context): IPickerDialog +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IPickerDialog.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IPickerDialog.kt new file mode 100644 index 0000000..eade788 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/IPickerDialog.kt @@ -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() +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/dialog/OnPickerChooseListener.kt b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/OnPickerChooseListener.kt new file mode 100644 index 0000000..eca2e4d --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/dialog/OnPickerChooseListener.kt @@ -0,0 +1,9 @@ +package org.jaaksi.pickerview.dialog + +interface OnPickerChooseListener { + /** + * @return 是否回调选中关闭dialog + */ + fun onConfirm(): Boolean + fun onCancel() +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/BasePicker.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/BasePicker.kt new file mode 100644 index 0000000..8167c9a --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/BasePicker.kt @@ -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

+ * 作者:fuchaoyang

+ * 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? = null + private val mPickerViews: MutableList> = 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> + get() = mPickerViews + + /** + * 如果使用[.createPickerView]创建pickerview,就不需要手动添加 + * + * @param pickerView pickerView + */ + protected fun addPicker(pickerView: PickerView<*>) { + mPickerViews.add(pickerView) + } + + /** + * 创建pickerview + * + * @param tag settag + * @param weight 权重 + */ + protected fun createPickerView(tag: Any?, weight: Float): PickerView { + val pickerView: PickerView = 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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/OptionPicker.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/OptionPicker.kt new file mode 100644 index 0000000..bebe9c8 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/OptionPicker.kt @@ -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.

+ * 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(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> { + return pickerViews as List> + } + + }) + } + + /** + * 根据选中的values初始化选中的position并初始化pickerview数据 + * + * @param options data + */ + fun setData(vararg options: List) { + // 初始化是否关联 + 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 + /** + * 获取选中的选项 + * + * @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 + ) + } + + interface Delegate { + val hierarchy: Int + val selectedPosition: IntArray + fun getPickerViews(): List> + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/TimePicker.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/TimePicker.kt new file mode 100644 index 0000000..9de96b6 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/TimePicker.kt @@ -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

+ * 作者:fuchaoyang

+ * 描述:时间选择器 + * 强大点: + * 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? = null + private var mYearPicker: PickerView? = null + private var mMonthPicker: PickerView? = null + private var mDayPicker: PickerView? = null + private var mTimePicker: PickerView? = null + private var mHourPicker: PickerView? = null + private var mNoonPicker: PickerView? = null + private var mMinutePicker: PickerView? = 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 = 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") + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/ForeignOptionDelegate.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/ForeignOptionDelegate.kt new file mode 100644 index 0000000..665f0e2 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/ForeignOptionDelegate.kt @@ -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.

+ * description:无关联的 OptionPicker Delegate + */ +class ForeignOptionDelegate : IOptionDelegate { + private var mDelegate: OptionPicker.Delegate? = null + private var mOptions: Array>? = null + override fun init(delegate: OptionPicker.Delegate) { + mDelegate = delegate + } + + override fun setData(vararg options: List) { + 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 + get() { + val optionDataSets = arrayOfNulls( + 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) + } + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/IOptionDelegate.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/IOptionDelegate.kt new file mode 100644 index 0000000..2a83cff --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/IOptionDelegate.kt @@ -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.

+ * description: + */ +interface IOptionDelegate { + fun init(delegate: OptionPicker.Delegate) + + fun setData(vararg options: List) + + /** + * 根据选中的values初始化选中的position + */ + fun setSelectedWithValues(vararg values: String?) + + /** + * 获取选中的选项,如果指定index为null则表示该列没有数据 + */ + val selectedOptions: Array + fun reset() +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/OptionDelegate.kt b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/OptionDelegate.kt new file mode 100644 index 0000000..305be00 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/picker/option/OptionDelegate.kt @@ -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.

+ * description:关联的Option Picker Delegate + */ +class OptionDelegate : IOptionDelegate { + private var mDelegate: OptionPicker.Delegate? = null + private var mOptions: List? = null + + override fun init(delegate: OptionPicker.Delegate) { + mDelegate = delegate + } + + override fun setData(vararg options: List) { + 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 + /** + * 获取选中的选项 + * + * @return 选中的选项,如果指定index为null则表示该列没有数据 + */ + get() { + val optionDataSets = arrayOfNulls( + 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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/util/DateUtil.kt b/pickerview/src/main/java/org/jaaksi/pickerview/util/DateUtil.kt new file mode 100644 index 0000000..21fc701 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/util/DateUtil.kt @@ -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

+ * 作者:fuchaoyang

+ * 描述:时间工具类 + */ +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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/util/Util.kt b/pickerview/src/main/java/org/jaaksi/pickerview/util/Util.kt new file mode 100644 index 0000000..bcd91d8 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/util/Util.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/widget/BasePickerView.kt b/pickerview/src/main/java/org/jaaksi/pickerview/widget/BasePickerView.kt new file mode 100644 index 0000000..e9164de --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/widget/BasePickerView.kt @@ -0,0 +1,1004 @@ +package org.jaaksi.pickerview.widget + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.view.View +import android.view.animation.Interpolator +import android.widget.Scroller +import org.jaaksi.pickerview.R +import org.jaaksi.pickerview.adapter.WheelAdapter +import org.jaaksi.pickerview.util.Util.dip2px + +/** + * 滚动选择器,带惯性滑动 + * https://github.com/1993hzw/Androids/blob/master/androids/src/cn/forward/androids/views/ScrollPickerView.java + * 做一下修改: + * 改为adapter填充数据,不再直接持有数据源。提供两种常用adapter + * 增加属性itemSize,支持高度为wrap_content时,根据itemCount和visibleItemCount计算总高度,便于动态改变visibleItemCount + * 提供接口Formatter 外界可以对显示的文案处理。比如添加把2018变成2018年,8变成 08月,MixedTimePicker中用处更大 + * 绘制中心item由绘制drawable改为由接口CenterDecoration控制,提供默认实现。用户可以自定义,更强大,使用更方便 + * 修改数据源size < visibleItemCount时强制不进行循环 + * 修改默认选中第0个 + */ +abstract class BasePickerView @JvmOverloads constructor( + context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + private var mVisibleItemCount = sDefaultVisibleItemCount // 可见的item数量 + + /** + * 设置快速滑动时是否惯性滚动一段距离 + * + * @param inertiaScroll 快速滑动时是否惯性滚动一段距离 + */ + var isInertiaScroll = true // 快速滑动时是否惯性滚动一段距离,默认开启 + private var mIsCirculation = false // 是否循环滚动,默认关闭 + var isIsCirculation = false // 是否有必要循环滚动 + private set + + /** + * 是否允许父元素拦截事件,设置true后可以保证在ScrollView下正常滚动 + *//* + 不允许父组件拦截触摸事件,设置为true为不允许拦截,此时该设置才生效 + 当嵌入到ScrollView等滚动组件中,为了使该自定义滚动选择器可以正常工作,请设置为true + */ + var isDisallowInterceptTouch = false + private var mSelected = 0 // 当前选中的item下标 + protected var mAdapter: WheelAdapter? = null + var itemHeight = 0 // 每个条目的高度,当垂直滚动时,高度=mMeasureHeight/mVisibleItemCount + private set + var itemWidth = 0 // 每个条目的宽度,当水平滚动时,宽度=mMeasureWidth/mVisibleItemCount + private set + private var mItemSize = 0 // 当垂直滚动时,mItemSize = mItemHeight;水平滚动时,mItemSize = mItemWidth + + // 标记是否使用默认的 centerPosition = mVisibleItemCount / 2 + private var mUseDefaultCenterPosition = true + private var mCenterPosition = -1 + + /** + * @return 中间item的起始坐标y(不考虑偏移), 当垂直滚动时,y= mCenterPosition*mItemHeight + */ + // 中间item的位置,0<=mCenterPosition<mVisibleItemCount,默认为 mVisibleItemCount / 2 + var centerY = 0 // 中间item的起始坐标y(不考虑偏移),当垂直滚动时,y= mCenterPosition*mItemHeight + private set + + /** + * @return 中间item的起始坐标x(不考虑偏移), 当垂直滚动时,x = mCenterPosition*mItemWidth + */ + var centerX = 0 // 中间item的起始坐标x(不考虑偏移),当垂直滚动时,x = mCenterPosition*mItemWidth + private set + + /** + * @return 当垂直滚动时,mCenterPoint = mCenterY;水平滚动时,mCenterPoint = mCenterX + */ + var centerPoint = 0 // 当垂直滚动时,mCenterPoint = mCenterY;水平滚动时,mCenterPoint = mCenterX + private set + private var mLastMoveY = 0f // 触摸的坐标y + private var mLastMoveX = 0f // 触摸的坐标X + private var mMoveLength = 0f // item移动长度,负数表示向上移动,正数表示向下移动 + private val mGestureDetector: GestureDetector + var listener: OnSelectedListener? = null + private set + + /** + * 设置内容Formatter + * + * @param formatter formatter + */ + var formatter: Formatter? = null + private val mScroller: Scroller + var isFling = false // 是否正在惯性滑动 + private set + var isMovingCenter = false // 是否正在滑向中间 + private set + + // 可以把scroller看做模拟的触屏滑动操作,mLastScrollY为上次触屏滑动的坐标 + private var mLastScrollY = 0 // Scroller的坐标y + private var mLastScrollX = 0 // Scroller的坐标x + + /** + * 设置是否允许手动触摸滚动 + */ + var isDisallowTouch = false // 不允许触摸 + private var isTouching = false //手指按下 + private var mSelectedOnTouch = 0 + private val mPaint: Paint + private var mCenterDecoration: CenterDecoration? = null + + /** 是否绘制 CenterDecoration */ + private var mDrawIndicator = sDefaultDrawIndicator + + // 当没有数据时是否绘制指示器 + private var mDrawIndicatorNoData = DEFAULT_DRAW_INDICATOR_NO_DATA + + /** + * 设置 单击切换选项或触发点击监听器 + */ + var isCanTap = true // 单击切换选项或触发点击监听器 + private var mIsHorizontal = false // 是否水平滚动 + private fun init(attrs: AttributeSet?) { + if (attrs != null) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BasePickerView) + mVisibleItemCount = typedArray.getInt( + R.styleable.BasePickerView_pv_visible_item_count, sDefaultVisibleItemCount + ) + mItemSize = typedArray.getDimensionPixelSize(R.styleable.BasePickerView_pv_item_size, 0) + val centerPosition = + typedArray.getInt(R.styleable.BasePickerView_pv_center_item_position, -1) + if (centerPosition != -1) { + setSafeCenterPosition(centerPosition) + } + setIsCirculation( + typedArray.getBoolean( + R.styleable.BasePickerView_pv_is_circulation, sDefaultIsCirculation + ) + ) + isDisallowInterceptTouch = typedArray.getBoolean( + R.styleable.BasePickerView_pv_disallow_intercept_touch, isDisallowInterceptTouch + ) + mIsHorizontal = typedArray.getInt( + R.styleable.BasePickerView_pv_orientation, if (mIsHorizontal) 1 else 2 + ) == 1 + typedArray.recycle() + } else { + setIsCirculation(sDefaultIsCirculation) + } + if (mItemSize == 0) mItemSize = dip2px(context, sDefaultItemSize.toFloat()) + } + + /** + * 设置 中心装饰 + * + * @param centerDecoration 中心装饰 + */ + fun setCenterDecoration(centerDecoration: CenterDecoration?) { + mCenterDecoration = centerDecoration + } + + /** + * 设置是否绘制指示器 + * + * @param drawIndicator 是否绘制指示器 + */ + fun setDrawIndicator(drawIndicator: Boolean) { + mDrawIndicator = drawIndicator + } + + /** + * 设置没有数据时是否绘制指示器 + * + * @param drawIndicatorNoData 没有数据时是否绘制指示器 + */ + fun setDrawIndicatorNoData(drawIndicatorNoData: Boolean) { + mDrawIndicatorNoData = drawIndicatorNoData + } + + override fun onDraw(canvas: Canvas) { + val noData = mAdapter == null || mAdapter!!.itemCount <= 0 + if (mDrawIndicator && (!noData || mDrawIndicatorNoData)) { + if (mCenterDecoration == null) { + mCenterDecoration = DefaultCenterDecoration(context) + } + mCenterDecoration!!.drawIndicator( + this, canvas, centerX, centerY, centerX + itemWidth, centerY + itemHeight + ) + } + if (noData) return + isIsCirculation = mIsCirculation && mVisibleItemCount < mAdapter!!.itemCount + + // 1.只绘制可见的item,找到绘制的起始点 + // 比较头和尾找距离中心点较远的 + val length = Math.max(mCenterPosition + 1, mVisibleItemCount - mCenterPosition) + var position: Int + //int start = Math.min(length, mAdapter.getItemCount()); + val start: Int + // fix 当itemcount <= visibleCount时,设置循环也不进行循环绘制 + start = if (isIsCirculation) { + length + } else { + Math.min(length, mAdapter!!.itemCount) + } + + // 2.绘制mCenterPoint上下两边的item:当前选中的绘制在mCenter + for (i in start downTo 1) { // 先从远离中间位置的item绘制,当item内容偏大时,较近的item覆盖在较远的上面 + if (i <= mCenterPosition + 1) { // 上面的items,相对位置为 -i + // 根据是否循环,计算出偏离mCenterPoint i个item对应的position + position = + if (mSelected - i < 0) mAdapter!!.itemCount + mSelected - i else mSelected - i + // 传入位置信息,绘制item + if (isIsCirculation) { + drawItem( + canvas, + mAdapter!!.getItem(position), + position, + -i, + mMoveLength, + centerPoint + mMoveLength - i * mItemSize + ) + } else if (mSelected - i >= 0) { // 非循环滚动 + // 如果当前选中的下标 < 偏离mCenter i个距离的,就不绘制了 + drawItem( + canvas, + mAdapter!!.getItem(position), + position, + -i, + mMoveLength, + centerPoint + mMoveLength - i * mItemSize + ) + } + } + if (i <= mVisibleItemCount - mCenterPosition) { // 下面的items,相对位置为 i + position = + if (mSelected + i >= mAdapter!!.itemCount) mSelected + i - mAdapter!!.itemCount else mSelected + i + // 传入位置信息,绘制item + if (isIsCirculation) { + drawItem( + canvas, + mAdapter!!.getItem(position), + position, + i, + mMoveLength, + centerPoint + mMoveLength + i * mItemSize + ) + } else if (mSelected + i < mAdapter!!.itemCount) { // 非循环滚动 + // 如果当前选中的下标 + 偏移mCenter i个距离的 > 数据个数,也不绘制 + drawItem( + canvas, + mAdapter!!.getItem(position), + position, + i, + mMoveLength, + centerPoint + mMoveLength + i * mItemSize + ) + } + } + } + // 选中的item + drawItem( + canvas, + mAdapter!!.getItem(mSelected), + mSelected, + 0, + mMoveLength, + centerPoint + mMoveLength + ) + } + + /** + * 绘制item + * + * @param data  数据集 + * @param position 在data数据集中的位置 + * @param relative 相对中间item的位置,relative==0表示中间item,relative<0表示上(左)边的item,relative>0表示下(右)边的item + * @param moveLength 中间item滚动的距离,moveLength<0则表示向上(右)滚动的距离,moveLength>0则表示向下(左)滚动的距离 + * @param top 当前绘制item的坐标,当垂直滚动时为顶部y的坐标;当水平滚动时为item最左边x的坐标 + */ + abstract fun drawItem( + canvas: Canvas?, data: T?, position: Int, relative: Int, moveLength: Float, top: Float + ) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // 根据方向判断是不是MeasureSpec.EXACTLY的,如vertical,height=match_parent则根据高度计算itemSize,否则根据itemSize设置高度 + var widthMeasureSpec = widthMeasureSpec + var heightMeasureSpec = heightMeasureSpec + if (mIsHorizontal) { + if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + mItemSize = MeasureSpec.getSize(widthMeasureSpec) / mVisibleItemCount + } else { + widthMeasureSpec = + MeasureSpec.makeMeasureSpec(mItemSize * mVisibleItemCount, MeasureSpec.EXACTLY) + } + } else { + // 如果高度为MeasureSpec.EXACTLY,则size=height/count + if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { + mItemSize = MeasureSpec.getSize(heightMeasureSpec) / mVisibleItemCount + } else { + heightMeasureSpec = + MeasureSpec.makeMeasureSpec(mItemSize * mVisibleItemCount, MeasureSpec.EXACTLY) + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + // 测量结果之后的尺寸才是有效的,调用reset重新计算 + reset() + } + + private fun reset() { + // bug fix 使用标记,避免 default和用户设置的相互覆盖 + if (mUseDefaultCenterPosition) { + mCenterPosition = mVisibleItemCount / 2 + } + if (mIsHorizontal) { + itemHeight = measuredHeight + itemWidth = mItemSize + centerY = 0 + centerX = mCenterPosition * itemWidth + centerPoint = centerX + } else { + itemHeight = mItemSize + itemWidth = measuredWidth + centerY = mCenterPosition * itemHeight + centerX = 0 + centerPoint = centerY + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (isDisallowTouch) { // 不允许触摸 + return true + } + if (mAdapter == null || mAdapter!!.itemCount <= 0) { + return false + } + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + mSelectedOnTouch = mSelected + } + if (mGestureDetector.onTouchEvent(event)) { + return true + } + when (event.actionMasked) { + MotionEvent.ACTION_MOVE -> { + isTouching = true + mMoveLength += if (mIsHorizontal) { + if (Math.abs(event.x - mLastMoveX) < 0.1f) { + return true + } + event.x - mLastMoveX + } else { + if (Math.abs(event.y - mLastMoveY) < 0.1f) { + return true + } + event.y - mLastMoveY + } + mLastMoveY = event.y + mLastMoveX = event.x + checkCirculation() + invalidate() + } + + MotionEvent.ACTION_UP -> { + isTouching = false + mLastMoveY = event.y + mLastMoveX = event.x + if (mMoveLength == 0f) { + if (mSelectedOnTouch != mSelected) { //前后发生变化 + notifySelected() + } + } else { + moveToCenter() // 滚动到中间位置 + } + } + + MotionEvent.ACTION_CANCEL -> isTouching = false + } + return true + } + + /** + * + */ + private fun computeScroll(curr: Int, end: Int, rate: Float) { + if (rate < 1) { // 正在滚动 + if (mIsHorizontal) { + // 可以把scroller看做模拟的触屏滑动操作,mLastScrollX为上次滑动的坐标 + mMoveLength = mMoveLength + curr - mLastScrollX + mLastScrollX = curr + } else { + // 可以把scroller看做模拟的触屏滑动操作,mLastScrollY为上次滑动的坐标 + mMoveLength = mMoveLength + curr - mLastScrollY + mLastScrollY = curr + } + checkCirculation() + invalidate() + } else { // 滚动完毕 + isMovingCenter = false + mLastScrollY = 0 + mLastScrollX = 0 + + // 直接居中,不通过动画 + mMoveLength = if (mMoveLength > 0) { //// 向下滑动 + if (mMoveLength < mItemSize / 2) { + 0f + } else { + mItemSize.toFloat() + } + } else { + if (-mMoveLength < mItemSize / 2) { + 0f + } else { + -mItemSize.toFloat() + } + } + checkCirculation() + notifySelected() + invalidate() + } + } + + override fun computeScroll() { + if (mScroller.computeScrollOffset()) { // 正在滚动 + mMoveLength = if (mIsHorizontal) { + // 可以把scroller看做模拟的触屏滑动操作,mLastScrollX为上次滑动的坐标 + mMoveLength + mScroller.currX - mLastScrollX + } else { + // 可以把scroller看做模拟的触屏滑动操作,mLastScrollY为上次滑动的坐标 + mMoveLength + mScroller.currY - mLastScrollY + } + mLastScrollY = mScroller.currY + mLastScrollX = mScroller.currX + checkCirculation() // 检测当前选中的item + invalidate() + } else { // 滚动完毕 + if (isFling) { + isFling = false + if (equalsFloat(mMoveLength.toDouble(), 0.0)) { //惯性滑动后的位置刚好居中的情况 + notifySelected() + } else { + moveToCenter() // 滚动到中间位置 + } + } else if (isMovingCenter) { // 选择完成,回调给监听器 + notifySelected() + } + } + } + + fun cancelScroll() { + mLastScrollY = 0 + mLastScrollX = 0 + isMovingCenter = false + isFling = isMovingCenter + mScroller.abortAnimation() + stopAutoScroll() + } + + // 检测当前选择的item位置 + private fun checkCirculation() { + if (mMoveLength >= mItemSize) { // 向下滑动 + // 该次滚动距离中越过的item数量 + val span = (mMoveLength / mItemSize).toInt() + mSelected -= span + if (mSelected < 0) { // 滚动顶部,判断是否循环滚动 + if (isIsCirculation) { + do { + mSelected = mAdapter!!.itemCount + mSelected + } while (mSelected < 0) // 当越过的item数量超过一圈时 + mMoveLength = (mMoveLength - mItemSize) % mItemSize + } else { // 非循环滚动 + mSelected = 0 + mMoveLength = mItemSize.toFloat() + if (isFling) { // 停止惯性滑动,根据computeScroll()中的逻辑,下一步将调用moveToCenter() + mScroller.forceFinished(true) + } + if (isMovingCenter) { // 移回中间位置 + scroll(mMoveLength, 0) + } + } + } else { + mMoveLength = (mMoveLength - mItemSize) % mItemSize + } + } else if (mMoveLength <= -mItemSize) { // 向上滑动 + // 该次滚动距离中越过的item数量 + val span = (-mMoveLength / mItemSize).toInt() + mSelected += span + if (mSelected >= mAdapter!!.itemCount) { // 滚动末尾,判断是否循环滚动 + if (isIsCirculation) { + do { + mSelected = mSelected - mAdapter!!.itemCount + } while (mSelected >= mAdapter!!.itemCount) // 当越过的item数量超过一圈时 + mMoveLength = (mMoveLength + mItemSize) % mItemSize + } else { // 非循环滚动 + mSelected = mAdapter!!.itemCount - 1 + mMoveLength = -mItemSize.toFloat() + if (isFling) { // 停止惯性滑动,根据computeScroll()中的逻辑,下一步将调用moveToCenter() + mScroller.forceFinished(true) + } + if (isMovingCenter) { // 移回中间位置 + scroll(mMoveLength, 0) + } + } + } else { + mMoveLength = (mMoveLength + mItemSize) % mItemSize + } + } + } + + // 移动到中间位置 + private fun moveToCenter() { + if (!mScroller.isFinished || isFling || mMoveLength == 0f) { + return + } + cancelScroll() + + // 向下滑动 + if (mMoveLength > 0) { + if (mIsHorizontal) { + if (mMoveLength < itemWidth / 2) { + scroll(mMoveLength, 0) + } else { + scroll(mMoveLength, itemWidth) + } + } else { + if (mMoveLength < itemHeight / 2) { + scroll(mMoveLength, 0) + } else { + scroll(mMoveLength, itemHeight) + } + } + } else { + if (mIsHorizontal) { + if (-mMoveLength < itemWidth / 2) { + scroll(mMoveLength, 0) + } else { + scroll(mMoveLength, -itemWidth) + } + } else { + if (-mMoveLength < itemHeight / 2) { + scroll(mMoveLength, 0) + } else { + scroll(mMoveLength, -itemHeight) + } + } + } + } + + // 平滑滚动 + private fun scroll(from: Float, to: Int) { + if (mIsHorizontal) { + mLastScrollX = from.toInt() + isMovingCenter = true + mScroller.startScroll(from.toInt(), 0, 0, 0) + mScroller.finalX = to + } else { + mLastScrollY = from.toInt() + isMovingCenter = true + mScroller.startScroll(0, from.toInt(), 0, 0) + mScroller.finalY = to + } + invalidate() + } + + // 惯性滑动, + private fun fling(from: Float, vel: Float) { + if (mIsHorizontal) { + mLastScrollX = from.toInt() + isFling = true + // 最多可以惯性滑动10个item,这个数值越大,滑动越快 + mScroller.fling(from.toInt(), 0, vel.toInt(), 0, -10 * itemWidth, 10 * itemWidth, 0, 0) + } else { + mLastScrollY = from.toInt() + isFling = true + // 最多可以惯性滑动10个item + mScroller.fling( + 0, from.toInt(), 0, vel.toInt(), 0, 0, -10 * itemHeight, 10 * itemHeight + ) + } + invalidate() + } + + private fun notifySelected() { + mMoveLength = 0f + cancelScroll() + if (listener != null) { + // 告诉监听器选择完毕 + listener!!.onSelected(this@BasePickerView, mSelected) + } + } + + var isAutoScrolling = false + private set + private val mAutoScrollAnimator: ValueAnimator + + init { + mGestureDetector = GestureDetector(getContext(), FlingOnGestureListener()) + mScroller = Scroller(getContext()) + mAutoScrollAnimator = ValueAnimator.ofInt(0, 0) + mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mPaint.style = Paint.Style.FILL + init(attrs) + } + /** + * 自动滚动(必须设置为可循环滚动) + * + * @param speed 每毫秒移动的像素点 + */ + /** + * 自动滚动 + * + * @see BasePickerView.autoScrollFast + */ + @JvmOverloads + fun autoScrollFast( + position: Int, + duration: Long, + speed: Float, + interpolator: Interpolator? = sAutoScrollInterpolator + ) { + if (isAutoScrolling || !isIsCirculation) { + return + } + cancelScroll() + isAutoScrolling = true + val length = (speed * duration).toInt() + var circle = (length * 1f / (mAdapter!!.itemCount * mItemSize) + 0.5f).toInt() // 圈数 + circle = if (circle <= 0) 1 else circle + val aPlan = circle * mAdapter!!.itemCount * mItemSize + (mSelected - position) * mItemSize + val bPlan = aPlan + mAdapter!!.itemCount * mItemSize // 多一圈 + // 让其尽量接近length + val end = if (Math.abs(length - aPlan) < Math.abs(length - bPlan)) aPlan else bPlan + mAutoScrollAnimator.cancel() + mAutoScrollAnimator.setIntValues(0, end) + mAutoScrollAnimator.interpolator = interpolator + mAutoScrollAnimator.duration = duration + mAutoScrollAnimator.removeAllUpdateListeners() + if (end != 0) { // itemHeight为0导致endy=0 + mAutoScrollAnimator.addUpdateListener { animation -> + var rate = 0f + rate = animation.currentPlayTime * 1f / animation.duration + computeScroll(animation.animatedValue as Int, end, rate) + } + mAutoScrollAnimator.removeAllListeners() + mAutoScrollAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + isAutoScrolling = false + } + }) + mAutoScrollAnimator.start() + } else { + computeScroll(end, end, 1f) + isAutoScrolling = false + } + } + + /** + * 自动滚动,默认速度为 0.6dp/ms + * + * @see BasePickerView.autoScrollFast + */ + fun autoScrollFast(position: Int, duration: Long) { + val speed = dip2px(context, 0.6f).toFloat() + autoScrollFast(position, duration, speed, sAutoScrollInterpolator) + } + + /** + * 滚动到指定位置 + * + * @param toPosition  需要滚动到的位置 + * @param duration  滚动时间 + */ + fun autoScrollToPosition(toPosition: Int, duration: Long, interpolator: Interpolator?) { + var toPosition = toPosition + toPosition %= mAdapter!!.itemCount + val endY = (mSelected - toPosition) * itemHeight + autoScrollTo(endY, duration, interpolator, false) + } + + /** + * @param endY  需要滚动到的位置 + * @param duration  滚动时间 + * @param canIntercept 能否终止滚动,比如触摸屏幕终止滚动 + */ + fun autoScrollTo( + endY: Int, duration: Long, interpolator: Interpolator?, canIntercept: Boolean + ) { + if (isAutoScrolling) { + return + } + val temp = isDisallowTouch + isDisallowTouch = !canIntercept + isAutoScrolling = true + mAutoScrollAnimator.cancel() + mAutoScrollAnimator.setIntValues(0, endY) + mAutoScrollAnimator.interpolator = interpolator + mAutoScrollAnimator.duration = duration + mAutoScrollAnimator.removeAllUpdateListeners() + mAutoScrollAnimator.addUpdateListener { animation -> + var rate = 0f + rate = animation.currentPlayTime * 1f / animation.duration + computeScroll(animation.animatedValue as Int, endY, rate) + } + mAutoScrollAnimator.removeAllListeners() + mAutoScrollAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + isAutoScrolling = false + isDisallowTouch = temp + } + }) + mAutoScrollAnimator.start() + } + + /** + * 停止自动滚动 + */ + fun stopAutoScroll() { + isAutoScrolling = false + mAutoScrollAnimator.cancel() + } + + private class SlotInterpolator : Interpolator { + override fun getInterpolation(input: Float): Float { + return (Math.cos((input + 1) * Math.PI) / 2.0f).toFloat() + 0.5f + } + } + + /** + * 快速滑动时,惯性滑动一段距离 + * + */ + private inner class FlingOnGestureListener : SimpleOnGestureListener() { + private var mIsScrollingLastTime = false + override fun onDown(e: MotionEvent): Boolean { + if (isDisallowInterceptTouch) { // 不允许父组件拦截事件 + val parent = parent + parent?.requestDisallowInterceptTouchEvent(true) + } + mIsScrollingLastTime = isScrolling // 记录是否从滚动状态终止 + // 点击时取消所有滚动效果 + cancelScroll() + mLastMoveY = e.y + mLastMoveX = e.x + return true + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + // 惯性滑动 + if (isInertiaScroll) { + cancelScroll() + if (mIsHorizontal) { + fling(mMoveLength, velocityX) + } else { + fling(mMoveLength, velocityY) + } + } + if (e2.action == MotionEvent.ACTION_UP) { + isTouching = false + } + return true + } + + + /** + * 快速点击,立刻抬起触发。这里用来 + */ + override fun onSingleTapUp(e: MotionEvent): Boolean { + mLastMoveY = e.y + mLastMoveX = e.x + var lastMove = 0f + if (isHorizontal) { + centerPoint = centerX + lastMove = mLastMoveX + } else { + centerPoint = centerY + lastMove = mLastMoveY + } + if (isCanTap && !isScrolling && !mIsScrollingLastTime) { + if (lastMove >= centerPoint && lastMove <= centerPoint + mItemSize) { //点击中间item,回调点击事件 + performClick() + } else if (lastMove < centerPoint) { // 点击两边的item,移动到相应的item + val move = mItemSize + autoScrollTo(move, 150, sAutoScrollInterpolator, false) + } else { // lastMove > mCenterPoint + mItemSize + val move = -mItemSize + autoScrollTo(move, 150, sAutoScrollInterpolator, false) + } + } else { + moveToCenter() + } + isTouching = false + return true + } + } + + var adapter: WheelAdapter? + get() = mAdapter + /** + * 设置数据适配器 + * + * @param adapter adapter + */ + set(adapter) { + mAdapter = adapter + mSelected = 0 + invalidate() + } + val selectedItem: T? + /** + * @return 获取选中的item + */ + get() = mAdapter!!.getItem(mSelected) + var selectedPosition: Int + /** + * @return 获取选中的item position + */ + get() = mSelected + /** + * 选中position + * + * @param position 选中position + */ + set(position) { + setSelectedPosition(position, true) + } + + /** + * 供开发者内部使用,用户不要使用该方法。使用[.setSelectedPosition] + * + * @param isNotify 是否回调[.notifySelected] + */ + fun setSelectedPosition(position: Int, isNotify: Boolean) { + // bugfix: 这里不能判断position == mSelected,因为可能页面滑动了,但是没有点确定,一样不是目标位置 + if (position < 0 || position > mAdapter!!.itemCount - 1 /*|| position == mSelected*/) { + return + } + mSelected = position + invalidate() + if (isNotify /* && mListener != null*/) { + notifySelected() + } + } + + /** + * 设置滑动选中监听 + * + * @param listener 滑动选中监听 + */ + fun setOnSelectedListener(listener: OnSelectedListener?) { + this.listener = listener + } + + /** + * 设置是否循环绘制,如果 adapter.getItemCount < visibleCount,则即使设置为循环,也无效 + * + * @param isCirculation 是否循环 + */ + fun setIsCirculation(isCirculation: Boolean) { + mIsCirculation = isCirculation + } + + var visibleItemCount: Int + get() = mVisibleItemCount + /** + * 设置可见的item count + * + * @param visibleItemCount 可见的item count + */ + set(visibleItemCount) { + mVisibleItemCount = visibleItemCount + // 如果没有 + reset() + invalidate() + } + var itemSize: Int + /** + * @return 当垂直滚动时,mItemSize = mItemHeight;水平滚动时,mItemSize = mItemWidth + */ + get() = mItemSize + /** + * 设置item的高度/宽度 + * + * @param itemSize dp + */ + set(itemSize) { + mItemSize = + dip2px(context, (if (itemSize <= 0) sDefaultItemSize else itemSize).toFloat()) + } + + private fun setSafeCenterPosition(centerPosition: Int) { + mUseDefaultCenterPosition = false + mCenterPosition = if (centerPosition < 0) { + 0 + } else if (centerPosition >= mVisibleItemCount) { + mVisibleItemCount - 1 + } else { + centerPosition + } + } + + var centerPosition: Int + /** + * 中间item的位置,默认为 mVisibleItemCount / 2 + */ + get() = mCenterPosition + /** + * 中间item的位置,0 <= centerPosition <= mVisibleItemCount + */ + set(centerPosition) { + setSafeCenterPosition(centerPosition) + // bugfix 这里应该调用reset + //mCenterY = mCenterPosition * mItemHeight; + reset() + invalidate() + } + + /** + * @return 是否可选择的状态。非滑动及触摸状态 + */ + fun canSelected(): Boolean { + return !isTouching && !isScrolling + } + + val isScrolling: Boolean + get() = isFling || isMovingCenter || isAutoScrolling + var isHorizontal: Boolean + get() = mIsHorizontal + set(horizontal) { + if (mIsHorizontal == horizontal) { + return + } + mIsHorizontal = horizontal + reset() + invalidate() + } + + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + if (visibility == VISIBLE) { + moveToCenter() + } + } + + /** + * 绘制中心指示器 + */ + interface CenterDecoration { + fun drawIndicator( + pickerView: BasePickerView<*>, + canvas: Canvas, + left: Int, + top: Int, + right: Int, + bottom: Int + ) + } + + interface OnSelectedListener { + fun onSelected(pickerView: BasePickerView<*>, position: Int) + } + + fun interface Formatter { + fun format( + pickerView: BasePickerView<*>, position: Int, charSequence: CharSequence? + ): CharSequence? + } + + companion object { + private const val TAG = "BasePickerView" + + /** 默认可见的item个数:5个 */ + var sDefaultVisibleItemCount = 5 + + /** 默认itemSize:50dp */ + var sDefaultItemSize = 50 //dp + + /** 默认是否循环:false */ + var sDefaultIsCirculation = false + + /** 默认值:是否绘制 CenterDecoration */ + var sDefaultDrawIndicator = true + const val DEFAULT_DRAW_INDICATOR_NO_DATA = true + private val sAutoScrollInterpolator = SlotInterpolator() + + /** + * 在一定精度内比较浮点数 + */ + fun equalsFloat(a: Double, b: Double): Boolean { + return a == b || Math.abs(a - b) < 0.01f + } + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/widget/DefaultCenterDecoration.kt b/pickerview/src/main/java/org/jaaksi/pickerview/widget/DefaultCenterDecoration.kt new file mode 100644 index 0000000..0ee5005 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/widget/DefaultCenterDecoration.kt @@ -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

+ * 作者:fuchaoyang

+ * 描述: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 + } +} \ No newline at end of file diff --git a/pickerview/src/main/java/org/jaaksi/pickerview/widget/PickerView.kt b/pickerview/src/main/java/org/jaaksi/pickerview/widget/PickerView.kt new file mode 100644 index 0000000..dfc7c05 --- /dev/null +++ b/pickerview/src/main/java/org/jaaksi/pickerview/widget/PickerView.kt @@ -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 @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : BasePickerView(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()) + } +} \ No newline at end of file diff --git a/pickerview/src/main/res/layout/dialog_pickerview_default.xml b/pickerview/src/main/res/layout/dialog_pickerview_default.xml new file mode 100644 index 0000000..9352657 --- /dev/null +++ b/pickerview/src/main/res/layout/dialog_pickerview_default.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/pickerview/src/main/res/values/attrs.xml b/pickerview/src/main/res/values/attrs.xml new file mode 100644 index 0000000..1dff12b --- /dev/null +++ b/pickerview/src/main/res/values/attrs.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pickerview/src/main/res/values/strings.xml b/pickerview/src/main/res/values/strings.xml new file mode 100644 index 0000000..6d6de00 --- /dev/null +++ b/pickerview/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PickerView + diff --git a/pickerview/src/main/res/values/styles.xml b/pickerview/src/main/res/values/styles.xml new file mode 100644 index 0000000..0da6e43 --- /dev/null +++ b/pickerview/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 63634b4..76b5cab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") \ No newline at end of file